Merge pull request #1421 from Retrospring/feature/turbo-relationships

Move relationship functionality to Turbo Streams
This commit is contained in:
Karina Kwiatek 2023-10-28 10:44:47 +02:00 committed by GitHub
commit 742492555f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 124 additions and 202 deletions

View File

@ -1,35 +0,0 @@
# frozen_string_literal: true
class Ajax::RelationshipController < AjaxController
before_action :authenticate_user!
def create
params.require :screen_name
UseCase::Relationship::Create.call(
source_user: current_user,
target_user: ::User.find_by!(screen_name: params[:screen_name]),
type: params[:type]
)
@response[:success] = true
@response[:message] = t(".#{params[:type]}.success")
rescue Errors::Base => e
@response[:message] = t(e.locale_tag)
ensure
return_response
end
def destroy
UseCase::Relationship::Destroy.call(
source_user: current_user,
target_user: ::User.find_by!(screen_name: params[:screen_name]),
type: params[:type]
)
@response[:success] = true
@response[:message] = t(".#{params[:type]}.success")
rescue Errors::Base => e
@response[:message] = t(e.locale_tag)
ensure
return_response
end
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
class RelationshipsController < ApplicationController
include TurboStreamable
before_action :authenticate_user!
turbo_stream_actions :create, :destroy
def create
params.require :screen_name
UseCase::Relationship::Create.call(
source_user: current_user,
target_user: ::User.find_by!(screen_name: params[:screen_name]),
type: params[:type],
)
respond_to do |format|
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace("#{params[:type]}-#{params[:screen_name]}", partial: "relationships/destroy", locals: { type: params[:type], screen_name: params[:screen_name] }),
render_toast(t(".#{params[:type]}.success"))
]
end
format.html { redirect_back(fallback_location: user_path(username: params[:screen_name])) }
end
end
def destroy
UseCase::Relationship::Destroy.call(
source_user: current_user,
target_user: ::User.find_by!(screen_name: params[:screen_name]),
type: params[:type],
)
respond_to do |format|
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace("#{params[:type]}-#{params[:screen_name]}", partial: "relationships/create", locals: { type: params[:type], screen_name: params[:screen_name] }),
render_toast(t(".#{params[:type]}.success"))
]
end
format.html { redirect_back(fallback_location: user_path(username: params[:screen_name])) }
end
end
end

View File

@ -1,115 +0,0 @@
import { post } from '@rails/request.js';
import { showNotification, showErrorNotification } from 'utilities/notifications';
import I18n from 'retrospring/i18n';
export function userActionHandler(event: Event): void {
event.preventDefault();
const button: HTMLButtonElement = event.target as HTMLButtonElement;
const target = button.dataset.target;
const action = button.dataset.action;
button.disabled = true;
let targetURL, relationshipType;
switch (action) {
case 'follow':
targetURL = '/ajax/create_relationship';
relationshipType = 'follow';
break;
case 'unfollow':
targetURL = '/ajax/destroy_relationship';
relationshipType = 'follow';
break;
case 'block':
targetURL = '/ajax/create_relationship';
relationshipType = 'block';
break;
case 'unblock':
targetURL = '/ajax/destroy_relationship';
relationshipType = 'block';
break;
case 'mute':
targetURL = '/ajax/create_relationship';
relationshipType = 'mute';
break;
case 'unmute':
targetURL = '/ajax/destroy_relationship';
relationshipType = 'mute';
break;
}
let success = false;
post(targetURL, {
body: {
screen_name: target,
type: relationshipType,
},
contentType: 'application/json'
})
.then(async response => {
const data = await response.json;
success = data.success;
showNotification(data.message, data.success);
})
.catch(err => {
console.log(err);
showErrorNotification(I18n.translate('frontend.error.message'));
})
.finally(() => {
button.disabled = false;
if (!success) return;
switch (action) {
case 'follow':
button.dataset.action = 'unfollow';
button.innerText = I18n.translate('voc.unfollow');
button.classList.remove('btn-primary');
button.classList.add('btn-default');
break;
case 'unfollow':
resetFollowButton(button);
break;
case 'block':
button.dataset.action = 'unblock';
button.querySelector('span').innerText = I18n.translate('voc.unblock');
if (button.classList.contains('btn')) {
button.classList.remove('btn-primary');
button.classList.add('btn-default');
}
resetFollowButton(document.querySelector<HTMLButtonElement>('button[data-action="unfollow"]'));
break;
case 'unblock':
button.dataset.action = 'block';
button.querySelector('span').innerText = I18n.translate('voc.block');
if (button.classList.contains('btn')) {
button.classList.remove('btn-default');
button.classList.add('btn-primary');
}
break;
case 'mute':
button.dataset.action = 'unmute';
button.querySelector('span').innerText = I18n.translate('voc.unmute');
if (button.classList.contains('btn')) {
button.classList.remove('btn-primary');
button.classList.add('btn-default');
}
break;
case 'unmute':
button.dataset.action = 'mute';
button.querySelector('span').innerText = I18n.translate('voc.mute');
if (button.classList.contains('btn')) {
button.classList.remove('btn-default');
button.classList.add('btn-primary');
}
break;
}
});
}
function resetFollowButton(button: HTMLButtonElement) {
button.dataset.action = 'follow';
button.innerText = I18n.translate('voc.follow');
button.classList.remove('btn-default');
button.classList.add('btn-primary');
}

View File

@ -1,12 +1,8 @@
import { userActionHandler } from './action';
import { userReportHandler } from './report'; import { userReportHandler } from './report';
import registerEvents from 'retrospring/utilities/registerEvents'; import registerEvents from 'retrospring/utilities/registerEvents';
export default (): void => { export default (): void => {
registerEvents([ registerEvents([
{ type: 'click', target: 'button[name=user-action]', handler: userActionHandler, global: true },
{ type: 'click', target: '[data-action=block], [data-action=unblock]', handler: userActionHandler, global: true },
{ type: 'click', target: '[data-action=mute], [data-action=unmute]', handler: userActionHandler, global: true },
{ type: 'click', target: 'a[data-action=report-user]', handler: userReportHandler, global: true } { type: 'click', target: 'a[data-action=report-user]', handler: userReportHandler, global: true }
]); ]);
} }

View File

@ -0,0 +1,13 @@
- if type == "follow"
= button_to relationships_path(screen_name:, type:), form: { id: "#{type}-#{screen_name}" }, class: "btn btn-primary", form_class: "d-grid" do
= t("voc.follow")
- if type == "block"
= button_to relationships_path(screen_name:, type:), form: { id: "#{type}-#{screen_name}" }, class: "dropdown-item" do
%i.fa.fa-minus-circle.fa-fw
= t("voc.block")
- if type == "mute"
= button_to relationships_path(screen_name:, type:), form: { id: "#{type}-#{screen_name}" }, class: "dropdown-item" do
%i.fa.fa-volume-off.fa-fw
= t("voc.mute")

View File

@ -0,0 +1,14 @@
- if type == "follow"
= button_to relationships_path(screen_name:, type:), method: :delete, form: { id: "#{type}-#{screen_name}" }, class: "btn btn-primary",
form_class: "d-grid" do
= t("voc.unfollow")
- if type == "block"
= button_to relationships_path(screen_name:, type:), method: :delete, form: { id: "#{type}-#{screen_name}" }, class: "dropdown-item" do
%i.fa.fa-minus-circle.fa-fw
= t("voc.unblock")
- if type == "mute"
= button_to relationships_path(screen_name:, type:), method: :delete, form: { id: "#{type}-#{screen_name}" }, class: "dropdown-item" do
%i.fa.fa-volume-off.fa-fw
= t("voc.unmute")

View File

@ -9,11 +9,9 @@
- elsif user_signed_in? - elsif user_signed_in?
.d-grid.gap-2 .d-grid.gap-2
- if own_followings&.include?(user.id) || current_user.following?(user) - if own_followings&.include?(user.id) || current_user.following?(user)
%button.btn.btn-primary{ type: :button, name: 'user-action', data: { action: :unfollow, type: type, target: user.screen_name } } = render "relationships/destroy", type: "follow", screen_name: user.screen_name
= t("voc.unfollow")
- else - else
%button.btn.btn-primary{ type: :button, name: 'user-action', data: { action: :follow, type: type, target: user.screen_name } } = render "relationships/create", type: "follow", screen_name: user.screen_name
= t("voc.follow")
.btn-group .btn-group
%button.btn.btn-light.btn-sm.dropdown-toggle{ data: { bs_toggle: :dropdown }, aria: { expanded: false } } %button.btn.btn-light.btn-sm.dropdown-toggle{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
= t(".title") = t(".title")
@ -23,21 +21,13 @@
%i.fa.fa-list.fa-fw %i.fa.fa-list.fa-fw
= t(".list") = t(".list")
- if own_blocks&.include?(user.id) || current_user.blocking?(user) - if own_blocks&.include?(user.id) || current_user.blocking?(user)
%a.dropdown-item{ href: '#', data: { action: :unblock, target: user.screen_name } } = render "relationships/destroy", type: "block", screen_name: user.screen_name
%i.fa.fa-minus-circle.fa-fw
%span.pe-none= t("voc.unblock")
- else - else
%a.dropdown-item{ href: '#', data: { action: :block, target: user.screen_name } } = render "relationships/create", type: "block", screen_name: user.screen_name
%i.fa.fa-minus-circle.fa-fw
%span.pe-none= t("voc.block")
- if own_mutes&.include?(user.id) || current_user.muting?(user) - if own_mutes&.include?(user.id) || current_user.muting?(user)
%a.dropdown-item{ href: '#', data: { action: :unmute, target: user.screen_name } } = render "relationships/destroy", type: "mute", screen_name: user.screen_name
%i.fa.fa-volume-off.fa-fw
%span.pe-none= t("voc.unmute")
- else - else
%a.dropdown-item{ href: '#', data: { action: :mute, target: user.screen_name } } = render "relationships/create", type: "mute", screen_name: user.screen_name
%i.fa.fa-volume-off.fa-fw
%span.pe-none= t("voc.mute")
%a.dropdown-item{ href: '#', data: { action: 'report-user', target: user.screen_name } } %a.dropdown-item{ href: '#', data: { action: 'report-user', target: user.screen_name } }
%i.fa.fa-exclamation-triangle.fa-fw %i.fa.fa-exclamation-triangle.fa-fw
= t("voc.report") = t("voc.report")

View File

@ -101,27 +101,6 @@ en:
notfound: "Question does not exist." notfound: "Question does not exist."
noauth: "You are not allowed to delete this question." noauth: "You are not allowed to delete this question."
success: "Successfully deleted question." success: "Successfully deleted question."
relationship:
create:
block:
success: "Successfully blocked user."
error: "You are already blocking that user."
follow:
success: "Successfully followed user."
error: "You are already following that user."
mute:
success: "Successfully muted user."
error: "You are already muting that user."
destroy:
block:
success: "Successfully unblocked user."
error: "You are not blocking that user."
follow:
success: "Successfully unfollowed user."
error: "You are not following that user."
mute:
success: "Successfully unmuted user."
error: "You are not muting that user."
report: report:
create: create:
noauth: :ajax.noauth noauth: :ajax.noauth
@ -216,6 +195,27 @@ en:
registrations: registrations:
destroy: destroy:
export_pending: "You may not delete your account while account data is currently being exported." export_pending: "You may not delete your account while account data is currently being exported."
relationships:
create:
block:
success: "Successfully blocked user."
error: "You are already blocking that user."
follow:
success: "Successfully followed user."
error: "You are already following that user."
mute:
success: "Successfully muted user."
error: "You are already muting that user."
destroy:
block:
success: "Successfully unblocked user."
error: "You are not blocking that user."
follow:
success: "Successfully unfollowed user."
error: "You are not following that user."
mute:
success: "Successfully unmuted user."
error: "You are not muting that user."
timeline: timeline:
public: public:
title: "Public Timeline" title: "Public Timeline"

View File

@ -147,6 +147,7 @@ Rails.application.routes.draw do
get "/inbox", to: "inbox#show", as: :inbox get "/inbox", to: "inbox#show", as: :inbox
resource :subscriptions, controller: :subscriptions, only: %i[create destroy] resource :subscriptions, controller: :subscriptions, only: %i[create destroy]
resource :relationships, only: %i[create destroy]
get "/user/:username", to: "user#show" get "/user/:username", to: "user#show"
get "/@:username", to: "user#show", as: :user get "/@:username", to: "user#show", as: :user

View File

@ -3,11 +3,13 @@
require "rails_helper" require "rails_helper"
describe Ajax::RelationshipController, type: :controller do describe RelationshipsController, type: :controller do
render_views
shared_examples_for "params is empty" do shared_examples_for "params is empty" do
let(:params) { {} } let(:params) { {} }
include_examples "ajax does not succeed", "is required" include_examples "turbo does not succeed", "is required"
end end
let!(:user) { FactoryBot.create(:user) } let!(:user) { FactoryBot.create(:user) }
@ -20,13 +22,13 @@ describe Ajax::RelationshipController, type: :controller do
context "screen_name does not exist" do context "screen_name does not exist" do
let(:screen_name) { "peter-witzig" } let(:screen_name) { "peter-witzig" }
include_examples "ajax does not succeed", "not found" include_examples "turbo does not succeed", "not found"
end end
context "screen_name is current user" do context "screen_name is current user" do
let(:screen_name) { user.screen_name } let(:screen_name) { user.screen_name }
include_examples "ajax does not succeed", "yourself" include_examples "turbo does not succeed", "yourself"
end end
context "screen_name is different from current_user" do context "screen_name is different from current_user" do
@ -41,9 +43,9 @@ describe Ajax::RelationshipController, type: :controller do
let(:type) { "Sauerkraut" } let(:type) { "Sauerkraut" }
let(:screen_name) { user2.screen_name } let(:screen_name) { user2.screen_name }
let(:params) { { type: type, screen_name: screen_name } } let(:params) { { type:, screen_name: } }
subject { post(:create, params: params) } subject { post(:create, params:, format: :turbo_stream) }
it_behaves_like "requires login" it_behaves_like "requires login"
@ -82,7 +84,7 @@ describe Ajax::RelationshipController, type: :controller do
let(:type) { "dick" } let(:type) { "dick" }
it_behaves_like "params is empty" it_behaves_like "params is empty"
include_examples "ajax does not succeed", "Invalid parameter" include_examples "turbo does not succeed", "Invalid parameter"
end end
end end
end end
@ -110,15 +112,15 @@ describe Ajax::RelationshipController, type: :controller do
context "screen_name does not exist" do context "screen_name does not exist" do
let(:screen_name) { "peter-witzig" } let(:screen_name) { "peter-witzig" }
include_examples "ajax does not succeed", "not found" include_examples "turbo does not succeed", "not found"
end end
end end
let(:type) { "Sauerkraut" } let(:type) { "Sauerkraut" }
let(:screen_name) { user2.screen_name } let(:screen_name) { user2.screen_name }
let(:params) { { type: type, screen_name: screen_name } } let(:params) { { type:, screen_name: } }
subject { delete(:destroy, params: params) } subject { delete(:destroy, params:, format: :turbo_stream) }
it_behaves_like "requires login" it_behaves_like "requires login"
@ -146,7 +148,7 @@ describe Ajax::RelationshipController, type: :controller do
context "type = 'dick'" do context "type = 'dick'" do
let(:type) { "dick" } let(:type) { "dick" }
include_examples "ajax does not succeed", "Invalid parameter" include_examples "turbo does not succeed", "Invalid parameter"
end end
end end
end end

View File

@ -13,3 +13,10 @@ RSpec.shared_examples_for "ajax does not succeed" do |part_of_error_message|
expect(assigns(:response)[:message]).to include(part_of_error_message) expect(assigns(:response)[:message]).to include(part_of_error_message)
end end
end end
RSpec.shared_examples_for "turbo does not succeed" do |part_of_error_message|
it "turbo does not succeed" do
subject
expect(response.body).to include(part_of_error_message)
end
end