Reject reaction if already done from another channel.

## Issue
1075, spam prevention.

## Approach
- When making a reaction, fetch reactions for all my channels for the particular comment id, and reject the reaction is any was found.
- Report the channel name in the toast so that user can at least know which channel to select in order to undo the reaction.
This commit is contained in:
infinite-persistence 2022-04-28 21:25:14 +08:00 committed by Thomas Zarebczan
parent 27bc209717
commit dcc66f211e
3 changed files with 113 additions and 6 deletions

View file

@ -1355,7 +1355,7 @@
"Unable to edit this comment, please try again later.": "Unable to edit this comment, please try again later.",
"No active channel selected.": "No active channel selected.",
"Unable to verify your channel. Please try again.": "Unable to verify your channel. Please try again.",
"Unable to verify signature for %channel%.": "Unable to verify signature for %channel%.",
"Unable to verify signature.": "Unable to verify signature.",
"Channel cannot be anonymous, please select a channel and try again.": "Channel cannot be anonymous, please select a channel and try again.",
"Change to list layout": "Change to list layout",
"Change to tile layout": "Change to tile layout",
@ -2254,5 +2254,7 @@
"This account has livestreaming disabled, please reach out to hello@odysee.com for assistance.": "This account has livestreaming disabled, please reach out to hello@odysee.com for assistance.",
"Attach images by pasting or drag-and-drop.": "Attach images by pasting or drag-and-drop.",
"There was a network error, but the publish may have been completed. Wait a few minutes, then check your Uploads or Wallet page to confirm.": "There was a network error, but the publish may have been completed. Wait a few minutes, then check your Uploads or Wallet page to confirm.",
"Already reacted to this comment from another channel.": "Already reacted to this comment from another channel.",
"Unable to react. Please try again later.": "Unable to react. Please try again later.",
"--end--": "--end--"
}

View file

@ -352,6 +352,77 @@ export function doCommentReactList(commentIds: Array<string>) {
};
}
function doFetchAllReactionsForId(commentIds: Array<string>, channelClaims: ?Array<Claim>) {
const commentIdsCsv = commentIds.join(',');
if (!channelClaims || channelClaims.length === 0) {
return Promise.reject(null);
}
return Promise.all(channelClaims.map((x) => channelSignName(x.claim_id, x.name)))
.then((channelSignatures) => {
const params = [];
channelSignatures.forEach((sigData, i) => {
if (sigData !== undefined && sigData !== null) {
params.push({
comment_ids: commentIdsCsv,
// $FlowFixMe: null 'channelClaims' already handled at the top
channel_name: channelClaims[i].name,
// $FlowFixMe: null 'channelClaims' already handled at the top
channel_id: channelClaims[i].claim_id,
signature: sigData.signature,
signing_ts: sigData.signing_ts,
});
}
});
// $FlowFixMe
return Promise.allSettled(params.map((p) => Comments.reaction_list(p))).then((response) => {
const results = [];
response.forEach((res, i) => {
if (res.status === 'fulfilled') {
results.push({
myReactions: res.value.my_reactions,
// othersReactions: res.value.others_reactions,
// commentIds,
channelId: params[i].channel_id,
channelName: params[i].channel_name,
});
}
});
return results;
});
})
.catch((error) => {
return null;
});
}
async function getReactedChannelNames(commentId: string, myChannelClaims: ?Array<Claim>) {
// 1. Fetch reactions for all channels:
const reactions = await doFetchAllReactionsForId([commentId], myChannelClaims);
if (reactions) {
const reactedChannelNames = [];
// 2. Collect all the channel names that have reacted
for (let i = 0; i < reactions.length; ++i) {
const r = reactions[i];
const myReactions = r.myReactions[commentId];
const myReactionValues = Object.values(myReactions);
if (myReactionValues.includes(1)) {
reactedChannelNames.push(r.channelName);
}
}
return reactedChannelNames;
} else {
return null;
}
}
export function doCommentReact(commentId: string, type: string) {
return async (dispatch: Dispatch, getState: GetState) => {
const state = getState();
@ -379,8 +450,10 @@ export function doCommentReact(commentId: string, type: string) {
}
const reactKey = `${commentId}:${activeChannelClaim.claim_id}`;
const myReacts = selectMyReactsForComment(state, reactKey) || [];
const myReacts = (selectMyReactsForComment(state, reactKey) || []).slice();
const othersReacts = selectOthersReactsForComment(state, reactKey) || {};
let checkIfAlreadyReacted = false;
let rejectReaction = false;
const signatureData = await channelSignName(activeChannelClaim.claim_id, activeChannelClaim.name);
if (!signatureData) {
@ -404,11 +477,18 @@ export function doCommentReact(commentId: string, type: string) {
if (Object.keys(exclusiveTypes).includes(type)) {
params['clear_types'] = exclusiveTypes[type];
if (myReacts.indexOf(exclusiveTypes[type]) !== -1) {
// Mutually-exclusive toggle:
myReacts.splice(myReacts.indexOf(exclusiveTypes[type]), 1);
} else {
// It's not a mutually-exclusive toggle, so check if we've already
// reacted from another channel. But the verification could take some
// time if we have lots of channels, so update the GUI first.
checkIfAlreadyReacted = true;
}
}
}
// --- Update the GUI for immediate feedback ---
dispatch({
type: ACTIONS.COMMENT_REACT_STARTED,
data: commentId + type,
@ -428,7 +508,31 @@ export function doCommentReact(commentId: string, type: string) {
},
});
Comments.reaction_react(params)
// --- Check if already commented from another channel ---
if (checkIfAlreadyReacted) {
const reactedChannelNames = await getReactedChannelNames(commentId, selectMyChannelClaims(state));
if (!reactedChannelNames) {
// Couldn't determine. Probably best to just stop the operation.
dispatch(doToast({ message: __('Unable to react. Please try again later.'), isError: true }));
rejectReaction = true;
} else if (reactedChannelNames.length) {
dispatch(
doToast({
message: __('Already reacted to this comment from another channel.'),
subMessage: reactedChannelNames.join(' • '),
duration: 'long',
isError: true,
})
);
rejectReaction = true;
}
}
new Promise((res, rej) => (rejectReaction ? rej('') : res(true)))
.then(() => {
return Comments.reaction_react(params);
})
.then((result: ReactionReactResponse) => {
dispatch({
type: ACTIONS.COMMENT_REACT_COMPLETED,
@ -451,8 +555,8 @@ export function doCommentReact(commentId: string, type: string) {
dispatch({
type: ACTIONS.COMMENT_REACTION_LIST_COMPLETED,
data: {
myReactions: { [commentId]: myRevertedReactsObj },
othersReactions: { [commentId]: othersReacts },
myReactions: { [reactKey]: myRevertedReactsObj },
othersReactions: { [reactKey]: othersReacts },
},
});
});

View file

@ -4,7 +4,8 @@ import { doToast } from 'redux/actions/notifications';
export function doFailedSignatureToast(dispatch: Dispatch, channelName: string) {
dispatch(
doToast({
message: __('Unable to verify signature for %channel%.', { channel: channelName }),
message: __('Unable to verify signature.'),
subMessage: channelName,
isError: true,
})
);