diff --git a/app/assets/javascripts/initializePage.js.erb b/app/assets/javascripts/initializePage.js.erb index a63dc5d2d..3269e1274 100644 --- a/app/assets/javascripts/initializePage.js.erb +++ b/app/assets/javascripts/initializePage.js.erb @@ -16,6 +16,7 @@ function callInitalizers(){ initializeAllTagEditButtons(); } initializeAllFollowButts(); + initializeUserFollowButts(); initializeReadingListIcons(); initializeSponsorshipVisibility(); if ( document.getElementById("sidebar-additional") ) { diff --git a/app/assets/javascripts/initializers/initializeAdditionalContentBoxes.js b/app/assets/javascripts/initializers/initializeAdditionalContentBoxes.js index 42e8420bf..7ad59788a 100644 --- a/app/assets/javascripts/initializers/initializeAdditionalContentBoxes.js +++ b/app/assets/javascripts/initializers/initializeAdditionalContentBoxes.js @@ -1,4 +1,4 @@ -'use strict'; +/* global initializeUserFollowButts */ function initializeAdditionalContentBoxes() { var el = document.getElementById('additional-content-area'); @@ -32,6 +32,7 @@ function initializeAdditionalContentBoxes() { el.innerHTML = html; initializeReadingListIcons(); initializeAllFollowButts(); + initializeUserFollowButts(); initializeSponsorshipVisibility(); }); } else { diff --git a/app/assets/javascripts/initializers/initializeAllFollowButts.js b/app/assets/javascripts/initializers/initializeAllFollowButts.js index ee1defafd..d3f3e309d 100644 --- a/app/assets/javascripts/initializers/initializeAllFollowButts.js +++ b/app/assets/javascripts/initializers/initializeAllFollowButts.js @@ -1,10 +1,59 @@ function initializeAllFollowButts() { var followButts = document.getElementsByClassName('follow-action-button'); for (var i = 0; i < followButts.length; i++) { - initializeFollowButt(followButts[i]); + if (!followButts[i].className.includes("follow-user")) { + initializeFollowButt(followButts[i]); + }; } } +function fetchUserFollowStatuses(idButtonHash) { + const url = new URL("/follows/bulk_show", document.location); + const searchParams = new URLSearchParams(); + Object.keys(idButtonHash).forEach((id) => { + searchParams.append("ids[]", id); + }); + searchParams.append("followable_type", "User"); + url.search = searchParams; + + fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + 'X-CSRF-Token': window.csrfToken, + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + }).then((response) => response.json()) + .then((idStatuses) => { + Object.keys(idStatuses).forEach(function(id) { + addButtClickHandle(idStatuses[id], idButtonHash[id]); + }) + }); +} + +function initializeUserFollowButtons(buttons) { + if (buttons.length > 0) { + var userIds = {}; + for (var i = 0; i < buttons.length; i++) { + var userStatus = document.body.getAttribute('data-user-status'); + if (userStatus === 'logged-out') { + addModalEventListener(buttons[i]); + } else { + var userId = JSON.parse(buttons[i].dataset.info).id + userIds[userId] = buttons[i]; + } + } + + if (Object.keys(userIds).length > 0) { fetchUserFollowStatuses(userIds); } + } +} + +function initializeUserFollowButts() { + var buttons = document.getElementsByClassName('follow-action-button follow-user'); + initializeUserFollowButtons(buttons); +} + //private function initializeFollowButt(butt) { diff --git a/app/assets/javascripts/initializers/initializeLocalStorageRender.js b/app/assets/javascripts/initializers/initializeLocalStorageRender.js index 53a77ad3a..e6a8b7480 100644 --- a/app/assets/javascripts/initializers/initializeLocalStorageRender.js +++ b/app/assets/javascripts/initializers/initializeLocalStorageRender.js @@ -1,4 +1,4 @@ -'use strict'; +/* global initializeUserFollowButts */ function initializeLocalStorageRender() { try { @@ -8,6 +8,7 @@ function initializeLocalStorageRender() { initializeBaseUserData(); initializeReadingListIcons(); initializeAllFollowButts(); + initializeUserFollowButts(); initializeSponsorshipVisibility(); } } catch (err) { diff --git a/app/assets/javascripts/utilities/buildArticleHTML.js.erb b/app/assets/javascripts/utilities/buildArticleHTML.js.erb index aff014bf6..ac8d42999 100644 --- a/app/assets/javascripts/utilities/buildArticleHTML.js.erb +++ b/app/assets/javascripts/utilities/buildArticleHTML.js.erb @@ -84,7 +84,7 @@ function buildArticleHTML(article) { SAVED\ ' } else if (article.class_name === "User") { - saveButton = '' diff --git a/app/controllers/follows_controller.rb b/app/controllers/follows_controller.rb index 3689c67d0..ac0130b73 100644 --- a/app/controllers/follows_controller.rb +++ b/app/controllers/follows_controller.rb @@ -25,6 +25,29 @@ class FollowsController < ApplicationController end end + def bulk_show + skip_authorization + render(plain: "not-logged-in") && return unless current_user + + response = params.require(:ids).map(&:to_i).each_with_object({}) do |id, hsh| + hsh[id] = if current_user.id == id + "self" + else + following_them_check = FollowChecker.new(current_user, params[:followable_type], id).cached_follow_check + following_you_check = FollowChecker.new(User.find_by(id: id), params[:followable_type], current_user.id).cached_follow_check + if following_them_check && following_you_check + "mutual" + elsif following_you_check + "follow-back" + else + following_them_check.to_s + end + end + end + + render json: response + end + def create authorize Follow diff --git a/app/services/search/user.rb b/app/services/search/user.rb index 12131b573..29b55d65c 100644 --- a/app/services/search/user.rb +++ b/app/services/search/user.rb @@ -25,7 +25,8 @@ module Search "comments_count" => source["comments_count"], "badge_achievements_count" => source["badge_achievements_count"], "last_comment_at" => source["last_comment_at"], - "roles" => source["roles"] + "roles" => source["roles"], + "user_id" => source["id"] } end diff --git a/app/views/articles/_search.html.erb b/app/views/articles/_search.html.erb index 99130ecb7..d6bb17ed1 100644 --- a/app/views/articles/_search.html.erb +++ b/app/views/articles/_search.html.erb @@ -116,6 +116,7 @@ document.getElementById("substories").innerHTML = resultDivs.join(""); initializeReadingListIcons(); initializeAllFollowButts(); + initializeUserFollowButts(); document.getElementById("substories").classList.add("search-results-loaded"); if (content.result.length == 0) { document.getElementById("substories").innerHTML = '
No results match that query
' diff --git a/config/fastly/snippets/safe_params_list.vcl b/config/fastly/snippets/safe_params_list.vcl index 41c6c3dbd..65a9d9e82 100644 --- a/config/fastly/snippets/safe_params_list.vcl +++ b/config/fastly/snippets/safe_params_list.vcl @@ -1,7 +1,7 @@ import querystring; sub vcl_recv { # return this URL with only the parameters that match this regular expression - if (req.url !~ "/internal/" && req.url !~ "/search/") { + if (req.url !~ "/internal/" && req.url !~ "/search/" && req.url !~ "/bulk_show") { set req.url = querystring.regfilter_except(req.url, "^(a_id|args|article_id|article_ids|articles|asc|callback_url|category|chat_channel_id|client_id|code|collection_id|commentable_id|commentable_type|confirmation_token|created_at|end|filter|followable_id|followable_type|fork_id|i|key|message_offset|name|oauth_token|oauth_verifier|offset|org_id|organization_id|p|page|per_page|prefill|preview|purchaser|reactable_ids|redirect_uri|reported_url|reporter_username|response_type|scope|search|signature|sort|start|state|status|tag|tag_list|top|type_of|url|username|ut|verb)$"); } } diff --git a/config/routes.rb b/config/routes.rb index e910d0963..c935e3fe3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -176,7 +176,11 @@ Rails.application.routes.draw do resources :feedback_messages, only: %i[index create] resources :organizations, only: %i[update create] resources :followed_articles, only: [:index] - resources :follows, only: %i[show create update] + resources :follows, only: %i[show create update] do + collection do + get "/bulk_show", to: "follows#bulk_show" + end + end resources :image_uploads, only: [:create] resources :blocks resources :notifications, only: [:index] diff --git a/spec/requests/follows_bulk_show_spec.rb b/spec/requests/follows_bulk_show_spec.rb new file mode 100644 index 000000000..1e68e6528 --- /dev/null +++ b/spec/requests/follows_bulk_show_spec.rb @@ -0,0 +1,42 @@ +require "rails_helper" + +RSpec.describe "Follows #bulk_show", type: :request do + let(:current_user) { create(:user) } + let(:followed_user) { create(:user) } + let(:not_followed_user) { create(:user) } + let(:follow_back_user) { create(:user) } + let(:mutal_follow_user) { create(:user) } + + context "when ids are present" do + before do + sign_in current_user + current_user.follow(followed_user) + current_user.follow(mutal_follow_user) + follow_back_user.follow(current_user) + mutal_follow_user.follow(current_user) + end + + it "returns correct following values" do + ids = [followed_user.id, not_followed_user.id, current_user.id, follow_back_user.id, mutal_follow_user.id] + get bulk_show_follows_path, params: { ids: ids } + + expect(response.parsed_body[current_user.id.to_s]).to eq("self") + expect(response.parsed_body[followed_user.id.to_s]).to eq("true") + expect(response.parsed_body[not_followed_user.id.to_s]).to eq("false") + expect(response.parsed_body[follow_back_user.id.to_s]).to eq("follow-back") + expect(response.parsed_body[mutal_follow_user.id.to_s]).to eq("mutual") + end + end + + it "without ids raises a missing param error" do + sign_in current_user + expect { get bulk_show_follows_path, params: {} }.to raise_error(ActionController::ParameterMissing) + end + + it "rejects unless logged-in" do + sign_out(current_user) + get bulk_show_follows_path + + expect(response.body).to eq("not-logged-in") + end +end diff --git a/spec/system/search/user_searches_users_spec.rb b/spec/system/search/user_searches_users_spec.rb new file mode 100644 index 000000000..123e8b98f --- /dev/null +++ b/spec/system/search/user_searches_users_spec.rb @@ -0,0 +1,26 @@ +require "rails_helper" + +RSpec.describe "User searches users", type: :system do + let(:current_user) { create(:user) } + let(:followed_user) { create(:user) } + let(:not_followed_user) { create(:user) } + let(:follow_back_user) { create(:user) } + + before do + sign_in current_user + current_user.follow(followed_user) + follow_back_user.follow(current_user) + [current_user, followed_user, not_followed_user, follow_back_user].each(&:index_to_elasticsearch_inline) + Search::User.refresh_index + end + + it "shows the correct follow buttons", js: true, elasticsearch: "User" do + stub_request(:post, "http://www.google-analytics.com/collect") + visit "/search?q=&filters=class_name:User" + + expect(JSON.parse(find_button("EDIT PROFILE")["data-info"])["id"]).to eq(current_user.id) + expect(JSON.parse(find_button("FOLLOWING")["data-info"])["id"]).to eq(followed_user.id) + expect(JSON.parse(find_button("+ FOLLOW")["data-info"])["id"]).to eq(not_followed_user.id) + expect(JSON.parse(find_button("+ FOLLOW BACK")["data-info"])["id"]).to eq(follow_back_user.id) + end +end