Support bites
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
614168d079
commit
77c21723e5
12 changed files with 121 additions and 9 deletions
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 = () =>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<IHeader> = ({ 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<IHeader> = ({ 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) {
|
||||
|
|
|
@ -55,6 +55,7 @@ const icons: Partial<Record<NotificationType, string>> = {
|
|||
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<NotificationType, MessageDescriptor> = defineMessages({
|
||||
|
@ -130,6 +131,10 @@ const messages: Record<NotificationType, MessageDescriptor> = 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<INotification> = (props) => {
|
|||
withRelationship
|
||||
/>
|
||||
) : null;
|
||||
case 'bite':
|
||||
return account && typeof account === 'object' ? (
|
||||
<AccountContainer
|
||||
id={account.id}
|
||||
hidden={hidden}
|
||||
avatarSize={avatarSize}
|
||||
actionType='biting'
|
||||
withRelationship
|
||||
/>
|
||||
) : null;
|
||||
case 'move':
|
||||
return account && typeof account === 'object' && notification.target && typeof notification.target === 'object' ? (
|
||||
<AccountContainer
|
||||
|
|
|
@ -8,11 +8,13 @@ import {
|
|||
unmuteAccount,
|
||||
authorizeFollowRequest,
|
||||
rejectFollowRequest,
|
||||
biteAccount,
|
||||
} from 'soapbox/actions/accounts';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { useFollow } from 'soapbox/api/hooks';
|
||||
import { Button, HStack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useFeatures, useLoggedIn } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import type { Account } from 'soapbox/normalizers';
|
||||
|
||||
|
@ -30,13 +32,16 @@ const messages = defineMessages({
|
|||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
|
||||
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
|
||||
bite: { id: 'account.bite', defaultMessage: 'Bite @{name}' },
|
||||
userBit: { id: 'account.bite.success', defaultMessage: 'You have bit @{acct}' },
|
||||
userBiteFail: { id: 'account.bite.fail', defaultMessage: 'Failed to bite @{acct}' },
|
||||
});
|
||||
|
||||
interface IActionButton {
|
||||
/** Target account for the action. */
|
||||
account: Account;
|
||||
/** Type of action to prioritize, eg on Blocks and Mutes pages. */
|
||||
actionType?: 'muting' | 'blocking' | 'follow_request';
|
||||
actionType?: 'muting' | 'blocking' | 'follow_request' | 'biting';
|
||||
/** Displays shorter text on the "Awaiting approval" button. */
|
||||
small?: boolean;
|
||||
}
|
||||
|
@ -86,6 +91,12 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
|
|||
dispatch(rejectFollowRequest(account.id));
|
||||
};
|
||||
|
||||
const handleBite = () => {
|
||||
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 handleRemoteFollow = () => {
|
||||
dispatch(openModal('UNAUTHORIZED', {
|
||||
action: 'FOLLOW',
|
||||
|
@ -126,6 +137,21 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
|
|||
);
|
||||
};
|
||||
|
||||
/** Handles actionType='blocking' */
|
||||
const bitingAction = () => {
|
||||
const text = intl.formatMessage(messages.bite, { name: account.username });
|
||||
|
||||
return (
|
||||
<Button
|
||||
theme='secondary'
|
||||
size='sm'
|
||||
text={text}
|
||||
onClick={handleBite}
|
||||
icon={require('@tabler/icons/outline/pacman.svg')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const followRequestAction = () => {
|
||||
if (account.relationship?.followed_by) return null;
|
||||
|
||||
|
@ -201,6 +227,8 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
|
|||
return blockingAction();
|
||||
} else if (actionType === 'follow_request') {
|
||||
return followRequestAction();
|
||||
} else if (actionType === 'biting') {
|
||||
return bitingAction();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
"account.badges.bot": "Bot",
|
||||
"account.birthday": "Born {date}",
|
||||
"account.birthday_today": "Birthday is today!",
|
||||
"account.bite": "Bite @{name}",
|
||||
"account.bite.fail": "Failed to bite @{acct}",
|
||||
"account.bite.success": "You have bit @{acct}",
|
||||
"account.block": "Block @{name}",
|
||||
"account.block_domain": "Hide everything from {domain}",
|
||||
"account.blocked": "Blocked",
|
||||
|
@ -1081,6 +1084,7 @@
|
|||
"new_group_panel.title": "Create group",
|
||||
"notification.admin.report": "{name} reported {target}",
|
||||
"notification.admin.sign_up": "{name} signed up",
|
||||
"notification.bite": "{name} has bit you",
|
||||
"notification.favourite": "{name} liked your post",
|
||||
"notification.follow": "{name} followed you",
|
||||
"notification.follow_request": "{name} has requested to follow you",
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
"account.badges.bot": "Bot",
|
||||
"account.birthday": "Urodzony(-a) {date}",
|
||||
"account.birthday_today": "Ma dziś urodziny!",
|
||||
"account.bite": "Ugryź @{name}",
|
||||
"account.bite.fail": "Nie udało się ugryźć @{acct}",
|
||||
"account.bite.success": "Ugryziono @{acct}",
|
||||
"account.block": "Blokuj @{name}",
|
||||
"account.block_domain": "Blokuj wszystko z {domain}",
|
||||
"account.blocked": "Zablokowany(-a)",
|
||||
|
@ -1081,6 +1084,7 @@
|
|||
"new_group_panel.title": "Utwórz grupę",
|
||||
"notification.admin.report": "{name} zgłosił(a) {target}",
|
||||
"notification.admin.sign_up": "{name} zarejestrował(a) się",
|
||||
"notification.bite": "{name} ugryzł(a) Cię",
|
||||
"notification.favourite": "{name} dodał(a) Twój wpis do ulubionych",
|
||||
"notification.follow": "{name} zaczął(-ęła) Cię obserwować",
|
||||
"notification.follow_request": "{name} poprosił(a) Cię o możliwość obserwacji",
|
||||
|
|
|
@ -68,7 +68,7 @@ const minifyNotification = (notification: Notification) => {
|
|||
created_at: string;
|
||||
id: string;
|
||||
} & (
|
||||
| { type: 'follow' | 'follow_request' | 'admin.sign_up' }
|
||||
| { type: 'follow' | 'follow_request' | 'admin.sign_up' | 'bite' }
|
||||
| {
|
||||
type: 'mention' | 'status' | 'reblog' | 'favourite' | 'poll' | 'update' | 'event_reminder';
|
||||
status: string;
|
||||
|
|
|
@ -279,7 +279,7 @@ const makeGetOtherAccounts = () => createSelector([
|
|||
], (accounts, authUserIds, me) =>
|
||||
authUserIds.reduce((list: ImmutableList<any>, id: string) => {
|
||||
if (id === me) return list;
|
||||
const account = accounts[id];
|
||||
const account = accounts?.[id];
|
||||
return account ? list.push(account) : list;
|
||||
}, ImmutableList()),
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { getFeatures, PLEROMA, type Instance } from 'pl-api';
|
||||
import { getFeatures, PLEROMA, TOKI, type Instance } from 'pl-api';
|
||||
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
|
@ -11,6 +11,8 @@ const getInstanceScopes = (instance: Instance) => {
|
|||
const v = getFeatures(instance).version;
|
||||
|
||||
switch (v.software) {
|
||||
case TOKI:
|
||||
return 'read write follow push write:bites';
|
||||
case PLEROMA:
|
||||
return 'read write follow push admin';
|
||||
default:
|
||||
|
|
Loading…
Reference in a new issue