diff --git a/CHANGELOG.md b/CHANGELOG.md index e5cd615cd..baaf46aa9 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,10 +26,12 @@ 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. - Posts: fix audio player progress bar visibility. +- Posts: fix audio player avatar aspect ratio for non-square avatars. - Posts: added missing gap in pending status. - Compatibility: fixed quote posting compatibility with custom Pleroma forks. - Profile: fix "load more" button height on account gallery page. @@ -37,6 +41,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 diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts index 228ba79c8..c09b0ef10 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,7 +152,9 @@ const replyCompose = (status: Status) => const state = getState(); const instance = state.instance; const { explicitAddressing } = getFeatures(instance); + const preserveSpoilers = !!getSettings(state).get('preserveSpoilers'); const account = selectOwnAccount(state); + if (!account) return; const action: ComposeReplyAction = { @@ -160,6 +163,7 @@ const replyCompose = (status: Status) => status: status, account, explicitAddressing, + preserveSpoilers, }; dispatch(action); diff --git a/app/soapbox/actions/moderation.tsx b/app/soapbox/actions/moderation.tsx index 62ffbfc9b..bdaebba5c 100644 --- a/app/soapbox/actions/moderation.tsx +++ b/app/soapbox/actions/moderation.tsx @@ -116,8 +116,7 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () = const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const accountId = state.statuses.get(statusId)!.account.id; - const acct = selectAccount(state, accountId)!.acct; + const acct = state.statuses.get(statusId)!.account.acct; dispatch(openModal('CONFIRM', { icon: require('@tabler/icons/alert-triangle.svg'), @@ -137,8 +136,7 @@ const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensiti const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const accountId = state.statuses.get(statusId)!.account.id; - const acct = selectAccount(state, accountId)!.acct; + const acct = state.statuses.get(statusId)!.account.acct; dispatch(openModal('CONFIRM', { icon: require('@tabler/icons/trash.svg'), 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/actions/streaming.ts b/app/soapbox/actions/streaming.ts index 65752c223..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'; @@ -10,39 +13,27 @@ 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, } 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); @@ -73,8 +64,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 +74,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()); @@ -191,49 +183,52 @@ const connectTimelineStream = ( }; }); -const refreshHomeTimelineAndNotification = (dispatch: AppDispatch, done?: () => void) => - dispatch(expandHomeTimeline({}, () => - dispatch(expandNotifications({}, () => - dispatch(fetchAnnouncements(done)))))); +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 {}; + } +} -const connectUserStream = (opts?: StreamOpts) => - connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification, null, opts); +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 + } +} -const connectCommunityStream = ({ onlyMedia }: Record = {}) => - connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); +function updateFollowRelationships(update: FollowUpdate) { + return (dispatch: AppDispatch, getState: () => RootState) => { + const me = getState().me; + const relationship = selectEntity(getState(), Entities.RELATIONSHIPS, update.following.id); -const connectPublicStream = ({ onlyMedia }: Record = {}) => - connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); + if (update.follower.id === me && relationship) { + const updated = { + ...relationship, + ...followStateToRelationship(update.state), + }; -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); - -const connectDirectStream = () => - connectTimelineStream('direct', 'direct'); - -const connectListStream = (id: string) => - connectTimelineStream(`list:${id}`, `list&list=${id}`); - -const connectGroupStream = (id: string) => - connectTimelineStream(`group:${id}`, `group&group=${id}`); - -const connectNostrStream = () => - connectTimelineStream('nostr', 'nostr'); + // 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, - connectUserStream, - connectCommunityStream, - connectPublicStream, - connectRemoteStream, - connectHashtagStream, - connectDirectStream, - connectListStream, - connectGroupStream, - connectNostrStream, + type TimelineStreamOpts, }; diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index e51a7d06c..ee5733c9f 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -43,3 +43,14 @@ 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 { 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'; +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/useDirectStream.ts b/app/soapbox/api/hooks/streaming/useDirectStream.ts new file mode 100644 index 000000000..9d3b47853 --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useDirectStream.ts @@ -0,0 +1,17 @@ +import { useLoggedIn } from 'soapbox/hooks'; + +import { useTimelineStream } from './useTimelineStream'; + +function useDirectStream() { + 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/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/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/api/hooks/streaming/useListStream.ts b/app/soapbox/api/hooks/streaming/useListStream.ts new file mode 100644 index 000000000..661bdce4f --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useListStream.ts @@ -0,0 +1,17 @@ +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 }, + ); +} + +export { useListStream }; \ 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..6748f95ea --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useNostrStream.ts @@ -0,0 +1,20 @@ +import { useFeatures, useLoggedIn } from 'soapbox/hooks'; + +import { useTimelineStream } from './useTimelineStream'; + +function useNostrStream() { + const features = useFeatures(); + 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/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/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/api/hooks/streaming/useTimelineStream.ts b/app/soapbox/api/hooks/streaming/useTimelineStream.ts new file mode 100644 index 000000000..28998e090 --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useTimelineStream.ts @@ -0,0 +1,42 @@ +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 [timelineId, path] = args; + 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 && streamingUrl && !stream.current) { + stream.current = dispatch(connectTimelineStream(...args)); + } + }; + + const disconnect = () => { + if (stream.current) { + stream.current(); + stream.current = null; + } + }; + + useEffect(() => { + connect(); + return disconnect; + }, [accessToken, streamingUrl, timelineId, path, 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..cededf2aa --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useUserStream.ts @@ -0,0 +1,31 @@ +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, enabled: isLoggedIn }, + ); +} + +/** 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/components/account.tsx b/app/soapbox/components/account.tsx index c81786b5d..72247a68e 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/hashtag.tsx b/app/soapbox/components/hashtag.tsx index 8dc74230e..41b2c07a6 100644 --- a/app/soapbox/components/hashtag.tsx +++ b/app/soapbox/components/hashtag.tsx @@ -23,7 +23,7 @@ const Hashtag: React.FC = ({ hashtag }) => { #{hashtag.name} - {hashtag.history && ( + {Boolean(count) && ( = ({ id, size = 'md', name, checked, onChange, r return ( ); 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/features/audio/index.tsx b/app/soapbox/features/audio/index.tsx index 3b91aee88..a4a5ee19c 100644 --- a/app/soapbox/features/audio/index.tsx +++ b/app/soapbox/features/audio/index.tsx @@ -465,10 +465,9 @@ const Audio: React.FC = (props) => { = (props) => { )} 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/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 ( diff --git a/app/soapbox/features/compose/components/search.tsx b/app/soapbox/features/compose/components/search.tsx index 87c0fa358..9bdc2c6b7 100644 --- a/app/soapbox/features/compose/components/search.tsx +++ b/app/soapbox/features/compose/components/search.tsx @@ -16,6 +16,7 @@ import AutosuggestAccountInput from 'soapbox/components/autosuggest-account-inpu import { Input } from 'soapbox/components/ui'; import SvgIcon from 'soapbox/components/ui/icon/svg-icon'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { selectAccount } from 'soapbox/selectors'; import { AppDispatch, RootState } from 'soapbox/store'; const messages = defineMessages({ @@ -25,7 +26,7 @@ const messages = defineMessages({ function redirectToAccount(accountId: string, routerHistory: any) { return (_dispatch: AppDispatch, getState: () => RootState) => { - const acct = getState().getIn(['accounts', accountId, 'acct']); + const acct = selectAccount(getState(), accountId)!.acct; if (acct && routerHistory) { routerHistory.push(`/@${acct}`); 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) => { 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