Add flag link to profile dropdown view (#12862)

* Add flag link to profile dropdown view

* Add JS for flag button on profile

* Only show flag button to trusted users

* Quick fix for accidental unflagging

This will be more properly addressed in a future refactoring.

* Update reaction specs

* Update spec

* Make flagging togglable

* Make CodeClimate happy

* Revert accidentally changed file

* CodeClimate

* Update app/javascript/profileDropdown/flagButton.js

Co-authored-by: Vaidehi Joshi <vaidehi.sj@gmail.com>

* Add JSDoc

* Remove second popup

* Refactor and add system spec

* CodeClimate

* Change send to public_send

* Address PR feedback by @aitchiss

Co-authored-by: Vaidehi Joshi <vaidehi.sj@gmail.com>
This commit is contained in:
Michael Kohl 2021-04-01 16:26:43 +07:00 committed by GitHub
parent a226379c45
commit 28e38cbb7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 145 additions and 14 deletions

View file

@ -236,6 +236,7 @@ class StoriesController < ApplicationController
# 2nd with 1 badge (!) <-- and that would look off.
@badges_limit = 6
@profile = @user.profile.decorate
@is_user_flagged = Reaction.where(user_id: session_current_user_id, reactable: @user).any?
set_surrogate_key_header "articles-user-#{@user.id}"
set_user_json_ld

View file

@ -1,7 +1,13 @@
import { initBlock } from '../profileDropdown/blockButton';
import { initFlag } from '../profileDropdown/flagButton';
function initButtons() {
initBlock();
initFlag();
}
window.InstantClick.on('change', () => {
initBlock();
initButtons();
});
initBlock();
initButtons();

View file

@ -0,0 +1,67 @@
/* global userData */
/* eslint-disable no-alert */
/**
* Adds a flag button visible only to trusted users on profile pages.
* @function initFlag
* @returns {(void|undefined)} This function has no useable return value.
*/
export function initFlag() {
const flagButton = document.getElementById(
'user-profile-dropdownmenu-flag-button',
);
if (!flagButton) {
// button not always present when this is called
return;
}
const user = userData();
if (!user) {
return;
}
const { profileUserId, profileUserName } = flagButton.dataset;
let isUserFlagged = flagButton.dataset.isUserFlagged === 'true';
const trustedOrAdmin = user.trusted || user.admin;
if (!trustedOrAdmin || user.id === parseInt(profileUserId, 10)) {
flagButton.remove();
}
function flag() {
const confirmFlag = window.confirm(
isUserFlagged
? 'Are you sure you want to unflag this person? This will make all of their posts visible again.'
: 'Are you sure you want to flag this person? This will make all of their posts less visible.',
);
if (confirmFlag) {
fetch('/reactions', {
method: 'POST',
headers: {
'X-CSRF-Token': window.csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
reactable_type: 'User',
category: 'vomit',
reactable_id: profileUserId,
}),
})
.then((response) => response.json())
.then((response) => {
if (response.result === 'create') {
isUserFlagged = true;
flagButton.innerHTML = `Unflag ${profileUserName}`;
} else {
isUserFlagged = false;
flagButton.innerHTML = `Flag ${profileUserName}`;
}
})
.catch((e) => window.alert(`Something went wrong: ${e}`));
}
}
flagButton.addEventListener('click', flag);
}
/* eslint-enable no-alert */

View file

@ -33,8 +33,17 @@
</button>
<div id="user-profile-dropdownmenu" class="crayons-dropdown top-100 right-0 p-1">
<a href="javascript:void(0);" id="user-profile-dropdownmenu-block-button" data-profile-user-id="<%= @user.id %>" class="border-none crayons-link crayons-link--block">Block @<%= @user.username %></a>
<% if user_signed_in? %>
<a href="javascript:void(0);" id="user-profile-dropdownmenu-block-button" data-profile-user-id="<%= @user.id %>" class="border-none crayons-link crayons-link--block">Block @<%= @user.username %></a>
<a
href="javascript:void(0);"
id="user-profile-dropdownmenu-flag-button"
data-profile-user-id="<%= @user.id %>"
data-profile-user-name="@<%= @user.username %>"
data-is-user-flagged="<%= @is_user_flagged %>"
class="border-none crayons-link crayons-link--block">
<%= @flag_status ? "Unflag" : "Flag" %> @<%= @user.username %>
</a>
<%= javascript_packs_with_chunks_tag "profileDropdown", defer: true %>
<% end %>
<span class="report-abuse-link-wrapper" data-path="/report-abuse?url=<%= user_url(@user) %>"></span>
@ -75,9 +84,9 @@
<% end %>
<span class="profile-header__meta__item -ml-1">
<% Profile.special_social_link_attributes.each do |attribute| %>
<% if @user.send(attribute).present? %>
<a href="<%= @user.send(attribute) %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
<%= inline_svg_tag("#{attribute.gsub("_url","")}.svg", class: "crayons-icon", aria: true, title: "#{attribute.split("_").first.titleize} logo") %>
<% if @user.public_send(attribute).present? %>
<a href="<%= @user.public_send(attribute) %>" target="_blank" rel="noopener me" class="px-1 align-middle inline-block">
<%= inline_svg_tag("#{attribute.gsub('_url', '')}.svg", class: "crayons-icon", aria: true, title: "#{attribute.split('_').first.titleize} logo") %>
</a>
<% end %>
<% end %>

View file

@ -190,17 +190,20 @@ RSpec.describe "Reactions", type: :request do
context "when reacting to an article" do
before do
sign_in user
post "/reactions", params: article_params
end
it "creates reaction" do
expect(Reaction.last.reactable_id).to eq(article.id)
expect do
post "/reactions", params: article_params
end.to change(Reaction, :count).by(1)
end
it "destroys existing reaction" do
# same route to destroy, so sending POST request again
post "/reactions", params: article_params
expect(Reaction.all.size).to eq(0)
expect do
# same route to destroy, so sending POST request again
post "/reactions", params: article_params
end.to change(Reaction, :count).by(-1)
end
end
@ -270,17 +273,19 @@ RSpec.describe "Reactions", type: :request do
context "when vomiting on a user" do
before do
sign_in trusted_user
post "/reactions", params: user_params
end
it "creates reaction" do
expect(Reaction.last.reactable_id).to eq(user.id)
expect do
post "/reactions", params: user_params
end.to change(Reaction, :count).by(1)
end
it "destroys existing reaction" do
# same route to destroy, so sending POST request again
post "/reactions", params: user_params
expect(Reaction.all.size).to eq(0)
expect do
post "/reactions", params: user_params
end.to change(Reaction, :count).by(-1)
end
end

View file

@ -0,0 +1,43 @@
require "rails_helper"
RSpec.describe "Flagging users from profile pages", type: :system, js: true do
let(:user) { create :user }
let(:flag_text) { "Flag @#{user.username}" }
context "when not logged in" do
it "does not show the flag button" do
visit user_profile_path(user.username)
click_button(id: "user-profile-dropdown")
expect(page).not_to have_link(flag_text)
end
end
context "when signed in as a non-trusted user" do
it "does not show the flag button" do
sign_in create(:user)
visit user_profile_path(user.username)
click_button(id: "user-profile-dropdown")
expect(page).not_to have_link(flag_text)
end
end
context "when signed in as the user" do
it "does not show a button for flagging yourself" do
sign_in user
visit user_profile_path(user.username)
expect(page).not_to have_selector("user-profile-dropdown")
end
end
context "when signed in as a trusted user" do
it "allows toggling the flagged status" do
sign_in create(:user, :trusted)
visit user_profile_path(user.username)
click_button(id: "user-profile-dropdown")
expect(page).to have_link(flag_text)
end
end
end