diff --git a/src/actions/accounts.ts b/src/actions/accounts.ts index 12acdcb03..a7c52ac42 100644 --- a/src/actions/accounts.ts +++ b/src/actions/accounts.ts @@ -110,6 +110,10 @@ const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUEST' as c const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS' as const; const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL' as const; +const ACCOUNT_BITE_REQUEST = 'ACCOUNT_BITE_REQUEST' as const; +const ACCOUNT_BITE_SUCCESS = 'ACCOUNT_BITE_SUCCESS' as const; +const ACCOUNT_BITE_FAIL = 'ACCOUNT_BITE_FAIL' as const; + const maybeRedirectLogin = (error: { response: PlfeResponse }, history?: History) => { // The client is unauthorized - redirect to login. if (history && error?.response?.status === 401) { @@ -809,6 +813,37 @@ const fetchBirthdayReminders = (month: number, day: number) => }); }; +const biteAccount = (accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const client = getClient(getState); + + dispatch(biteAccountRequest(accountId)); + + return client.accounts.biteAccount(accountId) + .then(() => { + return dispatch(biteAccountSuccess(accountId)); + }) + .catch(error => { + dispatch(biteAccountFail(accountId, error)); + throw error; + }); + }; + +const biteAccountRequest = (accountId: string) => ({ + type: ACCOUNT_BITE_REQUEST, + accountId, +}); + +const biteAccountSuccess = (accountId: string) => ({ + type: ACCOUNT_BITE_SUCCESS, +}); + +const biteAccountFail = (accountId: string, error: unknown) => ({ + type: ACCOUNT_BITE_FAIL, + accountId, + error, +}); + export { ACCOUNT_CREATE_REQUEST, ACCOUNT_CREATE_SUCCESS, @@ -879,6 +914,9 @@ export { BIRTHDAY_REMINDERS_FETCH_REQUEST, BIRTHDAY_REMINDERS_FETCH_SUCCESS, BIRTHDAY_REMINDERS_FETCH_FAIL, + ACCOUNT_BITE_REQUEST, + ACCOUNT_BITE_SUCCESS, + ACCOUNT_BITE_FAIL, createAccount, fetchAccount, fetchAccountByUsername, @@ -957,4 +995,8 @@ export { accountSearch, accountLookup, fetchBirthdayReminders, + biteAccount, + biteAccountRequest, + biteAccountSuccess, + biteAccountFail, }; diff --git a/src/actions/notifications.ts b/src/actions/notifications.ts index 199dfcb5b..1b66e984d 100644 --- a/src/actions/notifications.ts +++ b/src/actions/notifications.ts @@ -297,14 +297,14 @@ const scrollTopNotifications = (top: boolean) => }; const setFilter = (filterType: FilterType, abort?: boolean) => - (dispatch: AppDispatch) => { + (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: NOTIFICATIONS_FILTER_SET, path: ['notifications', 'quickFilter', 'active'], value: filterType, }); dispatch(expandNotifications(undefined, undefined, abort)); - dispatch(saveSettings()); + if (getSettings(getState()).getIn(['notifications', 'quickFilter', 'active']) !== filterType) dispatch(saveSettings()); }; const markReadNotifications = () => diff --git a/src/api/hooks/streaming/useTimelineStream.ts b/src/api/hooks/streaming/useTimelineStream.ts index 5788fcca6..ec36ff9d1 100644 --- a/src/api/hooks/streaming/useTimelineStream.ts +++ b/src/api/hooks/streaming/useTimelineStream.ts @@ -29,7 +29,7 @@ const useTimelineStream = (stream: string, params: { list?: string; tag?: string const streamingUrl = instance.configuration.urls.streaming; const connect = async () => { - if (!socket.current) { + if (!socket.current && streamingUrl) { socket.current = client.streaming.connect(); socket.current.subscribe(stream, params); diff --git a/src/components/account.tsx b/src/components/account.tsx index 8c461dd62..0a8f8962e 100644 --- a/src/components/account.tsx +++ b/src/components/account.tsx @@ -75,7 +75,7 @@ interface IAccount { actionIcon?: string; actionTitle?: string; /** Override other actions for specificity like mute/unmute. */ - actionType?: 'muting' | 'blocking' | 'follow_request'; + actionType?: 'muting' | 'blocking' | 'follow_request' | 'biting'; avatarSize?: number; hidden?: boolean; hideActions?: boolean; diff --git a/src/features/account/components/header.tsx b/src/features/account/components/header.tsx index 90a3f62a0..d5d0ee159 100644 --- a/src/features/account/components/header.tsx +++ b/src/features/account/components/header.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; -import { blockAccount, pinAccount, removeFromFollowers, unblockAccount, unmuteAccount, unpinAccount } from 'soapbox/actions/accounts'; +import { biteAccount, blockAccount, pinAccount, removeFromFollowers, unblockAccount, unmuteAccount, unpinAccount } from 'soapbox/actions/accounts'; import { mentionCompose, directCompose } from 'soapbox/actions/compose'; import { blockDomain, unblockDomain } from 'soapbox/actions/domain-blocks'; import { openModal } from 'soapbox/actions/modals'; @@ -57,6 +57,7 @@ const messages = defineMessages({ mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Mutes' }, endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, + bite: { id: 'account.bite', defaultMessage: 'Bite @{name}' }, removeFromFollowers: { id: 'account.remove_from_followers', defaultMessage: 'Remove this follower' }, adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' }, add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, @@ -69,6 +70,8 @@ const messages = defineMessages({ removeFromFollowersConfirm: { id: 'confirmations.remove_from_followers.confirm', defaultMessage: 'Remove' }, userEndorsed: { id: 'account.endorse.success', defaultMessage: 'You are now featuring @{acct} on your profile' }, userUnendorsed: { id: 'account.unendorse.success', defaultMessage: 'You are no longer featuring @{acct}' }, + userBit: { id: 'account.bite.success', defaultMessage: 'You have bit @{acct}' }, + userBiteFail: { id: 'account.bite.fail', defaultMessage: 'Failed to bite @{acct}' }, profileExternal: { id: 'account.profile_external', defaultMessage: 'View profile on {domain}' }, header: { id: 'account.header.alt', defaultMessage: 'Profile header' }, subscribeFeed: { id: 'account.rss_feed', defaultMessage: 'Subscribe to RSS feed' }, @@ -172,6 +175,12 @@ const Header: React.FC = ({ account }) => { } }; + const onBite = () => { + dispatch(biteAccount(account.id)) + .then(() => toast.success(intl.formatMessage(messages.userBit, { acct: account.acct }))) + .catch(() => toast.error(intl.formatMessage(messages.userBiteFail, { acct: account.acct }))); + }; + const onReport = () => { dispatch(initReport(ReportableEntities.ACCOUNT, account)); }; @@ -405,6 +414,14 @@ const Header: React.FC = ({ account }) => { }); } + if (features.bites) { + menu.push({ + text: intl.formatMessage(messages.bite, { name: account.username }), + action: onBite, + icon: require('@tabler/icons/outline/pacman.svg'), + }); + } + menu.push(null); if (features.removeFromFollowers && account.relationship?.followed_by) { diff --git a/src/features/notifications/components/notification.tsx b/src/features/notifications/components/notification.tsx index 6a033d56b..5bcd6157d 100644 --- a/src/features/notifications/components/notification.tsx +++ b/src/features/notifications/components/notification.tsx @@ -55,6 +55,7 @@ const icons: Partial> = { event_reminder: require('@tabler/icons/outline/calendar-time.svg'), participation_request: require('@tabler/icons/outline/calendar-event.svg'), participation_accepted: require('@tabler/icons/outline/calendar-event.svg'), + bite: require('@tabler/icons/outline/pacman.svg'), }; const messages: Record = defineMessages({ @@ -130,6 +131,10 @@ const messages: Record = defineMessages({ id: 'notification.moderation_warning', defaultMessage: 'You have received a moderation warning', }, + bite: { + id: 'notification.bite', + defaultMessage: '{name} has bit you', + }, }); const buildMessage = ( @@ -311,6 +316,16 @@ const Notification: React.FC = (props) => { withRelationship /> ) : null; + case 'bite': + return account && typeof account === 'object' ? ( +