Merge remote-tracking branch 'soapbox/develop' into lexical
This commit is contained in:
commit
2bfbca0777
|
@ -12,9 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Reactions: Support custom emoji reactions
|
- Reactions: Support custom emoji reactions
|
||||||
- Compatbility: Support Mastodon v2 timeline filters.
|
- Compatbility: Support Mastodon v2 timeline filters.
|
||||||
- Compatbility: Preliminary support for Ditto backend.
|
- Compatbility: Preliminary support for Ditto backend.
|
||||||
|
- Compatibility: Support Firefish.
|
||||||
- Posts: Support dislikes on Friendica.
|
- Posts: Support dislikes on Friendica.
|
||||||
- UI: added a character counter to some textareas.
|
- UI: added a character counter to some textareas.
|
||||||
- UI: added new experience for viewing Media
|
- UI: added new experience for viewing Media
|
||||||
|
- Hotkeys: Added `/` as a hotkey for search field.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Posts: truncate Nostr pubkeys in reply mentions.
|
- 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: added sticky column header.
|
||||||
- UI: add specific zones the user can drag-and-drop files.
|
- UI: add specific zones the user can drag-and-drop files.
|
||||||
- UI: disable toast notifications for API errors.
|
- UI: disable toast notifications for API errors.
|
||||||
|
- Chats: Display year for older messages creation date.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Posts: fixed emojis being cut off in reactions modal.
|
- Posts: fixed emojis being cut off in reactions modal.
|
||||||
- Posts: fix audio player progress bar visibility.
|
- 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.
|
- Posts: added missing gap in pending status.
|
||||||
- Compatibility: fixed quote posting compatibility with custom Pleroma forks.
|
- Compatibility: fixed quote posting compatibility with custom Pleroma forks.
|
||||||
- Profile: fix "load more" button height on account gallery page.
|
- 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 various overflow issues related to long usernames.
|
||||||
- UI: fixed display of Markdown code blocks in the reply indicator.
|
- UI: fixed display of Markdown code blocks in the reply indicator.
|
||||||
- Auth: fixed too many API requests when the server has an error.
|
- 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
|
## [3.2.0] - 2023-02-15
|
||||||
|
|
||||||
|
|
|
@ -144,6 +144,7 @@ interface ComposeReplyAction {
|
||||||
status: Status
|
status: Status
|
||||||
account: Account
|
account: Account
|
||||||
explicitAddressing: boolean
|
explicitAddressing: boolean
|
||||||
|
preserveSpoilers: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const replyCompose = (status: Status) =>
|
const replyCompose = (status: Status) =>
|
||||||
|
@ -151,7 +152,9 @@ const replyCompose = (status: Status) =>
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const instance = state.instance;
|
const instance = state.instance;
|
||||||
const { explicitAddressing } = getFeatures(instance);
|
const { explicitAddressing } = getFeatures(instance);
|
||||||
|
const preserveSpoilers = !!getSettings(state).get('preserveSpoilers');
|
||||||
const account = selectOwnAccount(state);
|
const account = selectOwnAccount(state);
|
||||||
|
|
||||||
if (!account) return;
|
if (!account) return;
|
||||||
|
|
||||||
const action: ComposeReplyAction = {
|
const action: ComposeReplyAction = {
|
||||||
|
@ -160,6 +163,7 @@ const replyCompose = (status: Status) =>
|
||||||
status: status,
|
status: status,
|
||||||
account,
|
account,
|
||||||
explicitAddressing,
|
explicitAddressing,
|
||||||
|
preserveSpoilers,
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch(action);
|
dispatch(action);
|
||||||
|
|
|
@ -116,8 +116,7 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
|
||||||
const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) =>
|
const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const accountId = state.statuses.get(statusId)!.account.id;
|
const acct = state.statuses.get(statusId)!.account.acct;
|
||||||
const acct = selectAccount(state, accountId)!.acct;
|
|
||||||
|
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
icon: require('@tabler/icons/alert-triangle.svg'),
|
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 = () => {}) =>
|
const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () => {}) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const accountId = state.statuses.get(statusId)!.account.id;
|
const acct = state.statuses.get(statusId)!.account.acct;
|
||||||
const acct = selectAccount(state, accountId)!.acct;
|
|
||||||
|
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
icon: require('@tabler/icons/trash.svg'),
|
icon: require('@tabler/icons/trash.svg'),
|
||||||
|
|
|
@ -44,6 +44,7 @@ const defaultSettings = ImmutableMap({
|
||||||
explanationBox: true,
|
explanationBox: true,
|
||||||
autoloadTimelines: true,
|
autoloadTimelines: true,
|
||||||
autoloadMore: true,
|
autoloadMore: true,
|
||||||
|
preserveSpoilers: false,
|
||||||
|
|
||||||
systemFont: false,
|
systemFont: false,
|
||||||
demetricator: false,
|
demetricator: false,
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import { getLocale, getSettings } from 'soapbox/actions/settings';
|
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 messages from 'soapbox/locales/messages';
|
||||||
import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats';
|
import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats';
|
||||||
import { queryClient } from 'soapbox/queries/client';
|
import { queryClient } from 'soapbox/queries/client';
|
||||||
|
@ -10,39 +13,27 @@ import { connectStream } from '../stream';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deleteAnnouncement,
|
deleteAnnouncement,
|
||||||
fetchAnnouncements,
|
|
||||||
updateAnnouncements,
|
updateAnnouncements,
|
||||||
updateReaction as updateAnnouncementsReaction,
|
updateReaction as updateAnnouncementsReaction,
|
||||||
} from './announcements';
|
} from './announcements';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
import { fetchFilters } from './filters';
|
import { fetchFilters } from './filters';
|
||||||
import { MARKER_FETCH_SUCCESS } from './markers';
|
import { MARKER_FETCH_SUCCESS } from './markers';
|
||||||
import { updateNotificationsQueue, expandNotifications } from './notifications';
|
import { updateNotificationsQueue } from './notifications';
|
||||||
import { updateStatus } from './statuses';
|
import { updateStatus } from './statuses';
|
||||||
import {
|
import {
|
||||||
// deleteFromTimelines,
|
// deleteFromTimelines,
|
||||||
expandHomeTimeline,
|
|
||||||
connectTimeline,
|
connectTimeline,
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
processTimelineUpdate,
|
processTimelineUpdate,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
|
|
||||||
import type { IStatContext } from 'soapbox/contexts/stat-context';
|
import type { IStatContext } from 'soapbox/contexts/stat-context';
|
||||||
|
import type { Relationship } from 'soapbox/schemas';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
import type { APIEntity, Chat } from 'soapbox/types/entities';
|
import type { APIEntity, Chat } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE';
|
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 removeChatMessage = (payload: string) => {
|
||||||
const data = JSON.parse(payload);
|
const data = JSON.parse(payload);
|
||||||
|
@ -73,8 +64,9 @@ const updateChatQuery = (chat: IChat) => {
|
||||||
queryClient.setQueryData<Chat>(ChatKeys.chat(chat.id), newChat as any);
|
queryClient.setQueryData<Chat>(ChatKeys.chat(chat.id), newChat as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface StreamOpts {
|
interface TimelineStreamOpts {
|
||||||
statContext?: IStatContext
|
statContext?: IStatContext
|
||||||
|
enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectTimelineStream = (
|
const connectTimelineStream = (
|
||||||
|
@ -82,7 +74,7 @@ const connectTimelineStream = (
|
||||||
path: string,
|
path: string,
|
||||||
pollingRefresh: ((dispatch: AppDispatch, done?: () => void) => void) | null = null,
|
pollingRefresh: ((dispatch: AppDispatch, done?: () => void) => void) | null = null,
|
||||||
accept: ((status: APIEntity) => boolean) | null = null,
|
accept: ((status: APIEntity) => boolean) | null = null,
|
||||||
opts?: StreamOpts,
|
opts?: TimelineStreamOpts,
|
||||||
) => connectStream(path, pollingRefresh, (dispatch: AppDispatch, getState: () => RootState) => {
|
) => connectStream(path, pollingRefresh, (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const locale = getLocale(getState());
|
const locale = getLocale(getState());
|
||||||
|
|
||||||
|
@ -191,49 +183,52 @@ const connectTimelineStream = (
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshHomeTimelineAndNotification = (dispatch: AppDispatch, done?: () => void) =>
|
function followStateToRelationship(followState: string) {
|
||||||
dispatch(expandHomeTimeline({}, () =>
|
switch (followState) {
|
||||||
dispatch(expandNotifications({}, () =>
|
case 'follow_pending':
|
||||||
dispatch(fetchAnnouncements(done))))));
|
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) =>
|
interface FollowUpdate {
|
||||||
connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification, null, opts);
|
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<string, any> = {}) =>
|
function updateFollowRelationships(update: FollowUpdate) {
|
||||||
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
|
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const me = getState().me;
|
||||||
|
const relationship = selectEntity<Relationship>(getState(), Entities.RELATIONSHIPS, update.following.id);
|
||||||
|
|
||||||
const connectPublicStream = ({ onlyMedia }: Record<string, any> = {}) =>
|
if (update.follower.id === me && relationship) {
|
||||||
connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
|
const updated = {
|
||||||
|
...relationship,
|
||||||
|
...followStateToRelationship(update.state),
|
||||||
|
};
|
||||||
|
|
||||||
const connectRemoteStream = (instance: string, { onlyMedia }: Record<string, any> = {}) =>
|
// Add a small delay to deal with API race conditions.
|
||||||
connectTimelineStream(`remote${onlyMedia ? ':media' : ''}:${instance}`, `public:remote${onlyMedia ? ':media' : ''}&instance=${instance}`);
|
setTimeout(() => dispatch(importEntities([updated], Entities.RELATIONSHIPS)), 300);
|
||||||
|
}
|
||||||
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');
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
STREAMING_CHAT_UPDATE,
|
STREAMING_CHAT_UPDATE,
|
||||||
STREAMING_FOLLOW_RELATIONSHIPS_UPDATE,
|
|
||||||
connectTimelineStream,
|
connectTimelineStream,
|
||||||
connectUserStream,
|
type TimelineStreamOpts,
|
||||||
connectCommunityStream,
|
|
||||||
connectPublicStream,
|
|
||||||
connectRemoteStream,
|
|
||||||
connectHashtagStream,
|
|
||||||
connectDirectStream,
|
|
||||||
connectListStream,
|
|
||||||
connectGroupStream,
|
|
||||||
connectNostrStream,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,3 +43,14 @@ export { useSuggestedGroups } from './groups/useSuggestedGroups';
|
||||||
export { useUnmuteGroup } from './groups/useUnmuteGroup';
|
export { useUnmuteGroup } from './groups/useUnmuteGroup';
|
||||||
export { useUpdateGroup } from './groups/useUpdateGroup';
|
export { useUpdateGroup } from './groups/useUpdateGroup';
|
||||||
export { useUpdateGroupTag } from './groups/useUpdateGroupTag';
|
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';
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { useTimelineStream } from './useTimelineStream';
|
||||||
|
|
||||||
|
function useGroupStream(groupId: string) {
|
||||||
|
return useTimelineStream(
|
||||||
|
`group:${groupId}`,
|
||||||
|
`group&group=${groupId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupStream };
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { useTimelineStream } from './useTimelineStream';
|
||||||
|
|
||||||
|
function useHashtagStream(tag: string) {
|
||||||
|
return useTimelineStream(
|
||||||
|
`hashtag:${tag}`,
|
||||||
|
`hashtag&tag=${tag}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useHashtagStream };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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<typeof connectTimelineStream>) {
|
||||||
|
// 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 };
|
|
@ -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 };
|
|
@ -189,6 +189,7 @@ const Account = ({
|
||||||
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
|
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
|
||||||
>
|
>
|
||||||
<LinkEl
|
<LinkEl
|
||||||
|
className='rounded-full'
|
||||||
to={`/@${account.acct}`}
|
to={`/@${account.acct}`}
|
||||||
title={account.acct}
|
title={account.acct}
|
||||||
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
||||||
|
|
|
@ -23,7 +23,7 @@ const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
|
||||||
<Text tag='span' size='sm' weight='semibold'>#{hashtag.name}</Text>
|
<Text tag='span' size='sm' weight='semibold'>#{hashtag.name}</Text>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{hashtag.history && (
|
{Boolean(count) && (
|
||||||
<Text theme='muted' size='sm'>
|
<Text theme='muted' size='sm'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='trends.count_by_accounts'
|
id='trends.count_by_accounts'
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
[data-reach-tab] {
|
[data-reach-tab] {
|
||||||
@apply flex-1 flex justify-center items-center
|
@apply flex-1 flex justify-center items-center
|
||||||
py-4 px-1 text-center font-medium text-sm text-gray-700
|
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] {
|
[data-reach-tab][data-selected] {
|
||||||
|
|
|
@ -16,7 +16,7 @@ const Toggle: React.FC<IToggle> = ({ id, size = 'md', name, checked, onChange, r
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx('flex-none rounded-full', {
|
className={clsx('flex-none rounded-full focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-gray-800 dark:ring-offset-0 dark:focus:ring-primary-500', {
|
||||||
'bg-gray-500': !checked && !disabled,
|
'bg-gray-500': !checked && !disabled,
|
||||||
'bg-primary-600': checked && !disabled,
|
'bg-primary-600': checked && !disabled,
|
||||||
'bg-gray-200': !checked && disabled,
|
'bg-gray-200': !checked && disabled,
|
||||||
|
@ -46,6 +46,7 @@ const Toggle: React.FC<IToggle> = ({ id, size = 'md', name, checked, onChange, r
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
required={required}
|
required={required}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import z from 'zod';
|
||||||
import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { importEntities } from '../actions';
|
import { importEntities } from '../actions';
|
||||||
|
import { selectEntity } from '../selectors';
|
||||||
|
|
||||||
import type { Entity } from '../types';
|
import type { Entity } from '../types';
|
||||||
import type { EntitySchema, EntityPath, EntityFn } from './types';
|
import type { EntitySchema, EntityPath, EntityFn } from './types';
|
||||||
|
@ -34,7 +35,7 @@ function useEntity<TEntity extends Entity>(
|
||||||
const defaultSchema = z.custom<TEntity>();
|
const defaultSchema = z.custom<TEntity>();
|
||||||
const schema = opts.schema || defaultSchema;
|
const schema = opts.schema || defaultSchema;
|
||||||
|
|
||||||
const entity = useAppSelector(state => state.entities[entityType]?.store[entityId] as TEntity | undefined);
|
const entity = useAppSelector(state => selectEntity<TEntity>(state, entityType, entityId));
|
||||||
|
|
||||||
const isEnabled = opts.enabled ?? true;
|
const isEnabled = opts.enabled ?? true;
|
||||||
const isLoading = isFetching && !entity;
|
const isLoading = isFetching && !entity;
|
||||||
|
|
|
@ -26,6 +26,14 @@ function useListState<K extends keyof EntityListState>(path: EntitiesPath, key:
|
||||||
return useAppSelector(state => selectListState(state, path, key));
|
return useAppSelector(state => selectListState(state, path, key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get a single entity by its ID from the store. */
|
||||||
|
function selectEntity<TEntity extends Entity>(
|
||||||
|
state: RootState,
|
||||||
|
entityType: string, id: string,
|
||||||
|
): TEntity | undefined {
|
||||||
|
return state.entities[entityType]?.store[id] as TEntity | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/** Get list of entities from Redux. */
|
/** Get list of entities from Redux. */
|
||||||
function selectEntities<TEntity extends Entity>(state: RootState, path: EntitiesPath): readonly TEntity[] {
|
function selectEntities<TEntity extends Entity>(state: RootState, path: EntitiesPath): readonly TEntity[] {
|
||||||
const cache = selectCache(state, path);
|
const cache = selectCache(state, path);
|
||||||
|
@ -63,5 +71,6 @@ export {
|
||||||
selectListState,
|
selectListState,
|
||||||
useListState,
|
useListState,
|
||||||
selectEntities,
|
selectEntities,
|
||||||
|
selectEntity,
|
||||||
findEntity,
|
findEntity,
|
||||||
};
|
};
|
|
@ -465,10 +465,9 @@ const Audio: React.FC<IAudio> = (props) => {
|
||||||
<canvas
|
<canvas
|
||||||
role='button'
|
role='button'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className='audio-player__canvas'
|
className='audio-player__canvas absolute left-0 top-0 w-full'
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
style={{ width: '100%', position: 'absolute', top: 0, left: 0 }}
|
|
||||||
ref={canvas}
|
ref={canvas}
|
||||||
onClick={togglePlay}
|
onClick={togglePlay}
|
||||||
onKeyDown={handleAudioKeyDown}
|
onKeyDown={handleAudioKeyDown}
|
||||||
|
@ -480,15 +479,12 @@ const Audio: React.FC<IAudio> = (props) => {
|
||||||
<img
|
<img
|
||||||
src={poster}
|
src={poster}
|
||||||
alt=''
|
alt=''
|
||||||
|
className='pointer-events-none absolute aspect-1 -translate-x-1/2 -translate-y-1/2 rounded-full object-cover'
|
||||||
width={(_getRadius() - TICK_SIZE) * 2}
|
width={(_getRadius() - TICK_SIZE) * 2}
|
||||||
height={(_getRadius() - TICK_SIZE) * 2}
|
height={(_getRadius() - TICK_SIZE) * 2}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
|
||||||
left: _getCX(),
|
left: _getCX(),
|
||||||
top: _getCY(),
|
top: _getCY(),
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
borderRadius: '50%',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -62,14 +62,21 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, onClick }) => {
|
||||||
icon: require('@tabler/icons/logout.svg'),
|
icon: require('@tabler/icons/logout.svg'),
|
||||||
}], []);
|
}], []);
|
||||||
|
|
||||||
|
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
onClick(chat);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/interactive-supports-focus
|
|
||||||
<div
|
<div
|
||||||
role='button'
|
role='button'
|
||||||
key={chat.id}
|
key={chat.id}
|
||||||
onClick={() => onClick(chat)}
|
onClick={() => 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'
|
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'
|
data-testid='chat-list-item'
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<HStack alignItems='center' justifyContent='between' space={2} className='w-full'>
|
<HStack alignItems='center' justifyContent='between' space={2} className='w-full'>
|
||||||
<HStack alignItems='center' space={2} className='overflow-hidden'>
|
<HStack alignItems='center' space={2} className='overflow-hidden'>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { connectCommunityStream } from 'soapbox/actions/streaming';
|
|
||||||
import { expandCommunityTimeline } from 'soapbox/actions/timelines';
|
import { expandCommunityTimeline } from 'soapbox/actions/timelines';
|
||||||
|
import { useCommunityStream } from 'soapbox/api/hooks';
|
||||||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||||
import { Column } from 'soapbox/components/ui';
|
import { Column } from 'soapbox/components/ui';
|
||||||
import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks';
|
import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||||
|
@ -18,7 +18,7 @@ const CommunityTimeline = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const settings = useSettings();
|
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 next = useAppSelector(state => state.timelines.get('community')?.next);
|
||||||
|
|
||||||
const timelineId = 'community';
|
const timelineId = 'community';
|
||||||
|
@ -28,16 +28,13 @@ const CommunityTimeline = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
return dispatch(expandCommunityTimeline({ onlyMedia } as any));
|
return dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useCommunityStream({ onlyMedia });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(expandCommunityTimeline({ onlyMedia } as any));
|
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||||
const disconnect = dispatch(connectCommunityStream({ onlyMedia } as any));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
disconnect();
|
|
||||||
};
|
|
||||||
}, [onlyMedia]);
|
}, [onlyMedia]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -16,6 +16,7 @@ import AutosuggestAccountInput from 'soapbox/components/autosuggest-account-inpu
|
||||||
import { Input } from 'soapbox/components/ui';
|
import { Input } from 'soapbox/components/ui';
|
||||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
import { selectAccount } from 'soapbox/selectors';
|
||||||
import { AppDispatch, RootState } from 'soapbox/store';
|
import { AppDispatch, RootState } from 'soapbox/store';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -25,7 +26,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
function redirectToAccount(accountId: string, routerHistory: any) {
|
function redirectToAccount(accountId: string, routerHistory: any) {
|
||||||
return (_dispatch: AppDispatch, getState: () => RootState) => {
|
return (_dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const acct = getState().getIn(['accounts', accountId, 'acct']);
|
const acct = selectAccount(getState(), accountId)!.acct;
|
||||||
|
|
||||||
if (acct && routerHistory) {
|
if (acct && routerHistory) {
|
||||||
routerHistory.push(`/@${acct}`);
|
routerHistory.push(`/@${acct}`);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { directComposeById } from 'soapbox/actions/compose';
|
import { directComposeById } from 'soapbox/actions/compose';
|
||||||
import { mountConversations, unmountConversations, expandConversations } from 'soapbox/actions/conversations';
|
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 AccountSearch from 'soapbox/components/account-search';
|
||||||
import { Column } from 'soapbox/components/ui';
|
import { Column } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
@ -19,15 +19,14 @@ const ConversationsTimeline = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useDirectStream();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(mountConversations());
|
dispatch(mountConversations());
|
||||||
dispatch(expandConversations());
|
dispatch(expandConversations());
|
||||||
|
|
||||||
const disconnect = dispatch(connectDirectStream());
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
dispatch(unmountConversations());
|
dispatch(unmountConversations());
|
||||||
disconnect();
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ import React, { useEffect } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { directComposeById } from 'soapbox/actions/compose';
|
import { directComposeById } from 'soapbox/actions/compose';
|
||||||
import { connectDirectStream } from 'soapbox/actions/streaming';
|
|
||||||
import { expandDirectTimeline } from 'soapbox/actions/timelines';
|
import { expandDirectTimeline } from 'soapbox/actions/timelines';
|
||||||
|
import { useDirectStream } from 'soapbox/api/hooks';
|
||||||
import AccountSearch from 'soapbox/components/account-search';
|
import AccountSearch from 'soapbox/components/account-search';
|
||||||
import { Column } from 'soapbox/components/ui';
|
import { Column } from 'soapbox/components/ui';
|
||||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||||
|
@ -20,13 +20,10 @@ const DirectTimeline = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const next = useAppSelector(state => state.timelines.get('direct')?.next);
|
const next = useAppSelector(state => state.timelines.get('direct')?.next);
|
||||||
|
|
||||||
|
useDirectStream();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(expandDirectTimeline());
|
dispatch(expandDirectTimeline());
|
||||||
const disconnect = dispatch(connectDirectStream());
|
|
||||||
|
|
||||||
return (() => {
|
|
||||||
disconnect();
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSuggestion = (accountId: string) => {
|
const handleSuggestion = (accountId: string) => {
|
||||||
|
|
|
@ -29,6 +29,7 @@ const HeaderPicker = React.forwardRef<HTMLInputElement, IMediaInput>(({ src, onC
|
||||||
<label
|
<label
|
||||||
className='dark:sm:shadow-inset relative h-24 w-full cursor-pointer overflow-hidden rounded-lg bg-primary-100 text-primary-500 dark:bg-gray-800 dark:text-accent-blue sm:h-36 sm:shadow'
|
className='dark:sm:shadow-inset relative h-24 w-full cursor-pointer overflow-hidden rounded-lg bg-primary-100 text-primary-500 dark:bg-gray-800 dark:text-accent-blue sm:h-36 sm:shadow'
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(messages.title)}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
{src && <img className='h-full w-full object-cover' src={src} alt='' />}
|
{src && <img className='h-full w-full object-cover' src={src} alt='' />}
|
||||||
<HStack
|
<HStack
|
||||||
|
|
|
@ -118,6 +118,7 @@ export const Checkbox: React.FC<ICheckbox> = (props) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
interface ISelectDropdown {
|
interface ISelectDropdown {
|
||||||
|
className?: string
|
||||||
label?: React.ReactNode
|
label?: React.ReactNode
|
||||||
hint?: React.ReactNode
|
hint?: React.ReactNode
|
||||||
items: Record<string, string>
|
items: Record<string, string>
|
||||||
|
|
|
@ -4,9 +4,8 @@ import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { groupCompose, setGroupTimelineVisible, uploadCompose } from 'soapbox/actions/compose';
|
import { groupCompose, setGroupTimelineVisible, uploadCompose } from 'soapbox/actions/compose';
|
||||||
import { connectGroupStream } from 'soapbox/actions/streaming';
|
|
||||||
import { expandGroupFeaturedTimeline, expandGroupTimeline } from 'soapbox/actions/timelines';
|
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 { Avatar, HStack, Icon, Stack, Text, Toggle } from 'soapbox/components/ui';
|
||||||
import ComposeForm from 'soapbox/features/compose/components/compose-form';
|
import ComposeForm from 'soapbox/features/compose/components/compose-form';
|
||||||
import { useAppDispatch, useAppSelector, useDraggedFiles, useOwnAccount } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useDraggedFiles, useOwnAccount } from 'soapbox/hooks';
|
||||||
|
@ -49,16 +48,12 @@ const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
|
||||||
dispatch(setGroupTimelineVisible(composeId, !groupTimelineVisible));
|
dispatch(setGroupTimelineVisible(composeId, !groupTimelineVisible));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useGroupStream(groupId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(expandGroupTimeline(groupId));
|
dispatch(expandGroupTimeline(groupId));
|
||||||
dispatch(expandGroupFeaturedTimeline(groupId));
|
dispatch(expandGroupFeaturedTimeline(groupId));
|
||||||
dispatch(groupCompose(composeId, groupId));
|
dispatch(groupCompose(composeId, groupId));
|
||||||
|
|
||||||
const disconnect = dispatch(connectGroupStream(groupId));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
disconnect();
|
|
||||||
};
|
|
||||||
}, [groupId]);
|
}, [groupId]);
|
||||||
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
|
|
|
@ -1,96 +1,31 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { connectHashtagStream } from 'soapbox/actions/streaming';
|
|
||||||
import { fetchHashtag, followHashtag, unfollowHashtag } from 'soapbox/actions/tags';
|
import { fetchHashtag, followHashtag, unfollowHashtag } from 'soapbox/actions/tags';
|
||||||
import { expandHashtagTimeline, clearTimeline } from 'soapbox/actions/timelines';
|
import { expandHashtagTimeline, clearTimeline } from 'soapbox/actions/timelines';
|
||||||
|
import { useHashtagStream } from 'soapbox/api/hooks';
|
||||||
import List, { ListItem } from 'soapbox/components/list';
|
import List, { ListItem } from 'soapbox/components/list';
|
||||||
import { Column, Toggle } from 'soapbox/components/ui';
|
import { Column, Toggle } from 'soapbox/components/ui';
|
||||||
import Timeline from 'soapbox/features/ui/components/timeline';
|
import Timeline from 'soapbox/features/ui/components/timeline';
|
||||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
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 {
|
interface IHashtagTimeline {
|
||||||
params?: {
|
params?: {
|
||||||
id?: string
|
id?: string
|
||||||
tags?: Tags
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const id = params?.id || '';
|
const id = params?.id || '';
|
||||||
const tags = params?.tags || { any: [], all: [], none: [] };
|
|
||||||
|
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const disconnects = useRef<(() => void)[]>([]);
|
|
||||||
const tag = useAppSelector((state) => state.tags.get(id));
|
const tag = useAppSelector((state) => state.tags.get(id));
|
||||||
const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
|
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) => {
|
const handleLoadMore = (maxId: string) => {
|
||||||
dispatch(expandHashtagTimeline(id, { url: next, maxId, tags }));
|
dispatch(expandHashtagTimeline(id, { url: next, maxId }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFollow = () => {
|
const handleFollow = () => {
|
||||||
|
@ -101,25 +36,20 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useHashtagStream(id);
|
||||||
subscribe();
|
|
||||||
dispatch(expandHashtagTimeline(id, { tags }));
|
|
||||||
dispatch(fetchHashtag(id));
|
|
||||||
|
|
||||||
return () => {
|
useEffect(() => {
|
||||||
unsubscribe();
|
dispatch(expandHashtagTimeline(id));
|
||||||
};
|
dispatch(fetchHashtag(id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
unsubscribe();
|
|
||||||
subscribe();
|
|
||||||
dispatch(clearTimeline(`hashtag:${id}`));
|
dispatch(clearTimeline(`hashtag:${id}`));
|
||||||
dispatch(expandHashtagTimeline(id, { tags }));
|
dispatch(expandHashtagTimeline(id));
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bodyClassName='space-y-3' label={title()} transparent>
|
<Column label={`#${id}`} transparent>
|
||||||
{features.followHashtags && (
|
{features.followHashtags && (
|
||||||
<List>
|
<List>
|
||||||
<ListItem
|
<ListItem
|
||||||
|
@ -136,7 +66,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
scrollKey='hashtag_timeline'
|
scrollKey='hashtag_timeline'
|
||||||
timelineId={`hashtag:${id}`}
|
timelineId={`hashtag:${id}`}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
emptyMessage={intl.formatMessage(messages.empty)}
|
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
||||||
divideType='space'
|
divideType='space'
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { fetchList } from 'soapbox/actions/lists';
|
import { fetchList } from 'soapbox/actions/lists';
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { connectListStream } from 'soapbox/actions/streaming';
|
|
||||||
import { expandListTimeline } from 'soapbox/actions/timelines';
|
import { expandListTimeline } from 'soapbox/actions/timelines';
|
||||||
|
import { useListStream } from 'soapbox/api/hooks';
|
||||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||||
import { Column, Button, Spinner } from 'soapbox/components/ui';
|
import { Column, Button, Spinner } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
@ -19,15 +19,11 @@ const ListTimeline: React.FC = () => {
|
||||||
const list = useAppSelector((state) => state.lists.get(id));
|
const list = useAppSelector((state) => state.lists.get(id));
|
||||||
const next = useAppSelector(state => state.timelines.get(`list:${id}`)?.next);
|
const next = useAppSelector(state => state.timelines.get(`list:${id}`)?.next);
|
||||||
|
|
||||||
|
useListStream(id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchList(id));
|
dispatch(fetchList(id));
|
||||||
dispatch(expandListTimeline(id));
|
dispatch(expandListTimeline(id));
|
||||||
|
|
||||||
const disconnect = dispatch(connectListStream(id));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
disconnect();
|
|
||||||
};
|
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const handleLoadMore = (maxId: string) => {
|
const handleLoadMore = (maxId: string) => {
|
||||||
|
|
|
@ -136,6 +136,7 @@ const Preferences = () => {
|
||||||
|
|
||||||
<ListItem label={<FormattedMessage id='preferences.fields.language_label' defaultMessage='Language' />}>
|
<ListItem label={<FormattedMessage id='preferences.fields.language_label' defaultMessage='Language' />}>
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
|
className='max-w-[200px]'
|
||||||
items={languages}
|
items={languages}
|
||||||
defaultValue={settings.get('locale') as string | undefined}
|
defaultValue={settings.get('locale') as string | undefined}
|
||||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => onSelectChange(event, ['locale'])}
|
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => onSelectChange(event, ['locale'])}
|
||||||
|
@ -144,6 +145,7 @@ const Preferences = () => {
|
||||||
|
|
||||||
<ListItem label={<FormattedMessage id='preferences.fields.media_display_label' defaultMessage='Sensitive content' />}>
|
<ListItem label={<FormattedMessage id='preferences.fields.media_display_label' defaultMessage='Sensitive content' />}>
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
|
className='max-w-[200px]'
|
||||||
items={displayMediaOptions}
|
items={displayMediaOptions}
|
||||||
defaultValue={settings.get('displayMedia') as string | undefined}
|
defaultValue={settings.get('displayMedia') as string | undefined}
|
||||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => onSelectChange(event, ['displayMedia'])}
|
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => onSelectChange(event, ['displayMedia'])}
|
||||||
|
@ -153,6 +155,7 @@ const Preferences = () => {
|
||||||
{features.privacyScopes && (
|
{features.privacyScopes && (
|
||||||
<ListItem label={<FormattedMessage id='preferences.fields.privacy_label' defaultMessage='Default post privacy' />}>
|
<ListItem label={<FormattedMessage id='preferences.fields.privacy_label' defaultMessage='Default post privacy' />}>
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
|
className='max-w-[200px]'
|
||||||
items={defaultPrivacyOptions}
|
items={defaultPrivacyOptions}
|
||||||
defaultValue={settings.get('defaultPrivacy') as string | undefined}
|
defaultValue={settings.get('defaultPrivacy') as string | undefined}
|
||||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => onSelectChange(event, ['defaultPrivacy'])}
|
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => onSelectChange(event, ['defaultPrivacy'])}
|
||||||
|
@ -163,12 +166,19 @@ const Preferences = () => {
|
||||||
{features.richText && (
|
{features.richText && (
|
||||||
<ListItem label={<FormattedMessage id='preferences.fields.content_type_label' defaultMessage='Default post format' />}>
|
<ListItem label={<FormattedMessage id='preferences.fields.content_type_label' defaultMessage='Default post format' />}>
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
|
className='max-w-[200px]'
|
||||||
items={defaultContentTypeOptions}
|
items={defaultContentTypeOptions}
|
||||||
defaultValue={settings.get('defaultContentType') as string | undefined}
|
defaultValue={settings.get('defaultContentType') as string | undefined}
|
||||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => onSelectChange(event, ['defaultContentType'])}
|
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => onSelectChange(event, ['defaultContentType'])}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{features.spoilers && (
|
||||||
|
<ListItem label={<FormattedMessage id='preferences.fields.preserve_spoilers_label' defaultMessage='Preserve content warning when replying' />}>
|
||||||
|
<SettingToggle settings={settings} settingPath={['preserveSpoilers']} onChange={onToggleChange} />
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { changeSetting } from 'soapbox/actions/settings';
|
import { changeSetting } from 'soapbox/actions/settings';
|
||||||
import { connectPublicStream } from 'soapbox/actions/streaming';
|
|
||||||
import { expandPublicTimeline } from 'soapbox/actions/timelines';
|
import { expandPublicTimeline } from 'soapbox/actions/timelines';
|
||||||
|
import { usePublicStream } from 'soapbox/api/hooks';
|
||||||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||||
import { Accordion, Column } from 'soapbox/components/ui';
|
import { Accordion, Column } from 'soapbox/components/ui';
|
||||||
import { useAppSelector, useAppDispatch, useInstance, useSettings } from 'soapbox/hooks';
|
import { useAppSelector, useAppDispatch, useInstance, useSettings } from 'soapbox/hooks';
|
||||||
|
@ -23,7 +23,7 @@ const CommunityTimeline = () => {
|
||||||
|
|
||||||
const instance = useInstance();
|
const instance = useInstance();
|
||||||
const settings = useSettings();
|
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 next = useAppSelector(state => state.timelines.get('public')?.next);
|
||||||
|
|
||||||
const timelineId = 'public';
|
const timelineId = 'public';
|
||||||
|
@ -44,16 +44,13 @@ const CommunityTimeline = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
return dispatch(expandPublicTimeline({ onlyMedia } as any));
|
return dispatch(expandPublicTimeline({ onlyMedia }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
usePublicStream({ onlyMedia });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(expandPublicTimeline({ onlyMedia } as any));
|
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||||
const disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
disconnect();
|
|
||||||
};
|
|
||||||
}, [onlyMedia]);
|
}, [onlyMedia]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { connectRemoteStream } from 'soapbox/actions/streaming';
|
|
||||||
import { expandRemoteTimeline } from 'soapbox/actions/timelines';
|
import { expandRemoteTimeline } from 'soapbox/actions/timelines';
|
||||||
|
import { useRemoteStream } from 'soapbox/api/hooks';
|
||||||
import IconButton from 'soapbox/components/icon-button';
|
import IconButton from 'soapbox/components/icon-button';
|
||||||
import { Column, HStack, Text } from 'soapbox/components/ui';
|
import { Column, HStack, Text } from 'soapbox/components/ui';
|
||||||
import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks';
|
import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||||
|
@ -26,20 +26,12 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
||||||
const instance = params?.instance as string;
|
const instance = params?.instance as string;
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
|
||||||
const stream = useRef<any>(null);
|
|
||||||
|
|
||||||
const timelineId = 'remote';
|
const timelineId = 'remote';
|
||||||
const onlyMedia = !!settings.getIn(['remote', 'other', 'onlyMedia']);
|
const onlyMedia = !!settings.getIn(['remote', 'other', 'onlyMedia']);
|
||||||
const next = useAppSelector(state => state.timelines.get('remote')?.next);
|
const next = useAppSelector(state => state.timelines.get('remote')?.next);
|
||||||
|
|
||||||
const pinned: boolean = (settings.getIn(['remote_timeline', 'pinnedHosts']) as any).includes(instance);
|
const pinned: boolean = (settings.getIn(['remote_timeline', 'pinnedHosts']) as any).includes(instance);
|
||||||
|
|
||||||
const disconnect = () => {
|
|
||||||
if (stream.current) {
|
|
||||||
stream.current();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseClick: React.MouseEventHandler = () => {
|
const handleCloseClick: React.MouseEventHandler = () => {
|
||||||
history.push('/timeline/fediverse');
|
history.push('/timeline/fediverse');
|
||||||
};
|
};
|
||||||
|
@ -48,15 +40,10 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
||||||
dispatch(expandRemoteTimeline(instance, { url: next, maxId, onlyMedia }));
|
dispatch(expandRemoteTimeline(instance, { url: next, maxId, onlyMedia }));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useRemoteStream({ instance, onlyMedia });
|
||||||
disconnect();
|
|
||||||
dispatch(expandRemoteTimeline(instance, { onlyMedia, maxId: undefined }));
|
|
||||||
stream.current = dispatch(connectRemoteStream(instance, { onlyMedia }));
|
|
||||||
|
|
||||||
return () => {
|
useEffect(() => {
|
||||||
disconnect();
|
dispatch(expandRemoteTimeline(instance, { onlyMedia, maxId: undefined }));
|
||||||
stream.current = null;
|
|
||||||
};
|
|
||||||
}, [onlyMedia]);
|
}, [onlyMedia]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -34,7 +34,7 @@ const UploadButton: React.FC<IUploadButton> = ({ disabled, onSelectFile }) => {
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text size='sm' theme='primary' weight='semibold' transform='uppercase'>
|
<Text size='sm' theme='primary' weight='semibold' transform='uppercase' tabIndex={0}>
|
||||||
<FormattedMessage id='compose_event.upload_banner' defaultMessage='Upload photo' />
|
<FormattedMessage id='compose_event.upload_banner' defaultMessage='Upload photo' />
|
||||||
</Text>
|
</Text>
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -112,7 +112,12 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button type='button' ref={refs.setReference} onClick={toggleVisible}>
|
<button
|
||||||
|
className='rounded-full focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-gray-800 dark:ring-offset-0 dark:focus:ring-primary-500'
|
||||||
|
type='button'
|
||||||
|
ref={refs.setReference}
|
||||||
|
onClick={toggleVisible}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
@ -14,16 +14,15 @@ import { openModal } from 'soapbox/actions/modals';
|
||||||
import { expandNotifications } from 'soapbox/actions/notifications';
|
import { expandNotifications } from 'soapbox/actions/notifications';
|
||||||
import { register as registerPushNotifications } from 'soapbox/actions/push-notifications';
|
import { register as registerPushNotifications } from 'soapbox/actions/push-notifications';
|
||||||
import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses';
|
import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses';
|
||||||
import { connectNostrStream, connectUserStream } from 'soapbox/actions/streaming';
|
|
||||||
import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions';
|
import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions';
|
||||||
import { expandHomeTimeline } from 'soapbox/actions/timelines';
|
import { expandHomeTimeline } from 'soapbox/actions/timelines';
|
||||||
|
import { useNostrStream, useUserStream } from 'soapbox/api/hooks';
|
||||||
import GroupLookupHoc from 'soapbox/components/hoc/group-lookup-hoc';
|
import GroupLookupHoc from 'soapbox/components/hoc/group-lookup-hoc';
|
||||||
import withHoc from 'soapbox/components/hoc/with-hoc';
|
import withHoc from 'soapbox/components/hoc/with-hoc';
|
||||||
import SidebarNavigation from 'soapbox/components/sidebar-navigation';
|
import SidebarNavigation from 'soapbox/components/sidebar-navigation';
|
||||||
import ThumbNavigation from 'soapbox/components/thumb-navigation';
|
import ThumbNavigation from 'soapbox/components/thumb-navigation';
|
||||||
import { Layout } from 'soapbox/components/ui';
|
import { Layout } from 'soapbox/components/ui';
|
||||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useDraggedFiles } from 'soapbox/hooks';
|
||||||
import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useInstance, useDraggedFiles } from 'soapbox/hooks';
|
|
||||||
import AdminPage from 'soapbox/pages/admin-page';
|
import AdminPage from 'soapbox/pages/admin-page';
|
||||||
import ChatsPage from 'soapbox/pages/chats-page';
|
import ChatsPage from 'soapbox/pages/chats-page';
|
||||||
import DefaultPage from 'soapbox/pages/default-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 SearchPage from 'soapbox/pages/search-page';
|
||||||
import StatusPage from 'soapbox/pages/status-page';
|
import StatusPage from 'soapbox/pages/status-page';
|
||||||
import { usePendingPolicy } from 'soapbox/queries/policies';
|
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 { isStandalone } from 'soapbox/utils/state';
|
||||||
|
|
||||||
import BackgroundShapes from './components/background-shapes';
|
import BackgroundShapes from './components/background-shapes';
|
||||||
|
@ -363,21 +362,13 @@ const UI: React.FC<IUI> = ({ children }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { data: pendingPolicy } = usePendingPolicy();
|
const { data: pendingPolicy } = usePendingPolicy();
|
||||||
const instance = useInstance();
|
|
||||||
const statContext = useStatContext();
|
|
||||||
|
|
||||||
const userStream = useRef<any>(null);
|
|
||||||
const nostrStream = useRef<any>(null);
|
|
||||||
const node = useRef<HTMLDivElement | null>(null);
|
const node = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const vapidKey = useAppSelector(state => getVapidKey(state));
|
const vapidKey = useAppSelector(state => getVapidKey(state));
|
||||||
|
|
||||||
const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.isOpen);
|
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 standalone = useAppSelector(isStandalone);
|
||||||
|
|
||||||
const { isDragging } = useDraggedFiles(node);
|
const { isDragging } = useDraggedFiles(node);
|
||||||
|
@ -390,28 +381,6 @@ const UI: React.FC<IUI> = ({ 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 handleDragEnter = (e: DragEvent) => e.preventDefault();
|
||||||
const handleDragLeave = (e: DragEvent) => e.preventDefault();
|
const handleDragLeave = (e: DragEvent) => e.preventDefault();
|
||||||
const handleDragOver = (e: DragEvent) => e.preventDefault();
|
const handleDragOver = (e: DragEvent) => e.preventDefault();
|
||||||
|
@ -458,10 +427,6 @@ const UI: React.FC<IUI> = ({ children }) => {
|
||||||
if (window.Notification?.permission === 'default') {
|
if (window.Notification?.permission === 'default') {
|
||||||
window.setTimeout(() => Notification.requestPermission(), 120 * 1000);
|
window.setTimeout(() => Notification.requestPermission(), 120 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
|
||||||
disconnectStreaming();
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -477,9 +442,8 @@ const UI: React.FC<IUI> = ({ children }) => {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useUserStream();
|
||||||
connectStreaming();
|
useNostrStream();
|
||||||
}, [accessToken, streamingUrl]);
|
|
||||||
|
|
||||||
// The user has logged in
|
// The user has logged in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -865,9 +865,6 @@
|
||||||
"groups.search.placeholder": "Search My Groups",
|
"groups.search.placeholder": "Search My Groups",
|
||||||
"groups.suggested.label": "Suggested Groups",
|
"groups.suggested.label": "Suggested Groups",
|
||||||
"groups.tags.title": "Browse Topics",
|
"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",
|
"hashtag.follow": "Follow hashtag",
|
||||||
"header.home.label": "Home",
|
"header.home.label": "Home",
|
||||||
"header.login.email.placeholder": "E-mail address",
|
"header.login.email.placeholder": "E-mail address",
|
||||||
|
@ -1193,6 +1190,7 @@
|
||||||
"preferences.fields.language_label": "Display Language",
|
"preferences.fields.language_label": "Display Language",
|
||||||
"preferences.fields.media_display_label": "Sensitive content",
|
"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.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.privacy_label": "Default post privacy",
|
||||||
"preferences.fields.reduce_motion_label": "Reduce motion in animations",
|
"preferences.fields.reduce_motion_label": "Reduce motion in animations",
|
||||||
"preferences.fields.system_font_label": "Use system's default font",
|
"preferences.fields.system_font_label": "Use system's default font",
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import toast from 'soapbox/toast';
|
||||||
|
|
||||||
|
import type { AnyAction } from 'redux';
|
||||||
|
import type { ThunkMiddleware } from 'redux-thunk';
|
||||||
|
|
||||||
|
/** Whether the action is considered a failure. */
|
||||||
|
const isFailType = (type: string): boolean => type.endsWith('_FAIL');
|
||||||
|
|
||||||
|
/** Whether the action is a failure to fetch from browser storage. */
|
||||||
|
const isRememberFailType = (type: string): boolean => type.endsWith('_REMEMBER_FAIL');
|
||||||
|
|
||||||
|
/** Whether the error contains an Axios response. */
|
||||||
|
const hasResponse = (error: any): boolean => Boolean(error && error.response);
|
||||||
|
|
||||||
|
/** Don't show 401's. */
|
||||||
|
const authorized = (error: any): boolean => error?.response?.status !== 401;
|
||||||
|
|
||||||
|
/** Whether the error should be shown to the user. */
|
||||||
|
const shouldShowError = ({ type, skipAlert, error }: AnyAction): boolean => {
|
||||||
|
return !skipAlert && hasResponse(error) && authorized(error) && isFailType(type) && !isRememberFailType(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Middleware to display Redux errors to the user. */
|
||||||
|
const errorsMiddleware = (): ThunkMiddleware =>
|
||||||
|
() => next => action => {
|
||||||
|
if (shouldShowError(action)) {
|
||||||
|
toast.showAlertForError(action.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default errorsMiddleware;
|
|
@ -315,6 +315,11 @@ export default function compose(state = initialState, action: ComposeAction | Ev
|
||||||
map.set('caretPosition', null);
|
map.set('caretPosition', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
map.set('content_type', defaultCompose.content_type);
|
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:
|
case COMPOSE_EVENT_REPLY:
|
||||||
return updateCompose(state, action.id, compose => compose.withMutations(map => {
|
return updateCompose(state, action.id, compose => compose.withMutations(map => {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|
||||||
import { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE } from 'soapbox/actions/streaming';
|
|
||||||
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import { ACCOUNT_NOTE_SUBMIT_SUCCESS } from '../actions/account-notes';
|
import { ACCOUNT_NOTE_SUBMIT_SUCCESS } from '../actions/account-notes';
|
||||||
|
@ -67,44 +66,12 @@ const importPleromaAccounts = (state: State, accounts: APIEntities) => {
|
||||||
return state;
|
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<string, Relationship>(), action: AnyAction) {
|
export default function relationships(state: State = ImmutableMap<string, Relationship>(), action: AnyAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ACCOUNT_IMPORT:
|
case ACCOUNT_IMPORT:
|
||||||
return importPleromaAccount(state, action.account);
|
return importPleromaAccount(state, action.account);
|
||||||
case ACCOUNTS_IMPORT:
|
case ACCOUNTS_IMPORT:
|
||||||
return importPleromaAccounts(state, action.accounts);
|
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_BLOCK_SUCCESS:
|
||||||
case ACCOUNT_UNBLOCK_SUCCESS:
|
case ACCOUNT_UNBLOCK_SUCCESS:
|
||||||
case ACCOUNT_MUTE_SUCCESS:
|
case ACCOUNT_MUTE_SUCCESS:
|
||||||
|
@ -122,12 +89,6 @@ export default function relationships(state: State = ImmutableMap<string, Relati
|
||||||
return setDomainBlocking(state, action.accounts, true);
|
return setDomainBlocking(state, action.accounts, true);
|
||||||
case DOMAIN_UNBLOCK_SUCCESS:
|
case DOMAIN_UNBLOCK_SUCCESS:
|
||||||
return setDomainBlocking(state, action.accounts, false);
|
return setDomainBlocking(state, action.accounts, false);
|
||||||
case STREAMING_FOLLOW_RELATIONSHIPS_UPDATE:
|
|
||||||
if (action.follower.id === action.me) {
|
|
||||||
return updateFollowRelationship(state, action.following.id, action.state);
|
|
||||||
} else {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import thunk, { ThunkDispatch } from 'redux-thunk';
|
import thunk, { ThunkDispatch } from 'redux-thunk';
|
||||||
|
|
||||||
|
import errorsMiddleware from './middleware/errors';
|
||||||
import soundsMiddleware from './middleware/sounds';
|
import soundsMiddleware from './middleware/sounds';
|
||||||
import appReducer from './reducers';
|
import appReducer from './reducers';
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ export const store = configureStore({
|
||||||
reducer: appReducer,
|
reducer: appReducer,
|
||||||
middleware: [
|
middleware: [
|
||||||
thunk,
|
thunk,
|
||||||
|
errorsMiddleware(),
|
||||||
soundsMiddleware(),
|
soundsMiddleware(),
|
||||||
],
|
],
|
||||||
devTools: true,
|
devTools: true,
|
||||||
|
|
|
@ -17,10 +17,10 @@ const overrides = custom('features');
|
||||||
const any = (arr: Array<any>): boolean => arr.some(Boolean);
|
const any = (arr: Array<any>): boolean => arr.some(Boolean);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calckey, a fork of Misskey.
|
* Firefish, a fork of Misskey. Formerly known as Calckey.
|
||||||
* @see {@link https://calckey.org/}
|
* @see {@link https://joinfirefish.org/}
|
||||||
*/
|
*/
|
||||||
export const CALCKEY = 'Calckey';
|
export const FIREFISH = 'Firefish';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ditto, a Nostr server with Mastodon API.
|
* Ditto, a Nostr server with Mastodon API.
|
||||||
|
@ -145,7 +145,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see GET /api/v1/accounts/lookup
|
* @see GET /api/v1/accounts/lookup
|
||||||
*/
|
*/
|
||||||
accountLookup: any([
|
accountLookup: any([
|
||||||
v.software === CALCKEY,
|
v.software === FIREFISH,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '2.4.50'),
|
v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||||
v.software === TAKAHE && gte(v.version, '0.6.1'),
|
v.software === TAKAHE && gte(v.version, '0.6.1'),
|
||||||
|
@ -231,7 +231,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see GET /api/v1/bookmarks
|
* @see GET /api/v1/bookmarks
|
||||||
*/
|
*/
|
||||||
bookmarks: any([
|
bookmarks: any([
|
||||||
v.software === CALCKEY,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
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/}
|
* @see {@link https://docs.joinmastodon.org/methods/conversations/}
|
||||||
*/
|
*/
|
||||||
conversations: any([
|
conversations: any([
|
||||||
v.software === CALCKEY,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '2.6.0'),
|
v.software === MASTODON && gte(v.compatVersion, '2.6.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
||||||
|
@ -371,7 +371,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see PATCH /api/v1/accounts/update_credentials
|
* @see PATCH /api/v1/accounts/update_credentials
|
||||||
*/
|
*/
|
||||||
editProfile: any([
|
editProfile: any([
|
||||||
v.software === CALCKEY,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
v.software === MASTODON,
|
v.software === MASTODON,
|
||||||
v.software === MITRA,
|
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. */
|
/** Whether the accounts who favourited or emoji-reacted to a status can be viewed through the API. */
|
||||||
exposableReactions: any([
|
exposableReactions: any([
|
||||||
v.software === CALCKEY,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
v.software === MASTODON,
|
v.software === MASTODON,
|
||||||
v.software === TAKAHE && gte(v.version, '0.6.1'),
|
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
|
* @see GET /api/v1/timelines/list/:list_id
|
||||||
*/
|
*/
|
||||||
lists: any([
|
lists: any([
|
||||||
v.software === CALCKEY,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '2.1.0'),
|
v.software === MASTODON && gte(v.compatVersion, '2.1.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
||||||
|
@ -750,7 +750,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see POST /api/v1/statuses
|
* @see POST /api/v1/statuses
|
||||||
*/
|
*/
|
||||||
polls: any([
|
polls: any([
|
||||||
v.software === CALCKEY,
|
v.software === FIREFISH,
|
||||||
v.software === MASTODON && gte(v.version, '2.8.0'),
|
v.software === MASTODON && gte(v.version, '2.8.0'),
|
||||||
v.software === PLEROMA,
|
v.software === PLEROMA,
|
||||||
v.software === TRUTHSOCIAL,
|
v.software === TRUTHSOCIAL,
|
||||||
|
@ -760,7 +760,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* Can set privacy scopes on statuses.
|
* Can set privacy scopes on statuses.
|
||||||
* @see POST /api/v1/statuses
|
* @see POST /api/v1/statuses
|
||||||
*/
|
*/
|
||||||
privacyScopes: v.software !== TRUTHSOCIAL,
|
privacyScopes: ![TRUTHSOCIAL, DITTO].includes(v.software!),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A directory of discoverable profiles from the instance.
|
* A directory of discoverable profiles from the instance.
|
||||||
|
@ -787,7 +787,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see GET /api/v1/timelines/public
|
* @see GET /api/v1/timelines/public
|
||||||
*/
|
*/
|
||||||
publicTimeline: any([
|
publicTimeline: any([
|
||||||
v.software === CALCKEY,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
v.software === MASTODON,
|
v.software === MASTODON,
|
||||||
v.software === PLEROMA,
|
v.software === PLEROMA,
|
||||||
|
@ -960,6 +960,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
|
||||||
v.software === TRUTHSOCIAL,
|
v.software === TRUTHSOCIAL,
|
||||||
|
v.software === DITTO,
|
||||||
]),
|
]),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue