Hide a blocked user's content [deploy] (#4678)

* Hide blocked users' content appropriately

* Use RESTful create route

* Update functionality to block the user

* Fix specs for new route

* Use const instead of var

* Add noopener and noreferrer

* Remove caching for blocked user ids

* Add new rule for lack of noopener noreferrer

* Update snapshot
This commit is contained in:
Andy Zhao 2019-11-04 14:47:26 -05:00 committed by Ben Halpern
parent 027dbb027b
commit 85d8f52789
18 changed files with 122 additions and 63 deletions

View file

@ -199,6 +199,8 @@ function algoliaPaginate(tag){
.then(function searchDone(content) {
nextPage += 1;
insertArticles(content.hits);
const checkBlockedContentEvent = new CustomEvent('checkBlockedContent')
window.dispatchEvent(checkBlockedContentEvent)
if (content.hits.length == 0) {
document.getElementById("loading-articles").style.display = "none"
done = true;

View file

@ -137,6 +137,8 @@ function algoliaFollowedArticles(){
if (user && user.followed_tag_names && user.followed_tag_names.length > 0) {
insertInitialArticles(user)
const checkBlockedContentEvent = new CustomEvent('checkBlockedContent')
window.dispatchEvent(checkBlockedContentEvent)
var followedUsersArray = [];
if (user.followed_user_ids){
followedUsersArray = user.followed_user_ids.map(function(id){return 'user_'+id});

View file

@ -113,7 +113,7 @@ function buildArticleHTML(article) {
timeAgoInWords = timeAgo(article.published_at_int);
}
return '<div class="single-article single-article-small-pic">\
return '<div class="single-article single-article-small-pic" data-content-user-id="'+article.user_id+'">\
'+videoHTML+'\
'+orgHeadline+'\
<div class="small-pic">\

View file

@ -34,7 +34,7 @@ function buildCommentHTML(comment) {
'+detailsStartHTML+'\
<div class="comment-hash-marker" id="'+comment.id_code+'"></div>\
<div id="comment-node-'+comment.id+'" class="single-comment-node '+depthClass+' comment-deep-'+comment.depth+'" "\
data-comment-id="'+comment.id+'" data-comment-author-id="'+comment.user.id+'" data-current-user-comment="'+comment.newly_created+'">\
data-comment-id="'+comment.id+'" data-comment-author-id="'+comment.user.id+'" data-current-user-comment="'+comment.newly_created+'" data-content-user-id="'+comment.user.id+'">\
<div class="inner-comment '+customClass+'">\
<div class="details">\
<a href="/'+comment.user.username+'">\

View file

@ -2,7 +2,9 @@
const fetchCallback = ({ url, headers = {}, addTokenToBody = false, body }) => {
return csrfToken => {
if (addTokenToBody) { body.append('authenticity_token', csrfToken) };
if (addTokenToBody) {
body.append('authenticity_token', csrfToken);
}
return window.fetch(url, {
method: 'POST',
headers: {
@ -24,37 +26,41 @@ function sendFetch(switchStatement, body) {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body
body,
});
case 'reaction-creation':
return fetchCallback({
url: '/reactions',
addTokenToBody: true,
body
body,
});
case 'image-upload':
return fetchCallback({
url: '/image_uploads',
addTokenToBody: true,
body
body,
});
case 'follow-creation':
return fetchCallback({
url: '/follows',
addTokenToBody: true,
body
body,
});
case 'chat-creation':
return fetchCallback({
url: '/chat_channels/create_chat',
addTokenToBody: true,
body
body,
});
case 'block-chat':
case 'block-user':
return fetchCallback({
url: '/chat_channels/block_chat',
addTokenToBody: true,
body
url: '/user_blocks',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
addTokenToBody: false,
body,
});
case 'comment-creation':
return fetchCallback({
@ -62,7 +68,7 @@ function sendFetch(switchStatement, body) {
headers: {
'Content-Type': 'application/json',
},
body
body,
});
case 'comment-preview':
return fetchCallback({
@ -70,7 +76,7 @@ function sendFetch(switchStatement, body) {
headers: {
'Content-Type': 'application/json',
},
body
body,
});
default:
console.log('A wrong switchStatement was used.'); // eslint-disable-line no-console

View file

@ -43,6 +43,7 @@ class AsyncInfoController < ApplicationController
followed_organization_ids: @user.cached_following_organizations_ids,
followed_podcast_ids: @user.cached_following_podcasts_ids,
reading_list_ids: ReadingList.new(@user).cached_ids_of_articles,
blocked_user_ids: @user.all_blocking.pluck(:blocked_id),
saw_onboarding: @user.saw_onboarding,
checked_code_of_conduct: @user.checked_code_of_conduct,
checked_terms_and_conditions: @user.checked_terms_and_conditions,

View file

@ -31,6 +31,7 @@ module.exports = {
},
},
],
'react/jsx-no-target-blank': [2, { enforceDynamicLinks: 'always' }],
},
globals: {
InstantClick: false,

View file

@ -40,6 +40,7 @@ exports[`<UserDetails /> for user1 should render and test snapshot 1`] = `
>
<a
href="https://twitter.com/bojacktwitter"
rel="noopener noreferrer"
target="_blank"
>
<img
@ -55,6 +56,7 @@ exports[`<UserDetails /> for user1 should render and test snapshot 1`] = `
</a>
<a
href="https://github.com/bojackgithub"
rel="noopener noreferrer"
target="_blank"
>
<img
@ -70,6 +72,7 @@ exports[`<UserDetails /> for user1 should render and test snapshot 1`] = `
</a>
<a
href="http://bojackhorseman.com"
rel="noopener noreferrer"
target="_blank"
>
<img
@ -254,6 +257,7 @@ exports[`<UserDetails /> for user2 should render and test snapshot 1`] = `
>
<a
href="https://twitter.com/mrpbtwitter"
rel="noopener noreferrer"
target="_blank"
>
<img
@ -269,6 +273,7 @@ exports[`<UserDetails /> for user2 should render and test snapshot 1`] = `
</a>
<a
href="https://github.com/mrpbgithub"
rel="noopener noreferrer"
target="_blank"
>
<img
@ -284,6 +289,7 @@ exports[`<UserDetails /> for user2 should render and test snapshot 1`] = `
</a>
<a
href="http://mrpeanutbutter.com"
rel="noopener noreferrer"
target="_blank"
>
<img

View file

@ -3,12 +3,14 @@ import twitterImage from 'images/twitter-logo.svg';
import githubImage from 'images/github-logo.svg';
import websiteImage from 'images/external-link-logo.svg';
function blockChat(activeChannelId) {
const formData = new FormData();
formData.append('chat_id', activeChannelId);
formData.append('controller', 'chat_channels');
function blockUser(blockedUserId) {
const body = {
user_block: {
blocked_id: blockedUserId,
},
};
getCsrfToken().then(sendFetch('block-chat', formData));
getCsrfToken().then(sendFetch('block-user', JSON.stringify(body)));
}
const setUpButton = ({ modalId = '', otherModalId = '', btnName = '' }) => {
@ -16,9 +18,7 @@ const setUpButton = ({ modalId = '', otherModalId = '', btnName = '' }) => {
<button
onClick={() => {
const modal = document.getElementById(`${modalId}`);
const otherModal = document.getElementById(
`${otherModalId}`,
);
const otherModal = document.getElementById(`${otherModalId}`);
otherModal.style.display = 'none';
if (modal.style.display === 'none') {
modal.style.display = 'block';
@ -32,26 +32,26 @@ const setUpButton = ({ modalId = '', otherModalId = '', btnName = '' }) => {
{btnName}
</button>
);
}
};
const userDetailsConfig = {
twitter_username: {
hostUrl: 'https://twitter.com/',
srcImage: twitterImage,
imageAltText: 'twitter logo'
imageAltText: 'twitter logo',
},
github_username: {
hostUrl: 'https://github.com/',
srcImage: githubImage,
imageAltText: 'github logo'
imageAltText: 'github logo',
},
website_url: {
className: 'external-link-img',
hostUrl: '',
srcImage: websiteImage,
imageAltText: 'external link icon'
}
}
imageAltText: 'external link icon',
},
};
export default class UserDetails extends Component {
render() {
@ -60,14 +60,11 @@ export default class UserDetails extends Component {
const channel = this.props.activeChannel || {};
const socialIcons = [];
const userMeta = ['twitter_username', 'github_username', 'website_url'];
userMeta.forEach((metaProp) => {
userMeta.forEach(metaProp => {
if (user[metaProp]) {
let { className, hostUrl, srcImage, imageAltText } = userDetailsConfig[metaProp];
socialIcons.push(
<a
href={`${hostUrl}${user[metaProp]}`}
target="_blank"
>
<a href={`${hostUrl}${user[metaProp]}`} target="_blank" rel="noopener noreferrer">
<img
className={className}
src={srcImage}
@ -89,10 +86,10 @@ export default class UserDetails extends Component {
}
let blockButton = '';
if (channel.channel_type === 'direct' && window.currentUser.id != user.id) {
blockButton = setUpButton({
modalId: 'userdetails__blockmsg',
otherModalId: 'userdetails__reportabuse',
btnName: 'Block User'
blockButton = setUpButton({
modalId: 'userdetails__blockmsg',
otherModalId: 'userdetails__reportabuse',
btnName: 'Block User',
});
}
return (
@ -124,13 +121,11 @@ export default class UserDetails extends Component {
</div>
<div className="userdetails__blockreport">
{blockButton}
{
setUpButton({
modalId: 'userdetails__reportabuse',
otherModalId: 'userdetails__blockmsg',
btnName: 'Report Abuse'
})
}
{setUpButton({
modalId: 'userdetails__reportabuse',
otherModalId: 'userdetails__blockmsg',
btnName: 'Report Abuse',
})}
</div>
<div id="userdetails__reportabuse" style="display:none">
<div className="userdetails__reportabuse">
@ -154,7 +149,7 @@ export default class UserDetails extends Component {
tabIndex="0"
href="/report-abuse"
onClick={() => {
blockChat(channelId);
blockUser(channelId);
}}
>
Yes, Report
@ -204,12 +199,12 @@ export default class UserDetails extends Component {
<a
tabIndex="0"
onClick={() => {
blockChat(channelId);
blockUser(user.id);
window.location.href = `/connect`;
}}
onKeyUp={e => {
if (e.keyCode === 13) {
blockChat(channelId);
blockUser(user.id);
window.location.href = `/connect`;
}
}}

View file

@ -0,0 +1,26 @@
export default function hideBlockedContent() {
const contentUserElements = Array.from(
document.querySelectorAll('div[data-content-user-id]'),
);
const blockedUserIds = userData().blocked_user_ids;
const divsToHide = contentUserElements.filter(div => {
const { contentUserId } = div.dataset;
return blockedUserIds.includes(parseInt(contentUserId, 10));
});
divsToHide.forEach(div => {
if (div.className.includes('single-article')) {
div.style.display = 'none';
} else if (div.className.includes('single-comment-node')) {
const divInnerComment = div.querySelector('div.inner-comment');
divInnerComment.innerHTML = `
<div class="body " style="padding-bottom:32px;opacity:0.3;user-select:none;cursor:default">
[blocked content]
</div>
`;
}
});
}
window.addEventListener('checkBlockedContent', hideBlockedContent);

View file

@ -0,0 +1,7 @@
import hideBlockedContent from '../contentDisplayPolicy/hideBlockedContent';
window.InstantClick.on('change', () => {
hideBlockedContent();
});
hideBlockedContent();

View file

@ -1,3 +1,5 @@
/* eslint-disable no-alert */
export default function initBlock() {
const blockButton = document.getElementById(
'user-profile-dropdownmenu-block-button',
@ -25,13 +27,18 @@ export default function initBlock() {
.then(response => {
if (response.result === 'unblocked') {
blockButton.innerText = 'Block';
/* eslint-disable-next-line no-use-before-define */
blockButton.addEventListener('click', block, { once: true });
} else if (response.status === 422) {
window.alert(`Something went wrong: ${e} -- Please refresh the page to try again.`)
window.alert(
`Something went wrong: ${response.error} -- Please refresh the page to try again.`,
);
}
})
.catch(e => {
window.alert(`Something went wrong: ${e}. -- Please refresh the page to try again.`);
window.alert(
`Something went wrong: ${e}. -- Please refresh the page to try again.`,
);
});
}
@ -43,7 +50,7 @@ export default function initBlock() {
- prevent them from messaging you via DEV Connect`,
);
if (confirmBlock) {
fetch(`/user_blocks/${profileUserId}`, {
fetch(`/user_blocks`, {
method: 'POST',
headers: {
Accept: 'application/json',
@ -62,11 +69,15 @@ export default function initBlock() {
blockButton.innerText = 'Unblock';
blockButton.addEventListener('click', unblock, { once: true });
} else if (response.status === 422) {
window.alert(`Something went wrong: ${e}. -- Please refresh the page to try again.`)
window.alert(
`Something went wrong: ${response.error}. -- Please refresh the page to try again.`,
);
}
})
.catch(e => {
window.alert(`Something went wrong: ${e}. -- Please refresh the page to try again.`);
window.alert(
`Something went wrong: ${e}. -- Please refresh the page to try again.`,
);
});
} else {
blockButton.addEventListener('click', block, { once: true });
@ -74,6 +85,7 @@ export default function initBlock() {
}
// userData() is a global function
/* eslint-disable-next-line no-undef */
const currentUserId = userData().id;
if (currentUserId === parseInt(profileUserId, 10)) {
@ -91,3 +103,5 @@ export default function initBlock() {
});
}
}
/* eslint-enable no-alert */

View file

@ -1,4 +1,4 @@
<div class="single-article single-article-small-pic">
<div class="single-article single-article-small-pic" data-content-user-id="<%= story.user_id %>">
<% if story.video.present? && story.video_thumbnail_url.present? %>
<a class="single-article-video-preview" style="background-image:url(<%= story.cloudinary_video_url %>)" href="<%= story.path %>">
<div class="single-article-video-duration">

View file

@ -93,7 +93,7 @@
<div id="featured-story-marker" data-featured-article="articles-<%= @featured_story.id %>"></div>
<img src="<%= cloud_cover_url(@featured_story.main_image) %>" style="display:none" alt="<%= @featured_story.title %>" />
<a href="<%= @featured_story.path %>" id="article-link-<%= @featured_story.id %>" class="index-article-link" aria-label="Main Story" data-featured-article="articles-<%= @featured_story.id %>">
<div class="single-article big-article">
<div class="single-article big-article" data-content-user-id="<%= @featured_story.user_id %>">
<div class="picture image-final" style="background-color:<%= @featured_story.main_image_background_hex_color %>;background-image:url(<%= cloud_cover_url(@featured_story.main_image) %>)"></div>
<div class="content-wrapper">
<h3>

View file

@ -1,5 +1,5 @@
<div class="comment-hash-marker" id="<%= comment.id_code_generated %>"></div>
<div id="comment-node-<%= comment.id %>" class="single-comment-node <%= comment_class(comment, is_view_root) %> <%= "flat-node" if comment.depth > 3 %> comment-deep-<%= comment.depth %>" data-comment-id="<%= comment.id %>" data-comment-author-id="<%= comment_user_id_unless_deleted comment %>">
<div id="comment-node-<%= comment.id %>" class="single-comment-node <%= comment_class(comment, is_view_root) %> <%= "flat-node" if comment.depth > 3 %> comment-deep-<%= comment.depth %>" data-comment-id="<%= comment.id %>" data-comment-author-id="<%= comment_user_id_unless_deleted comment %>" data-content-user-id="<%= comment_user_id_unless_deleted comment %>">
<% if comment.deleted %>
<div class="inner-comment">
<div class="body" style="padding-bottom:32px;opacity:0.3;user-select:none;cursor:default">

View file

@ -40,6 +40,7 @@
<%= javascript_include_tag "base", defer: true %>
<% if user_signed_in? %>
<%= javascript_pack_tag "onboardingRedirectCheck", defer: true %>
<%= javascript_pack_tag "contentDisplayPolicy", defer: true %>
<% end %>
<% end %>
<% if !core_pages? %>

View file

@ -179,9 +179,7 @@ Rails.application.routes.draw do
resources :display_ad_events, only: [:create]
resources :badges, only: [:index]
resource :pro_membership, path: :pro, only: %i[show create update]
resources :user_blocks, param: :blocked_id, only: %i[show destroy] do
post ":blocked_id", on: :collection, to: "user_blocks#create"
end
resources :user_blocks, param: :blocked_id, only: %i[show create destroy]
resolve("ProMembership") { [:pro_membership] } # see https://guides.rubyonrails.org/routing.html#using-resolve
get "/chat_channel_memberships/find_by_chat_channel_id" => "chat_channel_memberships#find_by_chat_channel_id"

View file

@ -32,10 +32,10 @@ RSpec.describe "UserBlock", type: :request do
end
end
describe "POST /user_blocks/:blocked_id or #create" do
describe "POST /user_blocks or #create" do
it "renders 'not-logged-in' when not logged in" do
sign_out blocker
post "/user_blocks/#{blocked.id}", params: { user_block: { blocked_id: blocked.id } }
post "/user_blocks", params: { user_block: { blocked_id: blocked.id } }
json_response = JSON.parse(response.body)
expect(response.content_type).to eq "application/json"
expect(response.status).to eq 401
@ -43,14 +43,14 @@ RSpec.describe "UserBlock", type: :request do
end
it "creates the correct user_block" do
post "/user_blocks/#{blocked.id}", params: { user_block: { blocked_id: blocked.id } }
post "/user_blocks", params: { user_block: { blocked_id: blocked.id } }
expect(UserBlock.count).to eq 1
expect(UserBlock.first.blocker_id).to eq blocker.id
expect(UserBlock.first.blocked_id).to eq blocked.id
end
it "returns a JSON response with blocked" do
post "/user_blocks/#{blocked.id}", params: { user_block: { blocked_id: blocked.id } }
post "/user_blocks", params: { user_block: { blocked_id: blocked.id } }
json_response = JSON.parse(response.body)
expect(response.content_type).to eq "application/json"
expect(json_response["result"]).to eq "blocked"
@ -60,7 +60,7 @@ RSpec.describe "UserBlock", type: :request do
chat_channel = create(:chat_channel, channel_type: "direct", slug: "#{blocker.username}/#{blocked.username}", status: "active")
create(:chat_channel_membership, chat_channel_id: chat_channel.id, user_id: blocker.id)
create(:chat_channel_membership, chat_channel_id: chat_channel.id, user_id: blocked.id)
post "/user_blocks/#{blocked.id}", params: { user_block: { blocked_id: blocked.id } }
post "/user_blocks", params: { user_block: { blocked_id: blocked.id } }
expect(chat_channel.reload.status).to eq "blocked"
end
end