From 74155432cda16615c14f7b23222a622ed83992bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 19 Jul 2023 00:34:06 +0200 Subject: [PATCH 01/23] Add option to preserve spoilers text when replying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/compose.ts | 3 +++ app/soapbox/actions/settings.ts | 1 + app/soapbox/features/forms/index.tsx | 1 + app/soapbox/features/preferences/index.tsx | 10 ++++++++++ app/soapbox/reducers/compose.ts | 5 +++++ 5 files changed, 20 insertions(+) diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts index a8bec65ee..edec9382c 100644 --- a/app/soapbox/actions/compose.ts +++ b/app/soapbox/actions/compose.ts @@ -144,6 +144,7 @@ interface ComposeReplyAction { status: Status account: Account explicitAddressing: boolean + preserveSpoilers: boolean } const replyCompose = (status: Status) => @@ -151,6 +152,7 @@ const replyCompose = (status: Status) => const state = getState(); const instance = state.instance; const { explicitAddressing } = getFeatures(instance); + const preserveSpoilers = !!getSettings(state).get('preserveSpoilers'); const action: ComposeReplyAction = { type: COMPOSE_REPLY, @@ -158,6 +160,7 @@ const replyCompose = (status: Status) => status: status, account: state.accounts.get(state.me)!, explicitAddressing, + preserveSpoilers, }; dispatch(action); diff --git a/app/soapbox/actions/settings.ts b/app/soapbox/actions/settings.ts index f72ec5e96..1e5c241d1 100644 --- a/app/soapbox/actions/settings.ts +++ b/app/soapbox/actions/settings.ts @@ -44,6 +44,7 @@ const defaultSettings = ImmutableMap({ explanationBox: true, autoloadTimelines: true, autoloadMore: true, + preserveSpoilers: false, systemFont: false, demetricator: false, diff --git a/app/soapbox/features/forms/index.tsx b/app/soapbox/features/forms/index.tsx index 564280c9b..df779ef02 100644 --- a/app/soapbox/features/forms/index.tsx +++ b/app/soapbox/features/forms/index.tsx @@ -118,6 +118,7 @@ export const Checkbox: React.FC = (props) => ( ); interface ISelectDropdown { + className?: string label?: React.ReactNode hint?: React.ReactNode items: Record diff --git a/app/soapbox/features/preferences/index.tsx b/app/soapbox/features/preferences/index.tsx index 09ea08eea..cd10fd45a 100644 --- a/app/soapbox/features/preferences/index.tsx +++ b/app/soapbox/features/preferences/index.tsx @@ -136,6 +136,7 @@ const Preferences = () => { }> ) => onSelectChange(event, ['locale'])} @@ -144,6 +145,7 @@ const Preferences = () => { }> ) => onSelectChange(event, ['displayMedia'])} @@ -153,6 +155,7 @@ const Preferences = () => { {features.privacyScopes && ( }> ) => onSelectChange(event, ['defaultPrivacy'])} @@ -163,12 +166,19 @@ const Preferences = () => { {features.richText && ( }> ) => onSelectChange(event, ['defaultContentType'])} /> )} + + {features.spoilers && ( + }> + + + )} diff --git a/app/soapbox/reducers/compose.ts b/app/soapbox/reducers/compose.ts index b5fefa4d1..8478cff28 100644 --- a/app/soapbox/reducers/compose.ts +++ b/app/soapbox/reducers/compose.ts @@ -312,6 +312,11 @@ export default function compose(state = initialState, action: ComposeAction | Me map.set('caretPosition', null); map.set('idempotencyKey', uuid()); map.set('content_type', defaultCompose.content_type); + if (action.preserveSpoilers && action.status.spoiler_text) { + map.set('spoiler', true); + map.set('sensitive', true); + map.set('spoiler_text', action.status.spoiler_text); + } })); case COMPOSE_EVENT_REPLY: return updateCompose(state, action.id, compose => compose.withMutations(map => { From f555128d682064c01c084d6da190585d7fada54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 19 Jul 2023 00:35:02 +0200 Subject: [PATCH 02/23] Update en.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index c5808be64..bfd15580a 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -1196,6 +1196,7 @@ "preferences.fields.language_label": "Display Language", "preferences.fields.media_display_label": "Sensitive content", "preferences.fields.missing_description_modal_label": "Show confirmation dialog before sending a post without media descriptions", + "preferences.fields.preserve_spoilers_label": "Preserve content warning when replying", "preferences.fields.privacy_label": "Default post privacy", "preferences.fields.reduce_motion_label": "Reduce motion in animations", "preferences.fields.system_font_label": "Use system's default font", From 46313049b0ae6d7ffd5c6d8ae36609663ec7a685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 22 Jul 2023 00:04:41 +0200 Subject: [PATCH 03/23] Rename Calckey to Firefish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/utils/features.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 50c606fb4..921715c08 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -17,10 +17,10 @@ const overrides = custom('features'); const any = (arr: Array): boolean => arr.some(Boolean); /** - * Calckey, a fork of Misskey. - * @see {@link https://calckey.org/} + * Firefish, a fork of Misskey. Formerly known as Calckey. + * @see {@link https://joinfirefish.org/} */ -export const CALCKEY = 'Calckey'; +export const FIREFISH = 'Firefish'; /** * Ditto, a Nostr server with Mastodon API. @@ -145,7 +145,7 @@ const getInstanceFeatures = (instance: Instance) => { * @see GET /api/v1/accounts/lookup */ accountLookup: any([ - v.software === CALCKEY, + v.software === FIREFISH, v.software === MASTODON && gte(v.compatVersion, '3.4.0'), v.software === PLEROMA && gte(v.version, '2.4.50'), v.software === TAKAHE && gte(v.version, '0.6.1'), @@ -231,7 +231,7 @@ const getInstanceFeatures = (instance: Instance) => { * @see GET /api/v1/bookmarks */ bookmarks: any([ - v.software === CALCKEY, + v.software === FIREFISH, v.software === FRIENDICA, v.software === MASTODON && gte(v.compatVersion, '3.1.0'), v.software === PLEROMA && gte(v.version, '0.9.9'), @@ -332,7 +332,7 @@ const getInstanceFeatures = (instance: Instance) => { * @see {@link https://docs.joinmastodon.org/methods/conversations/} */ conversations: any([ - v.software === CALCKEY, + v.software === FIREFISH, v.software === FRIENDICA, v.software === MASTODON && gte(v.compatVersion, '2.6.0'), v.software === PLEROMA && gte(v.version, '0.9.9'), @@ -371,7 +371,7 @@ const getInstanceFeatures = (instance: Instance) => { * @see PATCH /api/v1/accounts/update_credentials */ editProfile: any([ - v.software === CALCKEY, + v.software === FIREFISH, v.software === FRIENDICA, v.software === MASTODON, v.software === MITRA, @@ -457,7 +457,7 @@ const getInstanceFeatures = (instance: Instance) => { /** Whether the accounts who favourited or emoji-reacted to a status can be viewed through the API. */ exposableReactions: any([ - v.software === CALCKEY, + v.software === FIREFISH, v.software === FRIENDICA, v.software === MASTODON, v.software === TAKAHE && gte(v.version, '0.6.1'), @@ -637,7 +637,7 @@ const getInstanceFeatures = (instance: Instance) => { * @see GET /api/v1/timelines/list/:list_id */ lists: any([ - v.software === CALCKEY, + v.software === FIREFISH, v.software === FRIENDICA, v.software === MASTODON && gte(v.compatVersion, '2.1.0'), v.software === PLEROMA && gte(v.version, '0.9.9'), @@ -750,7 +750,7 @@ const getInstanceFeatures = (instance: Instance) => { * @see POST /api/v1/statuses */ polls: any([ - v.software === CALCKEY, + v.software === FIREFISH, v.software === MASTODON && gte(v.version, '2.8.0'), v.software === PLEROMA, v.software === TRUTHSOCIAL, @@ -787,7 +787,7 @@ const getInstanceFeatures = (instance: Instance) => { * @see GET /api/v1/timelines/public */ publicTimeline: any([ - v.software === CALCKEY, + v.software === FIREFISH, v.software === FRIENDICA, v.software === MASTODON, v.software === PLEROMA, From 8e3dfce33712f3cb8ed4ef6383344a0ae026a753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 22 Jul 2023 00:15:11 +0200 Subject: [PATCH 04/23] Update changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5cd615cd..3f5f12683 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Reactions: Support custom emoji reactions - Compatbility: Support Mastodon v2 timeline filters. - Compatbility: Preliminary support for Ditto backend. +- Compatibility: Support Firefish. - Posts: Support dislikes on Friendica. - UI: added a character counter to some textareas. - UI: added new experience for viewing Media +- Hotkeys: Added `/` as a hotkey for search field. ### Changed - Posts: truncate Nostr pubkeys in reply mentions. @@ -24,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - UI: added sticky column header. - UI: add specific zones the user can drag-and-drop files. - UI: disable toast notifications for API errors. +- Chats: Display year for older messages creation date. ### Fixed - Posts: fixed emojis being cut off in reactions modal. @@ -37,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - UI: fixed various overflow issues related to long usernames. - UI: fixed display of Markdown code blocks in the reply indicator. - Auth: fixed too many API requests when the server has an error. +- Auth: Don't display "username or e-mail" if username is not allowed. ## [3.2.0] - 2023-02-15 From 77f0f4d3772b65db7851f90f7c7103c327cb1729 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 12:49:02 -0500 Subject: [PATCH 05/23] 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(() => { From 757f6600f88989dfbe76f53168564442daa2e40d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 12:53:06 -0500 Subject: [PATCH 06/23] Streaming: remove unused code --- app/soapbox/actions/streaming.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index 505c9445d..1e5e65060 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -10,18 +10,16 @@ import { connectStream } from '../stream'; import { deleteAnnouncement, - fetchAnnouncements, updateAnnouncements, updateReaction as updateAnnouncementsReaction, } from './announcements'; import { updateConversations } from './conversations'; import { fetchFilters } from './filters'; import { MARKER_FETCH_SUCCESS } from './markers'; -import { updateNotificationsQueue, expandNotifications } from './notifications'; +import { updateNotificationsQueue } from './notifications'; import { updateStatus } from './statuses'; import { // deleteFromTimelines, - expandHomeTimeline, connectTimeline, disconnectTimeline, processTimelineUpdate, @@ -192,14 +190,6 @@ const connectTimelineStream = ( }; }); -const refreshHomeTimelineAndNotification = (dispatch: AppDispatch, done?: () => void) => - dispatch(expandHomeTimeline({}, () => - dispatch(expandNotifications({}, () => - dispatch(fetchAnnouncements(done)))))); - -const connectUserStream = (opts?: TimelineStreamOpts) => - connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification, null, opts); - const connectCommunityStream = ({ onlyMedia }: Record = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); @@ -221,14 +211,10 @@ const connectListStream = (id: string) => const connectGroupStream = (id: string) => connectTimelineStream(`group:${id}`, `group&group=${id}`); -const connectNostrStream = () => - connectTimelineStream('nostr', 'nostr'); - export { STREAMING_CHAT_UPDATE, STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, connectTimelineStream, - connectUserStream, connectCommunityStream, connectPublicStream, connectRemoteStream, @@ -236,6 +222,5 @@ export { connectDirectStream, connectListStream, connectGroupStream, - connectNostrStream, type TimelineStreamOpts, }; From 3cef200a44c9cadeff6a4573f0b49edb25932789 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 13:05:41 -0500 Subject: [PATCH 07/23] Add useCommunityStream hook, refresh socket when timelineId or path changes --- app/soapbox/actions/streaming.ts | 4 ---- app/soapbox/api/hooks/index.ts | 1 + .../api/hooks/streaming/useCommunityStream.ts | 14 ++++++++++++++ .../api/hooks/streaming/useTimelineStream.ts | 3 ++- app/soapbox/features/community-timeline/index.tsx | 15 ++++++--------- 5 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 app/soapbox/api/hooks/streaming/useCommunityStream.ts diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index 1e5e65060..90b73569e 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -190,9 +190,6 @@ const connectTimelineStream = ( }; }); -const connectCommunityStream = ({ onlyMedia }: Record = {}) => - connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); - const connectPublicStream = ({ onlyMedia }: Record = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); @@ -215,7 +212,6 @@ export { STREAMING_CHAT_UPDATE, STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, connectTimelineStream, - connectCommunityStream, connectPublicStream, connectRemoteStream, connectHashtagStream, diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index 47a9101a6..6cd1802ba 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -46,4 +46,5 @@ export { useUpdateGroupTag } from './groups/useUpdateGroupTag'; // Streaming export { useUserStream } from './streaming/useUserStream'; +export { useCommunityStream } from './streaming/useCommunityStream'; export { useNostrStream } from './streaming/useNostrStream'; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/useCommunityStream.ts b/app/soapbox/api/hooks/streaming/useCommunityStream.ts new file mode 100644 index 000000000..f0ccca5d6 --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useCommunityStream.ts @@ -0,0 +1,14 @@ +import { useTimelineStream } from './useTimelineStream'; + +interface UseCommunityStreamOpts { + onlyMedia?: boolean +} + +function useCommunityStream({ onlyMedia }: UseCommunityStreamOpts = {}) { + return useTimelineStream( + `community${onlyMedia ? ':media' : ''}`, + `public:local${onlyMedia ? ':media' : ''}`, + ); +} + +export { useCommunityStream }; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/useTimelineStream.ts b/app/soapbox/api/hooks/streaming/useTimelineStream.ts index 0e7a2a267..a0dfb2ded 100644 --- a/app/soapbox/api/hooks/streaming/useTimelineStream.ts +++ b/app/soapbox/api/hooks/streaming/useTimelineStream.ts @@ -6,6 +6,7 @@ import { getAccessToken } from 'soapbox/utils/auth'; function useTimelineStream(...args: Parameters) { // TODO: get rid of streaming.ts and move the actual opts here. + const [timelineId, path] = args; const { enabled = true } = args[4] ?? {}; const dispatch = useAppDispatch(); @@ -31,7 +32,7 @@ function useTimelineStream(...args: Parameters) { useEffect(() => { connect(); return disconnect; - }, [accessToken, streamingUrl, enabled]); + }, [accessToken, streamingUrl, timelineId, path, enabled]); return { disconnect, diff --git a/app/soapbox/features/community-timeline/index.tsx b/app/soapbox/features/community-timeline/index.tsx index 387297a80..37f4e8dd2 100644 --- a/app/soapbox/features/community-timeline/index.tsx +++ b/app/soapbox/features/community-timeline/index.tsx @@ -1,8 +1,8 @@ import React, { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { connectCommunityStream } from 'soapbox/actions/streaming'; import { expandCommunityTimeline } from 'soapbox/actions/timelines'; +import { useCommunityStream } from 'soapbox/api/hooks'; import PullToRefresh from 'soapbox/components/pull-to-refresh'; import { Column } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks'; @@ -18,7 +18,7 @@ const CommunityTimeline = () => { const dispatch = useAppDispatch(); const settings = useSettings(); - const onlyMedia = settings.getIn(['community', 'other', 'onlyMedia']); + const onlyMedia = !!settings.getIn(['community', 'other', 'onlyMedia'], false); const next = useAppSelector(state => state.timelines.get('community')?.next); const timelineId = 'community'; @@ -28,16 +28,13 @@ const CommunityTimeline = () => { }; const handleRefresh = () => { - return dispatch(expandCommunityTimeline({ onlyMedia } as any)); + return dispatch(expandCommunityTimeline({ onlyMedia })); }; - useEffect(() => { - dispatch(expandCommunityTimeline({ onlyMedia } as any)); - const disconnect = dispatch(connectCommunityStream({ onlyMedia } as any)); + useCommunityStream({ onlyMedia }); - return () => { - disconnect(); - }; + useEffect(() => { + dispatch(expandCommunityTimeline({ onlyMedia })); }, [onlyMedia]); return ( From d99e266008875237998bae736b2398ad0f77ce1c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 13:09:45 -0500 Subject: [PATCH 08/23] Add usePublicStream hook --- app/soapbox/actions/streaming.ts | 4 ---- app/soapbox/api/hooks/index.ts | 1 + .../api/hooks/streaming/usePublicStream.ts | 14 ++++++++++++++ app/soapbox/features/public-timeline/index.tsx | 15 ++++++--------- 4 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 app/soapbox/api/hooks/streaming/usePublicStream.ts diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index 90b73569e..b828c5818 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -190,9 +190,6 @@ const connectTimelineStream = ( }; }); -const connectPublicStream = ({ onlyMedia }: Record = {}) => - connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); - const connectRemoteStream = (instance: string, { onlyMedia }: Record = {}) => connectTimelineStream(`remote${onlyMedia ? ':media' : ''}:${instance}`, `public:remote${onlyMedia ? ':media' : ''}&instance=${instance}`); @@ -212,7 +209,6 @@ export { STREAMING_CHAT_UPDATE, STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, connectTimelineStream, - connectPublicStream, connectRemoteStream, connectHashtagStream, connectDirectStream, diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index 6cd1802ba..ea473df1b 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -47,4 +47,5 @@ export { useUpdateGroupTag } from './groups/useUpdateGroupTag'; // Streaming export { useUserStream } from './streaming/useUserStream'; export { useCommunityStream } from './streaming/useCommunityStream'; +export { usePublicStream } from './streaming/usePublicStream'; export { useNostrStream } from './streaming/useNostrStream'; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/usePublicStream.ts b/app/soapbox/api/hooks/streaming/usePublicStream.ts new file mode 100644 index 000000000..eb189c996 --- /dev/null +++ b/app/soapbox/api/hooks/streaming/usePublicStream.ts @@ -0,0 +1,14 @@ +import { useTimelineStream } from './useTimelineStream'; + +interface UsePublicStreamOpts { + onlyMedia?: boolean +} + +function usePublicStream({ onlyMedia }: UsePublicStreamOpts = {}) { + return useTimelineStream( + `public${onlyMedia ? ':media' : ''}`, + `public${onlyMedia ? ':media' : ''}`, + ); +} + +export { usePublicStream }; \ No newline at end of file diff --git a/app/soapbox/features/public-timeline/index.tsx b/app/soapbox/features/public-timeline/index.tsx index cad8cd7f6..b08c2ed6f 100644 --- a/app/soapbox/features/public-timeline/index.tsx +++ b/app/soapbox/features/public-timeline/index.tsx @@ -3,8 +3,8 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; import { changeSetting } from 'soapbox/actions/settings'; -import { connectPublicStream } from 'soapbox/actions/streaming'; import { expandPublicTimeline } from 'soapbox/actions/timelines'; +import { usePublicStream } from 'soapbox/api/hooks'; import PullToRefresh from 'soapbox/components/pull-to-refresh'; import { Accordion, Column } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch, useInstance, useSettings } from 'soapbox/hooks'; @@ -23,7 +23,7 @@ const CommunityTimeline = () => { const instance = useInstance(); const settings = useSettings(); - const onlyMedia = settings.getIn(['public', 'other', 'onlyMedia']); + const onlyMedia = !!settings.getIn(['public', 'other', 'onlyMedia'], false); const next = useAppSelector(state => state.timelines.get('public')?.next); const timelineId = 'public'; @@ -44,16 +44,13 @@ const CommunityTimeline = () => { }; const handleRefresh = () => { - return dispatch(expandPublicTimeline({ onlyMedia } as any)); + return dispatch(expandPublicTimeline({ onlyMedia })); }; - useEffect(() => { - dispatch(expandPublicTimeline({ onlyMedia } as any)); - const disconnect = dispatch(connectPublicStream({ onlyMedia })); + usePublicStream({ onlyMedia }); - return () => { - disconnect(); - }; + useEffect(() => { + dispatch(expandPublicTimeline({ onlyMedia })); }, [onlyMedia]); return ( From 9b1352f0adbaa7ad47bafe8d7d4347ba1b8c7112 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 13:16:01 -0500 Subject: [PATCH 09/23] Add useRemoteStream hook --- app/soapbox/actions/streaming.ts | 4 ---- app/soapbox/api/hooks/index.ts | 1 + .../api/hooks/streaming/useRemoteStream.ts | 15 ++++++++++++ .../features/remote-timeline/index.tsx | 23 ++++--------------- 4 files changed, 21 insertions(+), 22 deletions(-) create mode 100644 app/soapbox/api/hooks/streaming/useRemoteStream.ts diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index b828c5818..8173752cf 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -190,9 +190,6 @@ const connectTimelineStream = ( }; }); -const connectRemoteStream = (instance: string, { onlyMedia }: Record = {}) => - connectTimelineStream(`remote${onlyMedia ? ':media' : ''}:${instance}`, `public:remote${onlyMedia ? ':media' : ''}&instance=${instance}`); - const connectHashtagStream = (id: string, tag: string, accept: (status: APIEntity) => boolean) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); @@ -209,7 +206,6 @@ export { STREAMING_CHAT_UPDATE, STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, connectTimelineStream, - connectRemoteStream, connectHashtagStream, connectDirectStream, connectListStream, diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index ea473df1b..9569318a7 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -48,4 +48,5 @@ export { useUpdateGroupTag } from './groups/useUpdateGroupTag'; export { useUserStream } from './streaming/useUserStream'; export { useCommunityStream } from './streaming/useCommunityStream'; export { usePublicStream } from './streaming/usePublicStream'; +export { useRemoteStream } from './streaming/useRemoteStream'; export { useNostrStream } from './streaming/useNostrStream'; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/useRemoteStream.ts b/app/soapbox/api/hooks/streaming/useRemoteStream.ts new file mode 100644 index 000000000..f67f99083 --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useRemoteStream.ts @@ -0,0 +1,15 @@ +import { useTimelineStream } from './useTimelineStream'; + +interface UseRemoteStreamOpts { + instance: string + onlyMedia?: boolean +} + +function useRemoteStream({ instance, onlyMedia }: UseRemoteStreamOpts) { + return useTimelineStream( + `remote${onlyMedia ? ':media' : ''}:${instance}`, + `public:remote${onlyMedia ? ':media' : ''}&instance=${instance}`, + ); +} + +export { useRemoteStream }; \ No newline at end of file diff --git a/app/soapbox/features/remote-timeline/index.tsx b/app/soapbox/features/remote-timeline/index.tsx index b0afd38a8..87fed22ec 100644 --- a/app/soapbox/features/remote-timeline/index.tsx +++ b/app/soapbox/features/remote-timeline/index.tsx @@ -1,9 +1,9 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; import { useHistory } from 'react-router-dom'; -import { connectRemoteStream } from 'soapbox/actions/streaming'; import { expandRemoteTimeline } from 'soapbox/actions/timelines'; +import { useRemoteStream } from 'soapbox/api/hooks'; import IconButton from 'soapbox/components/icon-button'; import { Column, HStack, Text } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks'; @@ -26,20 +26,12 @@ const RemoteTimeline: React.FC = ({ params }) => { const instance = params?.instance as string; const settings = useSettings(); - const stream = useRef(null); - const timelineId = 'remote'; const onlyMedia = !!settings.getIn(['remote', 'other', 'onlyMedia']); const next = useAppSelector(state => state.timelines.get('remote')?.next); const pinned: boolean = (settings.getIn(['remote_timeline', 'pinnedHosts']) as any).includes(instance); - const disconnect = () => { - if (stream.current) { - stream.current(); - } - }; - const handleCloseClick: React.MouseEventHandler = () => { history.push('/timeline/fediverse'); }; @@ -48,15 +40,10 @@ const RemoteTimeline: React.FC = ({ params }) => { dispatch(expandRemoteTimeline(instance, { url: next, maxId, onlyMedia })); }; - useEffect(() => { - disconnect(); - dispatch(expandRemoteTimeline(instance, { onlyMedia, maxId: undefined })); - stream.current = dispatch(connectRemoteStream(instance, { onlyMedia })); + useRemoteStream({ instance, onlyMedia }); - return () => { - disconnect(); - stream.current = null; - }; + useEffect(() => { + dispatch(expandRemoteTimeline(instance, { onlyMedia, maxId: undefined })); }, [onlyMedia]); return ( From 4090d6ab51c11682d43bff92ca576e093e62e4a2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 14:00:00 -0500 Subject: [PATCH 10/23] Add useDirectStream hook --- app/soapbox/actions/streaming.ts | 4 ---- app/soapbox/api/hooks/index.ts | 1 + app/soapbox/api/hooks/streaming/useDirectStream.ts | 7 +++++++ app/soapbox/features/conversations/index.tsx | 7 +++---- app/soapbox/features/direct-timeline/index.tsx | 9 +++------ 5 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 app/soapbox/api/hooks/streaming/useDirectStream.ts diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index 8173752cf..1d71d8d98 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -193,9 +193,6 @@ const connectTimelineStream = ( const connectHashtagStream = (id: string, tag: string, accept: (status: APIEntity) => boolean) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); -const connectDirectStream = () => - connectTimelineStream('direct', 'direct'); - const connectListStream = (id: string) => connectTimelineStream(`list:${id}`, `list&list=${id}`); @@ -207,7 +204,6 @@ export { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, connectTimelineStream, connectHashtagStream, - connectDirectStream, connectListStream, connectGroupStream, type TimelineStreamOpts, diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index 9569318a7..214607f6d 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -48,5 +48,6 @@ export { useUpdateGroupTag } from './groups/useUpdateGroupTag'; export { useUserStream } from './streaming/useUserStream'; export { useCommunityStream } from './streaming/useCommunityStream'; export { usePublicStream } from './streaming/usePublicStream'; +export { useDirectStream } from './streaming/useDirectStream'; export { useRemoteStream } from './streaming/useRemoteStream'; export { useNostrStream } from './streaming/useNostrStream'; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/useDirectStream.ts b/app/soapbox/api/hooks/streaming/useDirectStream.ts new file mode 100644 index 000000000..e97fc8860 --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useDirectStream.ts @@ -0,0 +1,7 @@ +import { useTimelineStream } from './useTimelineStream'; + +function useDirectStream() { + return useTimelineStream('direct', 'direct'); +} + +export { useDirectStream }; \ No newline at end of file diff --git a/app/soapbox/features/conversations/index.tsx b/app/soapbox/features/conversations/index.tsx index 12b418eb0..1f6774ab9 100644 --- a/app/soapbox/features/conversations/index.tsx +++ b/app/soapbox/features/conversations/index.tsx @@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl'; import { directComposeById } from 'soapbox/actions/compose'; import { mountConversations, unmountConversations, expandConversations } from 'soapbox/actions/conversations'; -import { connectDirectStream } from 'soapbox/actions/streaming'; +import { useDirectStream } from 'soapbox/api/hooks'; import AccountSearch from 'soapbox/components/account-search'; import { Column } from 'soapbox/components/ui'; import { useAppDispatch } from 'soapbox/hooks'; @@ -19,15 +19,14 @@ const ConversationsTimeline = () => { const intl = useIntl(); const dispatch = useAppDispatch(); + useDirectStream(); + useEffect(() => { dispatch(mountConversations()); dispatch(expandConversations()); - const disconnect = dispatch(connectDirectStream()); - return () => { dispatch(unmountConversations()); - disconnect(); }; }, []); diff --git a/app/soapbox/features/direct-timeline/index.tsx b/app/soapbox/features/direct-timeline/index.tsx index eee31a829..ba8ba1cb0 100644 --- a/app/soapbox/features/direct-timeline/index.tsx +++ b/app/soapbox/features/direct-timeline/index.tsx @@ -2,8 +2,8 @@ import React, { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { directComposeById } from 'soapbox/actions/compose'; -import { connectDirectStream } from 'soapbox/actions/streaming'; import { expandDirectTimeline } from 'soapbox/actions/timelines'; +import { useDirectStream } from 'soapbox/api/hooks'; import AccountSearch from 'soapbox/components/account-search'; import { Column } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; @@ -20,13 +20,10 @@ const DirectTimeline = () => { const dispatch = useAppDispatch(); const next = useAppSelector(state => state.timelines.get('direct')?.next); + useDirectStream(); + useEffect(() => { dispatch(expandDirectTimeline()); - const disconnect = dispatch(connectDirectStream()); - - return (() => { - disconnect(); - }); }, []); const handleSuggestion = (accountId: string) => { From 811a9af670bc3ede47cb4ad001f6aa2a7e64e586 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 14:03:00 -0500 Subject: [PATCH 11/23] Add useListStream hook --- app/soapbox/actions/streaming.ts | 4 ---- app/soapbox/api/hooks/index.ts | 1 + app/soapbox/api/hooks/streaming/useListStream.ts | 10 ++++++++++ app/soapbox/features/list-timeline/index.tsx | 10 +++------- 4 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 app/soapbox/api/hooks/streaming/useListStream.ts diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index 1d71d8d98..1367187eb 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -193,9 +193,6 @@ const connectTimelineStream = ( const connectHashtagStream = (id: string, tag: string, accept: (status: APIEntity) => boolean) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); -const connectListStream = (id: string) => - connectTimelineStream(`list:${id}`, `list&list=${id}`); - const connectGroupStream = (id: string) => connectTimelineStream(`group:${id}`, `group&group=${id}`); @@ -204,7 +201,6 @@ export { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, connectTimelineStream, connectHashtagStream, - connectListStream, connectGroupStream, type TimelineStreamOpts, }; diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index 214607f6d..1983c59f2 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -49,5 +49,6 @@ export { useUserStream } from './streaming/useUserStream'; export { useCommunityStream } from './streaming/useCommunityStream'; export { usePublicStream } from './streaming/usePublicStream'; export { useDirectStream } from './streaming/useDirectStream'; +export { useListStream } from './streaming/useListStream'; export { useRemoteStream } from './streaming/useRemoteStream'; export { useNostrStream } from './streaming/useNostrStream'; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/useListStream.ts b/app/soapbox/api/hooks/streaming/useListStream.ts new file mode 100644 index 000000000..f38b0209c --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useListStream.ts @@ -0,0 +1,10 @@ +import { useTimelineStream } from './useTimelineStream'; + +function useListStream(listId: string) { + return useTimelineStream( + `list:${listId}`, + `list&list=${listId}`, + ); +} + +export { useListStream }; \ No newline at end of file diff --git a/app/soapbox/features/list-timeline/index.tsx b/app/soapbox/features/list-timeline/index.tsx index f16acf18b..e9b56aada 100644 --- a/app/soapbox/features/list-timeline/index.tsx +++ b/app/soapbox/features/list-timeline/index.tsx @@ -4,8 +4,8 @@ import { useParams } from 'react-router-dom'; import { fetchList } from 'soapbox/actions/lists'; import { openModal } from 'soapbox/actions/modals'; -import { connectListStream } from 'soapbox/actions/streaming'; import { expandListTimeline } from 'soapbox/actions/timelines'; +import { useListStream } from 'soapbox/api/hooks'; import MissingIndicator from 'soapbox/components/missing-indicator'; import { Column, Button, Spinner } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; @@ -19,15 +19,11 @@ const ListTimeline: React.FC = () => { const list = useAppSelector((state) => state.lists.get(id)); const next = useAppSelector(state => state.timelines.get(`list:${id}`)?.next); + useListStream(id); + useEffect(() => { dispatch(fetchList(id)); dispatch(expandListTimeline(id)); - - const disconnect = dispatch(connectListStream(id)); - - return () => { - disconnect(); - }; }, [id]); const handleLoadMore = (maxId: string) => { From 4a4a2d1a8713940a3d250779f1ba945d43dcbf2a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 14:06:15 -0500 Subject: [PATCH 12/23] Add useGroupStream hook --- app/soapbox/actions/streaming.ts | 4 ---- app/soapbox/api/hooks/index.ts | 1 + app/soapbox/api/hooks/streaming/useGroupStream.ts | 10 ++++++++++ app/soapbox/features/group/group-timeline.tsx | 11 +++-------- 4 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 app/soapbox/api/hooks/streaming/useGroupStream.ts diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index 1367187eb..c96176960 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -193,14 +193,10 @@ const connectTimelineStream = ( const connectHashtagStream = (id: string, tag: string, accept: (status: APIEntity) => boolean) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); -const connectGroupStream = (id: string) => - connectTimelineStream(`group:${id}`, `group&group=${id}`); - export { STREAMING_CHAT_UPDATE, STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, connectTimelineStream, connectHashtagStream, - connectGroupStream, type TimelineStreamOpts, }; diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index 1983c59f2..2809bd8f8 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -50,5 +50,6 @@ export { useCommunityStream } from './streaming/useCommunityStream'; export { usePublicStream } from './streaming/usePublicStream'; export { useDirectStream } from './streaming/useDirectStream'; export { useListStream } from './streaming/useListStream'; +export { useGroupStream } from './streaming/useGroupStream'; export { useRemoteStream } from './streaming/useRemoteStream'; export { useNostrStream } from './streaming/useNostrStream'; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/useGroupStream.ts b/app/soapbox/api/hooks/streaming/useGroupStream.ts new file mode 100644 index 000000000..f9db3f69e --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useGroupStream.ts @@ -0,0 +1,10 @@ +import { useTimelineStream } from './useTimelineStream'; + +function useGroupStream(groupId: string) { + return useTimelineStream( + `group:${groupId}`, + `group&group=${groupId}`, + ); +} + +export { useGroupStream }; \ No newline at end of file diff --git a/app/soapbox/features/group/group-timeline.tsx b/app/soapbox/features/group/group-timeline.tsx index 75b7a95d3..b10c41f9e 100644 --- a/app/soapbox/features/group/group-timeline.tsx +++ b/app/soapbox/features/group/group-timeline.tsx @@ -4,9 +4,8 @@ import { FormattedMessage, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; import { groupCompose, setGroupTimelineVisible, uploadCompose } from 'soapbox/actions/compose'; -import { connectGroupStream } from 'soapbox/actions/streaming'; import { expandGroupFeaturedTimeline, expandGroupTimeline } from 'soapbox/actions/timelines'; -import { useGroup } from 'soapbox/api/hooks'; +import { useGroup, useGroupStream } from 'soapbox/api/hooks'; import { Avatar, HStack, Icon, Stack, Text, Toggle } from 'soapbox/components/ui'; import ComposeForm from 'soapbox/features/compose/components/compose-form'; import { useAppDispatch, useAppSelector, useDraggedFiles, useOwnAccount } from 'soapbox/hooks'; @@ -49,16 +48,12 @@ const GroupTimeline: React.FC = (props) => { dispatch(setGroupTimelineVisible(composeId, !groupTimelineVisible)); }; + useGroupStream(groupId); + useEffect(() => { dispatch(expandGroupTimeline(groupId)); dispatch(expandGroupFeaturedTimeline(groupId)); dispatch(groupCompose(composeId, groupId)); - - const disconnect = dispatch(connectGroupStream(groupId)); - - return () => { - disconnect(); - }; }, [groupId]); if (!group) { From 53c8858fa67484af89ccd3c22928063e1bd65dcb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 14:41:50 -0500 Subject: [PATCH 13/23] Add useHashtagStream hook, clean up hashtags timeline (remove unused code for fetching multiple hashtags) --- app/soapbox/actions/streaming.ts | 4 - app/soapbox/api/hooks/index.ts | 1 + .../api/hooks/streaming/useHashtagStream.ts | 10 ++ .../features/hashtag-timeline/index.tsx | 94 +++---------------- 4 files changed, 23 insertions(+), 86 deletions(-) create mode 100644 app/soapbox/api/hooks/streaming/useHashtagStream.ts diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index c96176960..f050c7b15 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -190,13 +190,9 @@ const connectTimelineStream = ( }; }); -const connectHashtagStream = (id: string, tag: string, accept: (status: APIEntity) => boolean) => - connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); - export { STREAMING_CHAT_UPDATE, STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, connectTimelineStream, - connectHashtagStream, type TimelineStreamOpts, }; diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index 2809bd8f8..ee5733c9f 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -49,6 +49,7 @@ export { useUserStream } from './streaming/useUserStream'; export { useCommunityStream } from './streaming/useCommunityStream'; export { usePublicStream } from './streaming/usePublicStream'; export { useDirectStream } from './streaming/useDirectStream'; +export { useHashtagStream } from './streaming/useHashtagStream'; export { useListStream } from './streaming/useListStream'; export { useGroupStream } from './streaming/useGroupStream'; export { useRemoteStream } from './streaming/useRemoteStream'; diff --git a/app/soapbox/api/hooks/streaming/useHashtagStream.ts b/app/soapbox/api/hooks/streaming/useHashtagStream.ts new file mode 100644 index 000000000..4f9483bad --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useHashtagStream.ts @@ -0,0 +1,10 @@ +import { useTimelineStream } from './useTimelineStream'; + +function useHashtagStream(tag: string) { + return useTimelineStream( + `hashtag:${tag}`, + `hashtag&tag=${tag}`, + ); +} + +export { useHashtagStream }; \ No newline at end of file diff --git a/app/soapbox/features/hashtag-timeline/index.tsx b/app/soapbox/features/hashtag-timeline/index.tsx index bf906ce01..69cc0cd60 100644 --- a/app/soapbox/features/hashtag-timeline/index.tsx +++ b/app/soapbox/features/hashtag-timeline/index.tsx @@ -1,96 +1,31 @@ -import React, { useEffect, useRef } from 'react'; -import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; -import { connectHashtagStream } from 'soapbox/actions/streaming'; import { fetchHashtag, followHashtag, unfollowHashtag } from 'soapbox/actions/tags'; import { expandHashtagTimeline, clearTimeline } from 'soapbox/actions/timelines'; +import { useHashtagStream } from 'soapbox/api/hooks'; import List, { ListItem } from 'soapbox/components/list'; import { Column, Toggle } from 'soapbox/components/ui'; import Timeline from 'soapbox/features/ui/components/timeline'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; -import type { Tag as TagEntity } from 'soapbox/types/entities'; - -type Mode = 'any' | 'all' | 'none'; - -type Tag = { value: string }; -type Tags = { [k in Mode]: Tag[] }; - -const messages = defineMessages({ - any: { id: 'hashtag.column_header.tag_mode.any', defaultMessage: 'or {additional}' }, - all: { id: 'hashtag.column_header.tag_mode.all', defaultMessage: 'and {additional}' }, - none: { id: 'hashtag.column_header.tag_mode.none', defaultMessage: 'without {additional}' }, - empty: { id: 'empty_column.hashtag', defaultMessage: 'There is nothing in this hashtag yet.' }, -}); - interface IHashtagTimeline { params?: { id?: string - tags?: Tags } } export const HashtagTimeline: React.FC = ({ params }) => { - const intl = useIntl(); const id = params?.id || ''; - const tags = params?.tags || { any: [], all: [], none: [] }; - + const features = useFeatures(); const dispatch = useAppDispatch(); - const disconnects = useRef<(() => void)[]>([]); const tag = useAppSelector((state) => state.tags.get(id)); const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next); - // Mastodon supports displaying results from multiple hashtags. - // https://github.com/mastodon/mastodon/issues/6359 - const title = (): string => { - const title: string[] = [`#${id}`]; - - if (additionalFor('any')) { - title.push(' ', intl.formatMessage(messages.any, { additional: additionalFor('any') })); - } - - if (additionalFor('all')) { - title.push(' ', intl.formatMessage(messages.any, { additional: additionalFor('all') })); - } - - if (additionalFor('none')) { - title.push(' ', intl.formatMessage(messages.any, { additional: additionalFor('none') })); - } - - return title.join(''); - }; - - const additionalFor = (mode: Mode) => { - if (tags && (tags[mode] || []).length > 0) { - return tags[mode].map(tag => tag.value).join('/'); - } else { - return ''; - } - }; - - const subscribe = () => { - const any = tags.any.map(tag => tag.value); - const all = tags.all.map(tag => tag.value); - const none = tags.none.map(tag => tag.value); - - [id, ...any].map(tag => { - disconnects.current.push(dispatch(connectHashtagStream(id, tag, status => { - const tags = status.tags.map((tag: TagEntity) => tag.name); - - return all.filter(tag => tags.includes(tag)).length === all.length && - none.filter(tag => tags.includes(tag)).length === 0; - }))); - }); - }; - - const unsubscribe = () => { - disconnects.current.map(disconnect => disconnect()); - disconnects.current = []; - }; const handleLoadMore = (maxId: string) => { - dispatch(expandHashtagTimeline(id, { url: next, maxId, tags })); + dispatch(expandHashtagTimeline(id, { url: next, maxId })); }; const handleFollow = () => { @@ -101,25 +36,20 @@ export const HashtagTimeline: React.FC = ({ params }) => { } }; - useEffect(() => { - subscribe(); - dispatch(expandHashtagTimeline(id, { tags })); - dispatch(fetchHashtag(id)); + useHashtagStream(id); - return () => { - unsubscribe(); - }; + useEffect(() => { + dispatch(expandHashtagTimeline(id)); + dispatch(fetchHashtag(id)); }, []); useEffect(() => { - unsubscribe(); - subscribe(); dispatch(clearTimeline(`hashtag:${id}`)); - dispatch(expandHashtagTimeline(id, { tags })); + dispatch(expandHashtagTimeline(id)); }, [id]); return ( - + {features.followHashtags && ( = ({ params }) => { scrollKey='hashtag_timeline' timelineId={`hashtag:${id}`} onLoadMore={handleLoadMore} - emptyMessage={intl.formatMessage(messages.empty)} + emptyMessage={} divideType='space' /> From 85c8f674b4926d03ac8edfbf3622ea76c9289850 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 14:55:21 -0500 Subject: [PATCH 14/23] Streaming: allow connecting if not logged in, gate certain topics --- .../api/hooks/streaming/useDirectStream.ts | 12 +++++++++++- app/soapbox/api/hooks/streaming/useListStream.ts | 7 +++++++ app/soapbox/api/hooks/streaming/useNostrStream.ts | 15 ++++++++++++--- .../api/hooks/streaming/useTimelineStream.ts | 2 +- app/soapbox/api/hooks/streaming/useUserStream.ts | 11 ++++++++++- 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/app/soapbox/api/hooks/streaming/useDirectStream.ts b/app/soapbox/api/hooks/streaming/useDirectStream.ts index e97fc8860..9d3b47853 100644 --- a/app/soapbox/api/hooks/streaming/useDirectStream.ts +++ b/app/soapbox/api/hooks/streaming/useDirectStream.ts @@ -1,7 +1,17 @@ +import { useLoggedIn } from 'soapbox/hooks'; + import { useTimelineStream } from './useTimelineStream'; function useDirectStream() { - return useTimelineStream('direct', 'direct'); + const { isLoggedIn } = useLoggedIn(); + + return useTimelineStream( + 'direct', + 'direct', + null, + null, + { enabled: isLoggedIn }, + ); } export { useDirectStream }; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/useListStream.ts b/app/soapbox/api/hooks/streaming/useListStream.ts index f38b0209c..661bdce4f 100644 --- a/app/soapbox/api/hooks/streaming/useListStream.ts +++ b/app/soapbox/api/hooks/streaming/useListStream.ts @@ -1,9 +1,16 @@ +import { useLoggedIn } from 'soapbox/hooks'; + import { useTimelineStream } from './useTimelineStream'; function useListStream(listId: string) { + const { isLoggedIn } = useLoggedIn(); + return useTimelineStream( `list:${listId}`, `list&list=${listId}`, + null, + null, + { enabled: isLoggedIn }, ); } diff --git a/app/soapbox/api/hooks/streaming/useNostrStream.ts b/app/soapbox/api/hooks/streaming/useNostrStream.ts index 3e6ef707f..6748f95ea 100644 --- a/app/soapbox/api/hooks/streaming/useNostrStream.ts +++ b/app/soapbox/api/hooks/streaming/useNostrStream.ts @@ -1,11 +1,20 @@ -import { useFeatures } from 'soapbox/hooks'; +import { useFeatures, useLoggedIn } 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 }); + const { isLoggedIn } = useLoggedIn(); + + return useTimelineStream( + 'nostr', + 'nostr', + null, + null, + { + enabled: isLoggedIn && features.nostrSign && Boolean(window.nostr), + }, + ); } 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 index a0dfb2ded..28998e090 100644 --- a/app/soapbox/api/hooks/streaming/useTimelineStream.ts +++ b/app/soapbox/api/hooks/streaming/useTimelineStream.ts @@ -17,7 +17,7 @@ function useTimelineStream(...args: Parameters) { const streamingUrl = instance.urls.get('streaming_api'); const connect = () => { - if (enabled && accessToken && streamingUrl && !stream.current) { + if (enabled && streamingUrl && !stream.current) { stream.current = dispatch(connectTimelineStream(...args)); } }; diff --git a/app/soapbox/api/hooks/streaming/useUserStream.ts b/app/soapbox/api/hooks/streaming/useUserStream.ts index 5e0bb9aed..cededf2aa 100644 --- a/app/soapbox/api/hooks/streaming/useUserStream.ts +++ b/app/soapbox/api/hooks/streaming/useUserStream.ts @@ -2,14 +2,23 @@ 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 { useLoggedIn } from 'soapbox/hooks'; import { useTimelineStream } from './useTimelineStream'; import type { AppDispatch } from 'soapbox/store'; function useUserStream() { + const { isLoggedIn } = useLoggedIn(); const statContext = useStatContext(); - return useTimelineStream('home', 'user', refresh, null, { statContext }); + + return useTimelineStream( + 'home', + 'user', + refresh, + null, + { statContext, enabled: isLoggedIn }, + ); } /** Refresh home timeline and notifications. */ From 26dfcb728bc40de09b23aa5ace8b54a9fe377098 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 15:45:02 -0500 Subject: [PATCH 15/23] yarn i18n --- app/soapbox/locales/en.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index bfd15580a..b2d0469e9 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -868,9 +868,6 @@ "groups.search.placeholder": "Search My Groups", "groups.suggested.label": "Suggested Groups", "groups.tags.title": "Browse Topics", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", "hashtag.follow": "Follow hashtag", "header.home.label": "Home", "header.login.email.placeholder": "E-mail address", From 1addfb96a9a0519cc9a2fa05822efdbd3a51945f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 16:38:21 -0500 Subject: [PATCH 16/23] Fix streaming follow update Fixes https://gitlab.com/soapbox-pub/soapbox/-/issues/1469 --- app/soapbox/actions/streaming.ts | 60 ++++++++++++++++----- app/soapbox/entity-store/hooks/useEntity.ts | 3 +- app/soapbox/entity-store/selectors.ts | 9 ++++ app/soapbox/reducers/relationships.ts | 39 -------------- 4 files changed, 59 insertions(+), 52 deletions(-) diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index f050c7b15..6a4219af6 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -1,4 +1,7 @@ import { getLocale, getSettings } from 'soapbox/actions/settings'; +import { importEntities } from 'soapbox/entity-store/actions'; +import { Entities } from 'soapbox/entity-store/entities'; +import { selectEntity } from 'soapbox/entity-store/selectors'; import messages from 'soapbox/locales/messages'; import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats'; import { queryClient } from 'soapbox/queries/client'; @@ -26,21 +29,11 @@ import { } from './timelines'; import type { IStatContext } from 'soapbox/contexts/stat-context'; +import type { Relationship } from 'soapbox/schemas'; import type { AppDispatch, RootState } from 'soapbox/store'; import type { APIEntity, Chat } from 'soapbox/types/entities'; const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE'; -const STREAMING_FOLLOW_RELATIONSHIPS_UPDATE = 'STREAMING_FOLLOW_RELATIONSHIPS_UPDATE'; - -const updateFollowRelationships = (relationships: APIEntity) => - (dispatch: AppDispatch, getState: () => RootState) => { - const me = getState().me; - return dispatch({ - type: STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, - me, - ...relationships, - }); - }; const removeChatMessage = (payload: string) => { const data = JSON.parse(payload); @@ -190,9 +183,52 @@ const connectTimelineStream = ( }; }); +function followStateToRelationship(followState: string) { + switch (followState) { + case 'follow_pending': + return { following: false, requested: true }; + case 'follow_accept': + return { following: true, requested: false }; + case 'follow_reject': + return { following: false, requested: false }; + default: + return {}; + } +} + +interface FollowUpdate { + state: 'follow_pending' | 'follow_accept' | 'follow_reject' + follower: { + id: string + follower_count: number + following_count: number + } + following: { + id: string + follower_count: number + following_count: number + } +} + +function updateFollowRelationships(update: FollowUpdate) { + return (dispatch: AppDispatch, getState: () => RootState) => { + const me = getState().me; + const relationship = selectEntity(getState(), Entities.RELATIONSHIPS, update.following.id); + + if (update.follower.id === me && relationship) { + const updated = { + ...relationship, + ...followStateToRelationship(update.state), + }; + + // Add a small delay to deal with API race conditions. + setTimeout(() => dispatch(importEntities([updated], Entities.RELATIONSHIPS)), 300); + } + }; +} + export { STREAMING_CHAT_UPDATE, - STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, connectTimelineStream, type TimelineStreamOpts, }; diff --git a/app/soapbox/entity-store/hooks/useEntity.ts b/app/soapbox/entity-store/hooks/useEntity.ts index 14c84382c..af4aa06bc 100644 --- a/app/soapbox/entity-store/hooks/useEntity.ts +++ b/app/soapbox/entity-store/hooks/useEntity.ts @@ -5,6 +5,7 @@ import z from 'zod'; import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks'; import { importEntities } from '../actions'; +import { selectEntity } from '../selectors'; import type { Entity } from '../types'; import type { EntitySchema, EntityPath, EntityFn } from './types'; @@ -34,7 +35,7 @@ function useEntity( const defaultSchema = z.custom(); const schema = opts.schema || defaultSchema; - const entity = useAppSelector(state => state.entities[entityType]?.store[entityId] as TEntity | undefined); + const entity = useAppSelector(state => selectEntity(state, entityType, entityId)); const isEnabled = opts.enabled ?? true; const isLoading = isFetching && !entity; diff --git a/app/soapbox/entity-store/selectors.ts b/app/soapbox/entity-store/selectors.ts index d1017c5b6..3e827b360 100644 --- a/app/soapbox/entity-store/selectors.ts +++ b/app/soapbox/entity-store/selectors.ts @@ -26,6 +26,14 @@ function useListState(path: EntitiesPath, key: return useAppSelector(state => selectListState(state, path, key)); } +/** Get a single entity by its ID from the store. */ +function selectEntity( + state: RootState, + entityType: string, id: string, +): TEntity | undefined { + return state.entities[entityType]?.store[id] as TEntity | undefined; +} + /** Get list of entities from Redux. */ function selectEntities(state: RootState, path: EntitiesPath): readonly TEntity[] { const cache = selectCache(state, path); @@ -63,5 +71,6 @@ export { selectListState, useListState, selectEntities, + selectEntity, findEntity, }; \ No newline at end of file diff --git a/app/soapbox/reducers/relationships.ts b/app/soapbox/reducers/relationships.ts index 28a30e148..259885426 100644 --- a/app/soapbox/reducers/relationships.ts +++ b/app/soapbox/reducers/relationships.ts @@ -1,7 +1,6 @@ import { Map as ImmutableMap } from 'immutable'; import get from 'lodash/get'; -import { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE } from 'soapbox/actions/streaming'; import { type Relationship, relationshipSchema } from 'soapbox/schemas'; import { ACCOUNT_NOTE_SUBMIT_SUCCESS } from '../actions/account-notes'; @@ -67,44 +66,12 @@ const importPleromaAccounts = (state: State, accounts: APIEntities) => { return state; }; -const followStateToRelationship = (followState: string) => { - switch (followState) { - case 'follow_pending': - return { following: false, requested: true }; - case 'follow_accept': - return { following: true, requested: false }; - case 'follow_reject': - return { following: false, requested: false }; - default: - return {}; - } -}; - -const updateFollowRelationship = (state: State, id: string, followState: string) => { - const relationship = state.get(id) || relationshipSchema.parse({ id }); - - return state.set(id, { - ...relationship, - ...followStateToRelationship(followState), - }); -}; - export default function relationships(state: State = ImmutableMap(), action: AnyAction) { switch (action.type) { case ACCOUNT_IMPORT: return importPleromaAccount(state, action.account); case ACCOUNTS_IMPORT: return importPleromaAccounts(state, action.accounts); - // case ACCOUNT_FOLLOW_REQUEST: - // return state.setIn([action.id, 'following'], true); - // case ACCOUNT_FOLLOW_FAIL: - // return state.setIn([action.id, 'following'], false); - // case ACCOUNT_UNFOLLOW_REQUEST: - // return state.setIn([action.id, 'following'], false); - // case ACCOUNT_UNFOLLOW_FAIL: - // return state.setIn([action.id, 'following'], true); - // case ACCOUNT_FOLLOW_SUCCESS: - // case ACCOUNT_UNFOLLOW_SUCCESS: case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_UNBLOCK_SUCCESS: case ACCOUNT_MUTE_SUCCESS: @@ -122,12 +89,6 @@ export default function relationships(state: State = ImmutableMap Date: Sun, 23 Jul 2023 19:22:15 +0200 Subject: [PATCH 17/23] Improve focus handlding and focused state styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/account.tsx | 1 + app/soapbox/components/ui/tabs/tabs.css | 3 ++- app/soapbox/components/ui/toggle/toggle.tsx | 3 ++- app/soapbox/features/chats/components/chat-list-item.tsx | 9 ++++++++- .../features/edit-profile/components/header-picker.tsx | 1 + .../modals/compose-event-modal/upload-button.tsx | 2 +- app/soapbox/features/ui/components/profile-dropdown.tsx | 7 ++++++- 7 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index 861b8d475..9536ef6ed 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -189,6 +189,7 @@ const Account = ({ wrapper={(children) => {children}} > event.stopPropagation()} diff --git a/app/soapbox/components/ui/tabs/tabs.css b/app/soapbox/components/ui/tabs/tabs.css index 366211c32..e5cb6fe4a 100644 --- a/app/soapbox/components/ui/tabs/tabs.css +++ b/app/soapbox/components/ui/tabs/tabs.css @@ -13,7 +13,8 @@ [data-reach-tab] { @apply flex-1 flex justify-center items-center py-4 px-1 text-center font-medium text-sm text-gray-700 - dark:text-gray-600 hover:text-gray-800 dark:hover:text-gray-500; + dark:text-gray-600 hover:text-gray-800 dark:hover:text-gray-500 + focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:ring-gray-800 dark:ring-offset-0 dark:focus-visible:ring-primary-500; } [data-reach-tab][data-selected] { diff --git a/app/soapbox/components/ui/toggle/toggle.tsx b/app/soapbox/components/ui/toggle/toggle.tsx index 0311da8ac..abf7933c5 100644 --- a/app/soapbox/components/ui/toggle/toggle.tsx +++ b/app/soapbox/components/ui/toggle/toggle.tsx @@ -16,7 +16,7 @@ const Toggle: React.FC = ({ id, size = 'md', name, checked, onChange, r return ( ); diff --git a/app/soapbox/features/chats/components/chat-list-item.tsx b/app/soapbox/features/chats/components/chat-list-item.tsx index 9b54de9ed..82796283d 100644 --- a/app/soapbox/features/chats/components/chat-list-item.tsx +++ b/app/soapbox/features/chats/components/chat-list-item.tsx @@ -62,14 +62,21 @@ const ChatListItem: React.FC = ({ chat, onClick }) => { icon: require('@tabler/icons/logout.svg'), }], []); + const handleKeyDown: React.KeyboardEventHandler = (event) => { + if (event.key === 'Enter' || event.key === ' ') { + onClick(chat); + } + }; + return ( - // eslint-disable-next-line jsx-a11y/interactive-supports-focus
onClick(chat)} + onKeyDown={handleKeyDown} className='group flex w-full flex-col rounded-lg px-2 py-3 hover:bg-gray-100 focus:shadow-inset-ring dark:hover:bg-gray-800' data-testid='chat-list-item' + tabIndex={0} > diff --git a/app/soapbox/features/edit-profile/components/header-picker.tsx b/app/soapbox/features/edit-profile/components/header-picker.tsx index 8339f95ff..036814713 100644 --- a/app/soapbox/features/edit-profile/components/header-picker.tsx +++ b/app/soapbox/features/edit-profile/components/header-picker.tsx @@ -29,6 +29,7 @@ const HeaderPicker = React.forwardRef(({ src, onC