] the user's timeline
def timeline
Answer
+ .for_user(self)
.then do |query|
blocked_and_muted_user_ids = blocked_user_ids_cached + muted_user_ids_cached
next query if blocked_and_muted_user_ids.empty?
@@ -21,6 +22,6 @@ module User::TimelineMethods
.where("answers.user_id in (?) OR answers.user_id = ?", following_ids, id)
.order(:created_at)
.reverse_order
- .includes(comments: %i[user smiles], question: { user: :profile }, user: [:profile], smiles: [:user])
+ .includes(question: { user: [:profile] }, user: [:profile])
end
end
diff --git a/app/uploaders/base_uploader.rb b/app/uploaders/base_uploader.rb
index fea89216..ddf5152c 100644
--- a/app/uploaders/base_uploader.rb
+++ b/app/uploaders/base_uploader.rb
@@ -8,12 +8,14 @@ class BaseUploader < CarrierWave::Uploader::Base
# Store original size
version :original
- # Process cropping on upload
+ process :remove_animation
process :cropping
- def store_dir
- "/uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
- end
+ def content_type_whitelist = %w[image/jpeg image/gif image/png]
+
+ def store_dir = "/uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
+
+ def size_range = (1.byte)..(5.megabytes)
def paperclip_path
return "/users/:attachment/:id_partition/:style/:basename.:extension" if APP_CONFIG["fog"].blank?
@@ -31,4 +33,10 @@ class BaseUploader < CarrierWave::Uploader::Base
image.crop "#{w}x#{h}+#{x}+#{y}"
end
end
+
+ def remove_animation
+ return unless content_type == "image/gif"
+
+ manipulate!(&:collapse!)
+ end
end
diff --git a/app/uploaders/profile_header_uploader.rb b/app/uploaders/profile_header_uploader.rb
index 23a47588..649ab10e 100644
--- a/app/uploaders/profile_header_uploader.rb
+++ b/app/uploaders/profile_header_uploader.rb
@@ -1,7 +1,7 @@
class ProfileHeaderUploader < BaseUploader
- def default_url(*args)
- "/images/header/#{[version_name || args.first, 'no_header.jpg'].compact.join('/')}"
- end
+ def default_url(*args) = "/images/header/#{[version_name || args.first, 'no_header.jpg'].compact.join('/')}"
+
+ def size_range = (1.byte)..(10.megabytes)
version :web do
process resize_to_fit: [1500, 350]
diff --git a/app/validators/typoed_email_validator.rb b/app/validators/typoed_email_validator.rb
index 717fae97..73dbc446 100644
--- a/app/validators/typoed_email_validator.rb
+++ b/app/validators/typoed_email_validator.rb
@@ -56,7 +56,7 @@ class TypoedEmailValidator < ActiveModel::EachValidator
# check if the TLD is valid
tld = domain_parts.last
- return false unless TLDv.valid?(tld)
+ return false unless TLDv.valid?(tld) || (Rails.env.test? && %w[example test].include?(tld))
# finally, common typos
return false if INVALID_ENDINGS.any? { value.end_with?(_1) }
diff --git a/app/views/actions/_answer.html.haml b/app/views/actions/_answer.html.haml
index e63e452b..e029fe19 100644
--- a/app/views/actions/_answer.html.haml
+++ b/app/views/actions/_answer.html.haml
@@ -1,13 +1,8 @@
.dropdown-menu.dropdown-menu-end{ role: :menu }
- - if subscribed_answer_ids&.include?(answer.id)
- -# fun joke should subscribe?
- %a.dropdown-item{ href: "#", data: { a_id: answer.id, action: "ab-submarine", torpedo: "no" } }
- %i.fa.fa-fw.fa-anchor
- = t("voc.unsubscribe")
+ - if answer.is_subscribed
+ = render "subscriptions/destroy", answer: answer
- else
- %a.dropdown-item{ href: "#", data: { a_id: answer.id, action: "ab-submarine", torpedo: "yes" } }
- %i.fa.fa-fw.fa-anchor
- = t("voc.subscribe")
+ = render "subscriptions/create", answer: answer
- if privileged? answer.user
%a.dropdown-item.text-danger{ href: "#", data: { a_id: answer.id, action: "ab-destroy" } }
%i.fa.fa-fw.fa-trash-o
diff --git a/app/views/actions/_comment.html.haml b/app/views/actions/_comment.html.haml
index 23b8e408..a88d7a4a 100644
--- a/app/views/actions/_comment.html.haml
+++ b/app/views/actions/_comment.html.haml
@@ -1,5 +1,5 @@
.dropdown-menu.dropdown-menu-end{ role: :menu }
- %a.dropdown-item{ href: "#", data: { bs_target: "#modal-view-comment#{comment.id}-smiles", bs_toggle: :modal } }
+ = link_to comment_reactions_path(username: comment.user.screen_name, id: comment.id), class: "dropdown-item", data: { turbo_frame: "modal" } do
%i.fa.fa-fw.fa-smile-o
= t(".view_smiles")
- if privileged?(comment.user) || privileged?(answer.user)
diff --git a/app/views/actions/_question.html.haml b/app/views/actions/_question.html.haml
index c96e99a9..504e3e2c 100644
--- a/app/views/actions/_question.html.haml
+++ b/app/views/actions/_question.html.haml
@@ -8,9 +8,10 @@
%i.fa.fa-fw.fa-exclamation-triangle
= t("voc.report")
- if question.anonymous? && !question.generated?
- = button_to anonymous_block_path, method: :post, params: { question: question.id }, class: "dropdown-item" do
- %i.fa.fa-fw.fa-minus-circle
- = t("voc.block")
+ - unless question.user == current_user
+ = button_to anonymous_block_path, method: :post, params: { question: question.id }, class: "dropdown-item" do
+ %i.fa.fa-fw.fa-minus-circle
+ = t("voc.block")
- if current_user.mod?
= button_to anonymous_block_path, method: :post, params: { question: question.id, global: true }, class: "dropdown-item" do
%i.fas.fa-fw.fa-user-slash
diff --git a/app/views/actions/_share.html.haml b/app/views/actions/_share.html.haml
index 65daacc2..039ef411 100644
--- a/app/views/actions/_share.html.haml
+++ b/app/views/actions/_share.html.haml
@@ -2,11 +2,17 @@
%a.dropdown-item{ href: twitter_share_url(answer), target: "_blank" }
%i.fa.fa-fw.fa-twitter
= t(".twitter")
+ %a.dropdown-item{ href: bluesky_share_url(answer), target: "_blank" }
+ %i.fa.fa-fw.fa-cloud
+ = t(".bluesky")
%a.dropdown-item{ href: tumblr_share_url(answer), target: "_blank" }
%i.fa.fa-fw.fa-tumblr
= t(".tumblr")
%a.dropdown-item{ href: telegram_share_url(answer), target: "_blank" }
%i.fa.fa-fw.fa-telegram
= t(".telegram")
- %a.dropdown-item{ href: "#", name: "ab-share" }
+ %a.dropdown-item{ href: "#", data: { controller: :clipboard, action: "clipboard#copy", clipboard_copy_value: prepare_tweet(answer) } }
+ %i.fa.fa-fw.fa-solid.fa-copy
+ = t(".copy")
+ %a.dropdown-item{ href: "#", data: { controller: :share, action: "share#share", share_url_value: answer_share_url(answer) } }
= t(".other")
diff --git a/app/views/answer/show.html.haml b/app/views/answer/show.html.haml
index 55f2a90b..4cc12576 100644
--- a/app/views/answer/show.html.haml
+++ b/app/views/answer/show.html.haml
@@ -1,4 +1,4 @@
- provide(:title, answer_title(@answer))
- provide(:og, answer_opengraph(@answer))
.container-lg.container--main
- = render "answerbox", a: @answer, display_all: @display_all, subscribed_answer_ids: @subscribed_answer_ids
+ = render "answerbox", a: @answer, display_all: @display_all
diff --git a/app/views/answerbox/_actions.html.haml b/app/views/answerbox/_actions.html.haml
index 5c152abe..961507c2 100644
--- a/app/views/answerbox/_actions.html.haml
+++ b/app/views/answerbox/_actions.html.haml
@@ -1,16 +1,17 @@
-%button.btn.btn-link.answerbox__action{ type: :button, name: "ab-smile", data: { a_id: a.id, action: current_user&.smiled?(a) ? :unsmile : :smile, selection_hotkey: "s" }, disabled: !user_signed_in? }
- %i.fa.fa-fw.fa-smile-o
- %span{ id: "ab-smile-count-#{a.id}" }= a.smiles.count
+- if a.has_reacted
+ = render "reactions/destroy", type: "Answer", target: a
+- else
+ = render "reactions/create", type: "Answer", target: a
- unless display_all
%button.btn.btn-link.answerbox__action{ type: :button, name: "ab-comments", data: { a_id: a.id, state: :hidden, selection_hotkey: "x" } }
%i.fa.fa-fw.fa-comments
%span{ id: "ab-comment-count-#{a.id}" }= a.comment_count
-.btn-group
+.dropdown.d-inline
%button.btn.btn-link.answerbox__action{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
%i.fa.fa-fw.fa-share-alt{ title: t(".share.title") }
= render "actions/share", answer: a
- if user_signed_in?
- .btn-group
- %button.btn.btn-default.btn-sm.dropdown-toggle{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
- %span.caret
- = render "actions/answer", answer: a, subscribed_answer_ids:
+ .dropdown.d-inline
+ %button.btn.btn-link.answerbox__action{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
+ %i.fa.fa-fw.fa-ellipsis
+ = render "actions/answer", answer: a
diff --git a/app/views/answerbox/_comments.html.haml b/app/views/answerbox/_comments.html.haml
index 023990e9..e7d61654 100644
--- a/app/views/answerbox/_comments.html.haml
+++ b/app/views/answerbox/_comments.html.haml
@@ -1,43 +1,6 @@
-- if a.comments.all.count.zero?
+- if comments.all.count.zero?
= t(".none")
- else
%ul.comment__container
- - a.comments.order(:created_at).each do |comment|
- %li.comment{ data: { comment_id: comment.id } }
- %div{ style: "height: 0; width: 0" }= render "modal/comment_smiles", comment: comment
- .d-flex
- .flex-shrink-0
- %a{ href: user_path(comment.user) }
- %img.comment__user-avatar.avatar-sm{ src: comment.user.profile_picture.url(:small), loading: :lazy }
- .flex-grow-1
- %h6.comment__user
- = user_screen_name comment.user
- %span.text-muted{ title: comment.created_at, data: { bs_toggle: :tooltip, bs_placement: :right } }
- = t("time.distance_ago", time: time_ago_in_words(comment.created_at))
- .comment__content
- = markdown comment.content
- .flex-shrink-0.ms-auto
- %button.btn.btn-link.answerbox__action{ type: :button, name: "ab-smile-comment", data: { c_id: comment.id, action: current_user&.smiled?(comment) ? :unsmile : :smile }, disabled: !user_signed_in? }
- %i.fa.fa-fw.fa-smile-o
- %span{ id: "ab-comment-smile-count-#{comment.id}" }= comment.smile_count
- .btn-group
- %button.btn.btn-link.btn-sm.dropdown-toggle{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
- %span.caret
- = render "actions/comment", comment: comment, answer: a
-- if user_signed_in?
- %button.d-none{ name: "ab-open-and-comment", data: { a_id: a.id, selection_hotkey: "c" } }
- .comment__compose-wrapper{
- name: "ab-comment-new-group",
- data: { a_id: a.id, controller: "character-count", character_count_max_value: 512 }
- }
- .form-group.has-feedback.comment__input-group.input-group
- %textarea.form-control.comment__input{ type: :text, placeholder: t(".placeholder"), name: "ab-comment-new", data: { a_id: a.id, "character-count-target": "input" } }
- .comment__submit-wrapper
- %button.btn.btn-primary{
- type: :button,
- name: "ab-comment-new-submit",
- title: t(".action"),
- data: { a_id: a.id, "character-count-target": "action" }
- }
- %i.fa.fa-paper-plane-o
- %span.text-muted.form-control-feedback.comment__character-count{ id: "ab-comment-charcount-#{a.id}", data: { "character-count-target": "counter" } } 512
+ - comments.order(:created_at).each do |comment|
+ = render CommentComponent.new(comment:, answer: a)
diff --git a/app/views/answerbox/_header.html.haml b/app/views/answerbox/_header.html.haml
deleted file mode 100644
index 30dd9738..00000000
--- a/app/views/answerbox/_header.html.haml
+++ /dev/null
@@ -1,26 +0,0 @@
-.card-header
- .d-flex
- - unless a.question.author_is_anonymous
- .flex-shrink-0
- %a{ href: user_path(a.question.user) }
- %img.answerbox__question-user-avatar.avatar-md{ src: a.question.user.profile_picture.url(:small), loading: :lazy }
- .flex-grow-1
- %h6.text-muted.answerbox__question-user
- - if a.question.author_is_anonymous
- %i.fas.fa-user-secret{ title: t(".anon_hint") }
- = t(".asked_html", user: user_screen_name(a.question.user, context_user: a.user, author_identifier: a.question.author_is_anonymous ? a.question.author_identifier: nil), time: time_tooltip(a.question))
- - if !a.question.author_is_anonymous && !a.question.direct
- ·
- %a{ href: question_path(a.question.user.screen_name, a.question.id), data: { selection_hotkey: "a" } }
- = t(".answers", count: a.question.answer_count)
- .answerbox__question-body{ data: { controller: a.question.long? ? "collapse" : nil } }
- .answerbox__question-text{ class: a.question.long? && !display_all ? "collapsed" : "", data: { collapse_target: "content" } }
- = question_markdown a.question.content
- - if a.question.long? && !display_all
- = render "shared/collapse", type: "question"
- - if user_signed_in?
- .flex-shrink-0.ms-auto
- .btn-group
- %button.btn.btn-link.btn-sm.dropdown-toggle{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
- %span.caret
- = render "actions/question", question: a.question
diff --git a/app/views/answerbox/_smiles.html.haml b/app/views/answerbox/_smiles.html.haml
index 518c72ee..68f02e63 100644
--- a/app/views/answerbox/_smiles.html.haml
+++ b/app/views/answerbox/_smiles.html.haml
@@ -8,5 +8,5 @@
- a.smiles.all.each do |smile|
%a{ href: user_path(smile.user),
title: user_screen_name(smile.user, url: false),
- data: { bs_toggle: :tooltip, bs_placement: :top, smile_id: smile.id } }
- %img.avatar-xs{ src: smile.user.profile_picture.url(:small), loading: :lazy }
+ data: { controller: :tooltip, bs_placement: :top, smile_id: smile.id, turbo: :false } }
+ = render AvatarComponent.new(user: smile.user, size: "xs")
diff --git a/app/views/application/_answerbox.html.haml b/app/views/application/_answerbox.html.haml
index b84a5f17..565447d1 100644
--- a/app/views/application/_answerbox.html.haml
+++ b/app/views/application/_answerbox.html.haml
@@ -1,39 +1,55 @@
- display_all ||= nil
.card.answerbox{ data: { id: a.id, q_id: a.question.id, navigation_target: "traversable" } }
- if @question.nil?
- = render "answerbox/header", a: a, display_all: display_all
+ .card-header
+ = render QuestionComponent.new(question: a.question, context_user: a.user, collapse: !display_all)
.card-body
.answerbox__answer-body{ data: { controller: a.long? ? "collapse" : nil } }
.answerbox__answer-text{ class: a.long? && !display_all ? "collapsed" : "", data: { collapse_target: "content" } }
= markdown a.content
- if a.long? && !display_all
= render "shared/collapse", type: "answer"
- - if @user.nil?
- .row
- .col-sm-6.text-start.text-muted
- .d-flex
- .flex-shrink-0
- %a{ href: user_path(a.user) }
- %img.answerbox__answer-user-avatar.avatar-sm{ src: a.user.profile_picture.url(:small), loading: :lazy }
- .flex-grow-1
- %h6.answerbox__answer-user
- = raw t(".answered", hide: hidespan(t(".hide"), "d-none d-sm-inline"), user: user_screen_name(a.user))
- .answerbox__answer-date
- = link_to(raw(t("time.distance_ago", time: time_tooltip(a))), answer_path(a.user.screen_name, a.id), data: { selection_hotkey: "l" })
- .col-md-6.d-flex.d-md-block.answerbox__actions
- = render "answerbox/actions", a:, display_all:, subscribed_answer_ids:
- - else
- .row
- .col-md-6.text-start.text-muted
- %i.fa.fa-clock-o
- = link_to(raw(t("time.distance_ago", time: time_tooltip(a))), answer_path(a.user.screen_name, a.id), class: "answerbox__permalink")
- - if a.pinned_at.present?
- %span.answerbox__pinned
+ .d-md-flex
+ .text-muted
+ .d-flex.align-items-center
+ .flex-shrink-0
+ %a{ href: user_path(a.user) }
+ = render AvatarComponent.new(user: a.user, size: "sm", classes: ["answerbox__answer-user-avatar"])
+ .flex-grow-1
+ %h6.answerbox__answer-user
+ = user_screen_name(a.user)
·
- %i.fa.fa-thumbtack
- = t(".pinned")
- .col-md-6.d-md-flex.answerbox__actions
- = render "answerbox/actions", a:, display_all:, subscribed_answer_ids:
+ = link_to(time_tooltip(a), answer_path(a.user.screen_name, a.id), data: { selection_hotkey: "l" })
+ - if a.pinned_at.present?
+ %span.answerbox__pinned
+ ·
+ %i.fa.fa-thumbtack
+ = t(".pinned")
+ .d-flex.d-md-block.answerbox__actions.ms-auto
+ = render "answerbox/actions", a:, display_all:
.card-footer{ id: "ab-comments-section-#{a.id}", class: display_all.nil? ? "d-none" : nil }
- %div{ id: "ab-smiles-#{a.id}" }= render "answerbox/smiles", a: a
- %div{ id: "ab-comments-#{a.id}" }= render "answerbox/comments", a: a
+ = turbo_frame_tag("ab-reactions-list-#{a.id}", src: reactions_path(a.question, a), loading: :lazy) do
+ .d-flex.smiles
+ .flex-shrink-0.me-1
+ %i.fa.fa-smile-o
+ = turbo_frame_tag("ab-comments-list-#{a.id}", src: comments_path(a.question, a), loading: :lazy) do
+ .d-flex.justify-content-center
+ .spinner-border{ role: :status }
+ .visually-hidden= t("voc.loading")
+ - if user_signed_in?
+ %button.d-none{ name: "ab-open-and-comment", data: { a_id: a.id, selection_hotkey: "c" } }
+ .comment__compose-wrapper{
+ name: "ab-comment-new-group",
+ data: { a_id: a.id, controller: "character-count", character_count_max_value: 512 }
+ }
+ .form-group.has-feedback.comment__input-group.input-group
+ %textarea.form-control.comment__input{ type: :text, placeholder: t(".comments.placeholder"), name: "ab-comment-new", data: { a_id: a.id, "character-count-target": "input" } }
+ .comment__submit-wrapper
+ %button.btn.btn-primary{
+ type: :button,
+ name: "ab-comment-new-submit",
+ title: t(".comments.action"),
+ data: { a_id: a.id, "character-count-target": "action" }
+ }
+ %i.fa.fa-paper-plane-o
+ %span.text-muted.form-control-feedback.comment__character-count{ id: "ab-comment-charcount-#{a.id}", data: { "character-count-target": "counter" } } 512
diff --git a/app/views/application/_questionbox.html.haml b/app/views/application/_questionbox.html.haml
index d56fefda..c6bfee72 100644
--- a/app/views/application/_questionbox.html.haml
+++ b/app/views/application/_questionbox.html.haml
@@ -1,4 +1,4 @@
-.card
+.card#question-card
.card-header
- if user.profile.motivation_header.blank?
= t(".title")
diff --git a/app/views/comments/index.html.haml b/app/views/comments/index.html.haml
new file mode 100644
index 00000000..e5334203
--- /dev/null
+++ b/app/views/comments/index.html.haml
@@ -0,0 +1,2 @@
+= turbo_frame_tag "ab-comments-list-#{a.id}" do
+ %div{ id: "ab-comments-#{a.id}" }= render "answerbox/comments", a:, comments: @comments
diff --git a/app/views/comments/reactions/index.html.haml b/app/views/comments/reactions/index.html.haml
new file mode 100644
index 00000000..690943f8
--- /dev/null
+++ b/app/views/comments/reactions/index.html.haml
@@ -0,0 +1,21 @@
+= turbo_frame_tag "modal" do
+ .modal.fade.show.d-block{ id: "modal-view-comment-smiles", aria: { hidden: false, labelledby: "modal-commentsmile-label" }, role: :dialog, tabindex: -1 }
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %h5.modal-title#modal-commentsmile-label= t(".title")
+ = button_to modal_close_path, method: :get, class: "btn-close" do
+ %span.visually-hidden Close
+ .modal-body
+ - if @reactions.count.zero?
+ = t(".none")
+ - else
+ %ul.smiles__user-list
+ - @reactions.each do |smile|
+ %li.smiles__user-list-entry
+ %a{ href: user_path(smile.user) }
+ %img{ src: smile.user.profile_picture.url(:small), alt: user_screen_name(smile.user, url: false) }
+ %span= user_screen_name(smile.user, url: false)
+ .modal-footer
+ = button_to t("voc.close"), modal_close_path, method: :get, class: "btn btn-default"
+ = link_to "", modal_close_path, method: :get, class: "modal-backdrop fade show z-n1"
diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml
index 698e63b9..d0fa29d6 100644
--- a/app/views/devise/registrations/new.html.haml
+++ b/app/views/devise/registrations/new.html.haml
@@ -5,7 +5,7 @@
.card.mt-3
.card-body
%h1= t(".title")
- = bootstrap_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
+ = bootstrap_form_for(resource, as: resource_name, url: registration_path(resource_name), data: { turbo: false }) do |f|
= render "devise/shared/error_messages", resource: resource
= render "layouts/messages"
diff --git a/app/views/discover/_userbox.html.haml b/app/views/discover/_userbox.html.haml
index 8c322b50..8293104b 100644
--- a/app/views/discover/_userbox.html.haml
+++ b/app/views/discover/_userbox.html.haml
@@ -3,9 +3,9 @@
.d-flex
.flex-shrink-0
%a{ href: user_path(u) }
- %img.avatar-md.me-2{ src: u.profile_picture.url(:medium) }
+ = render AvatarComponent.new(user: u, size: "md", classes: ["me-2"])
.flex-grow-1
- %h6.answerbox__question-user
+ %h6.question__user
- if u.profile.display_name.blank?
%a{ href: user_path(u) }
= u.screen_name
@@ -13,4 +13,4 @@
%a{ href: user_path(u) }
= u.profile.display_name
%span.text-muted= u.screen_name
- %p.answerbox__question-text= t(".#{type}", value: value, count: value)
+ %p.question__text= t(".#{type}", value: value, count: value)
diff --git a/app/views/discover/tab/_answers.html.haml b/app/views/discover/tab/_answers.html.haml
index 4438edec..cef9a25a 100644
--- a/app/views/discover/tab/_answers.html.haml
+++ b/app/views/discover/tab/_answers.html.haml
@@ -1,3 +1,3 @@
.tab-pane.active.fade.show{ role: :tabpanel, id: "answers" }
- answers.each do |a|
- = render "answerbox", a:, subscribed_answer_ids:
+ = render "answerbox", a:
diff --git a/app/views/discover/tab/_discussed.html.haml b/app/views/discover/tab/_discussed.html.haml
index 7b3b7375..ed288036 100644
--- a/app/views/discover/tab/_discussed.html.haml
+++ b/app/views/discover/tab/_discussed.html.haml
@@ -1,3 +1,3 @@
.tab-pane.fade{ role: :tabpanel, id: "comments" }
- comments.each do |a|
- = render "answerbox", a:, subscribed_answer_ids:
+ = render "answerbox", a:
diff --git a/app/views/inbox/_entry.html.haml b/app/views/inbox/_entry.html.haml
index eb92ed7d..411ce337 100644
--- a/app/views/inbox/_entry.html.haml
+++ b/app/views/inbox/_entry.html.haml
@@ -1,40 +1,18 @@
.card.inbox-entry{ id: "inbox_#{i.id}", class: i.new? ? "inbox-entry--new" : "", data: { id: i.id } }
.card-header
- .d-flex
- - unless i.question.author_is_anonymous
- .flex-shrink-0
- %a.pull-left{ href: user_path(i.question.user) }
- %img.answerbox__question-user-avatar.avatar-md{ src: i.question.user.profile_picture.url(:small), loading: :lazy }
- .flex-grow-1
- %h6.text-muted.answerbox__question-user
- - if i.question.author_is_anonymous
- %i.fas.fa-user-secret{ title: t('.anon_hint') }
- = t(".asked_html", user: user_screen_name(i.question.user, context_user: i.user, author_identifier: i.question.author_is_anonymous ? i.question.author_identifier : nil), time: time_tooltip(i.question))
- - if !i.question.author_is_anonymous && i.question.answer_count.positive?
- ·
- %a{ href: question_path(i.question.user.screen_name, i.question.id) }
- = t(".answers", count: i.question.answer_count)
- .answerbox__question-body{ data: { controller: i.question.long? ? "collapse" : nil } }
- .answerbox__question-text{ class: i.question.long? ? "collapsed" : "", data: { collapse_target: "content" } }
- = question_markdown i.question.content
- - if i.question.long?
- = render "shared/collapse", type: "question"
- - if i.question.user_id != current_user.id || current_user.has_cached_role?(:administrator)
- .flex-shrink-0.ms-auto
- .btn-group
- %button.btn.btn-default.btn-sm.dropdown-toggle{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
- %span.caret
- = render "actions/question", question: i.question
+ = render QuestionComponent.new(question: i.question, context_user: i.user)
- if current_user == i.user
.card-body
%textarea.form-control.mb-3{ name: "ib-answer", placeholder: t(".placeholder"), data: { id: i.id } }
- .d-sm-flex
- %button.btn.btn-success.me-sm-1{ name: "ib-answer", data: { ib_id: i.id } }
- = t("voc.answer")
- %button.btn.btn-danger.me-sm-1{ name: "ib-destroy", data: { ib_id: i.id } }
- = t("voc.delete")
- %p.format-help.ms-auto.align-self-center.mt-2.mt-sm-0.text-center
+ .d-flex.flex-column-reverse.flex-sm-row
+ %p.format-help.me-sm-auto.align-self-center.mb-0.mt-2.mt-sm-0.text-center
= render "shared/format_link"
+ .d-grid.gap-2.d-sm-block
+ %button.btn.btn-default.text-muted.me-sm-1.mb-sm-0{ name: "ib-destroy", data: { ib_id: i.id } }
+ %i.fa.fa-trash.fa-fw
+ = t("voc.delete")
+ %button.btn.btn-primary.grid-row-1{ name: "ib-answer", data: { ib_id: i.id } }
+ = t("voc.answer")
- if current_user.sharing_enabled
.inbox-entry__sharing.text-center.p-2.justify-content-center.d-none{
data: { controller: "inbox-sharing", inbox_sharing_config_value: "{}", inbox_sharing_auto_close_value: current_user.sharing_autoclose.to_s } }
@@ -43,16 +21,25 @@
.align-self-center
%p.fs-3.fw-bold= t(".sharing.heading")
%p
- %a.btn.btn-primary{ href: "https://twitter.com/intent/tweet?text=", data: { inbox_sharing_target: "twitter" }, target: "_blank" }
+ %a.btn.btn-primary.mb-1{ href: "#", data: { inbox_sharing_target: "twitter" }, target: "_blank" }
%i.fab.fa-twitter.fa-fw
Twitter
- %a.btn.btn-primary{ href: "#", data: { inbox_sharing_target: "tumblr" }, target: "_blank" }
+ %a.btn.btn-primary.mb-1{ href: "#", data: { inbox_sharing_target: "bluesky" }, target: "_blank" }
+ %i.fas.fa-cloud.fa-fw
+ Bluesky
+ %a.btn.btn-primary.mb-1{ href: "#", data: { inbox_sharing_target: "tumblr" }, target: "_blank" }
%i.fab.fa-tumblr.fa-fw
Tumblr
- %a.btn.btn-primary{ href: "#", data: { inbox_sharing_target: "telegram" }, target: "_blank" }
+ %a.btn.btn-primary.mb-1{ href: "#", data: { inbox_sharing_target: "telegram" }, target: "_blank" }
%i.fab.fa-telegram.fa-fw
Telegram
+ %button.btn.btn-primary.mb-1{ data: { controller: :clipboard, action: "clipboard#copy", inbox_sharing_target: "clipboard" } }
+ %i.fa.fa-fw.fa-solid.fa-copy
+ = t("actions.share.copy")
+ %button.btn.btn-primary.mb-1{ data: { controller: "share", action: "share#share", inbox_sharing_target: "other" } }
+ %i.fa.fa-fw.fa-share-alt
+ = t("actions.share.other")
- if current_user.sharing_custom_url.present?
- %a.btn.btn-primary{ href: current_user.sharing_custom_url, data: { inbox_sharing_target: "custom" }, target: "_blank" }
+ %a.btn.btn-primary.mb-1{ href: current_user.sharing_custom_url, data: { inbox_sharing_target: "custom" }, target: "_blank" }
= current_user.display_sharing_custom_url
%p.text-muted= t(".sharing.hint_html", settings: link_to(t(".sharing.settings"), settings_sharing_path))
diff --git a/app/views/inbox/show.html.haml b/app/views/inbox/show.html.haml
index 802127ee..8299b6ce 100644
--- a/app/views/inbox/show.html.haml
+++ b/app/views/inbox/show.html.haml
@@ -3,7 +3,7 @@
= render "inbox/entry", i:
- if @inbox.empty?
- %p.empty= t(".empty")
+ = render "shared/empty", type: "inbox"
- if @more_data_available
.d-flex.justify-content-center#paginator
diff --git a/app/views/inbox/show.turbo_stream.haml b/app/views/inbox/show.turbo_stream.haml
index bbd233bd..e88cfc8d 100644
--- a/app/views/inbox/show.turbo_stream.haml
+++ b/app/views/inbox/show.turbo_stream.haml
@@ -1,3 +1,5 @@
+- inbox_count = current_user.unread_inbox_count
+
= turbo_stream.append "entries" do
- @inbox.each do |i|
= render "inbox/entry", i:
@@ -10,3 +12,13 @@
params: { last_id: @inbox_last_id, author: @author }.compact,
data: { controller: :hotkey, hotkey: "." },
form: { data: { turbo_stream: true } }
+
+= turbo_stream.update "nav-inbox-desktop" do
+ = nav_entry t("navigation.inbox"), "/inbox",
+ badge: inbox_count, badge_attr: { data: { controller: "pwa-badge" } },
+ icon: "inbox", hotkey: "g i"
+
+= turbo_stream.update "nav-inbox-mobile" do
+ = nav_entry t("navigation.inbox"), "/inbox",
+ badge: inbox_count, badge_color: "primary", badge_pill: true,
+ icon: "inbox", icon_only: true
diff --git a/app/views/layouts/base.html.haml b/app/views/layouts/base.html.haml
index 0607c377..cf829de8 100644
--- a/app/views/layouts/base.html.haml
+++ b/app/views/layouts/base.html.haml
@@ -32,6 +32,7 @@
= yield
= render "shared/formatting"
= render "shared/hotkeys"
+ = turbo_frame_tag "modal"
.d-none#toasts
- if Rails.env.development?
#debug
diff --git a/app/views/layouts/user/profile.html.haml b/app/views/layouts/user/profile.html.haml
index 1af75a29..ef497627 100644
--- a/app/views/layouts/user/profile.html.haml
+++ b/app/views/layouts/user/profile.html.haml
@@ -8,8 +8,9 @@
.d-none.d-sm-block= render 'shared/links'
.col-lg-9.col-md-8.col-xs-12.col-sm-8
= render 'questionbox', user: @user
- = render 'tabs/profile', user: @user
- = yield
+ - unless @user.banned?
+ = render 'tabs/profile', user: @user
+ = yield
- if user_signed_in?
= render 'modal/list', user: @user
- if current_user.mod? && @user != current_user
diff --git a/app/views/modal/_ask.html.haml b/app/views/modal/_ask.html.haml
index 00192b61..326a3172 100644
--- a/app/views/modal/_ask.html.haml
+++ b/app/views/modal/_ask.html.haml
@@ -6,9 +6,17 @@
%button.btn-close{ data: { bs_dismiss: :modal }, type: :button }
%span.visually-hidden= t("voc.close")
.modal-body
+ - if @user
+ .alert.alert-info.d-sm-none= t(".user_note_html", user: @user.profile.safe_name)
+ - if current_user.followers.count.zero?
+ .alert.alert-warning= t(".follower_note_html")
.form-group.has-feedback
%textarea.form-control{ name: "qb-all-question", placeholder: t(".placeholder"), data: { "character-count-warning-target": "input" } }
.alert.alert-warning.mt-3.d-none{ data: { "character-count-warning-target": "warning" } }= t('.long_question_warning')
.modal-footer
- %button.btn.btn-default{ type: :button, data: { bs_dismiss: :modal } }= t("voc.cancel")
- %button.btn.btn-primary{ name: "qb-all-ask", type: :button, data: { loading_text: t(".loading") } }= t(".action")
+ .flex-grow-1
+ %input.form-check-input#qb-send-to-own-inbox{ type: :checkbox }
+ %label.form-check-label{ for: 'qb-send-to-own-inbox' }= t('.send_to_own_inbox')
+ .flex-grow-1.d-flex
+ %button.btn.btn-default.ms-auto{ type: :button, data: { bs_dismiss: :modal } }= t("voc.cancel")
+ %button.btn.btn-primary{ name: "qb-all-ask", type: :button, data: { loading_text: t(".loading") } }= t(".action")
diff --git a/app/views/modal/_comment_smiles.html.haml b/app/views/modal/_comment_smiles.html.haml
deleted file mode 100644
index 70cf7772..00000000
--- a/app/views/modal/_comment_smiles.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-.modal.fade{ id: "modal-view-comment#{comment.id}-smiles", aria: { hidden: true, labelledby: "modal-commentsmile-label" }, role: :dialog, tabindex: -1 }
- .modal-dialog
- .modal-content
- .modal-header
- %h5.modal-title#modal-commentsmile-label= t(".title")
- %button.btn-close{ data: { bs_dismiss: :modal }, type: :button }
- %span.visually-hidden Close
- .modal-body
- - if comment.smiles.all.count.zero?
- = t(".none")
- - else
- %ul.smiles__user-list
- - comment.smiles.all.each do |smile|
- %li.smiles__user-list-entry
- %a{ href: user_path(smile.user) }
- %img{ src: smile.user.profile_picture.url(:small), alt: user_screen_name(smile.user, url: false) }
- %span= user_screen_name(smile.user, url: false)
diff --git a/app/views/modal/_privileges.html.haml b/app/views/modal/_privileges.html.haml
index 7444e664..45ad7eaf 100644
--- a/app/views/modal/_privileges.html.haml
+++ b/app/views/modal/_privileges.html.haml
@@ -9,6 +9,6 @@
%ul.list-group
- if current_user.has_cached_role?(:administrator)
= render "modal/privileges/item", privilege: "moderator", description: t(".role.moderator"), user: user
- = render "modal/privileges/item", privilege: "admin", description: t(".role.admin"), user: user
+ = render "modal/privileges/item", privilege: "administrator", description: t(".role.admin"), user: user
.modal-footer
%button.btn.btn-primary{ name: "checked-privileges", type: :button, data: { bs_dismiss: :modal } }= t("voc.close")
diff --git a/app/views/modal/privileges/_item.html.haml b/app/views/modal/privileges/_item.html.haml
index f87b4d3b..c9be9a30 100644
--- a/app/views/modal/privileges/_item.html.haml
+++ b/app/views/modal/privileges/_item.html.haml
@@ -1,12 +1,13 @@
:ruby
description ||= ""
- role_mapping = { admin: "administrator" }
- requires_role = %w[admin moderator].include?(privilege)
- checked = requires_role ? user.has_cached_role?(role_mapping.fetch(privilege, privilege).to_sym) : user.public_send("#{privilege}?")
%li.list-group-item{ id: "privilege-#{privilege}" }
.d-flex
.flex-shrink-0
- %input{ type: :checkbox, name: "check-your-privileges", data: { type: privilege, user: user.screen_name }, checked: checked, autocomplete: :off }
+ %input{ type: :checkbox,
+ name: "check-your-privileges",
+ data: { type: privilege, user: user.screen_name },
+ checked: user.has_cached_role?(privilege.to_sym),
+ autocomplete: :off }
.flex-grow-1
.list-group-item-heading= privilege.capitalize
- unless description.blank?
diff --git a/app/views/moderation/_moderationbox.html.haml b/app/views/moderation/_moderationbox.html.haml
index 721177af..ef0c16fb 100644
--- a/app/views/moderation/_moderationbox.html.haml
+++ b/app/views/moderation/_moderationbox.html.haml
@@ -1,6 +1,6 @@
.card.moderationbox{ data: { id: report.id } }
.card-header
- %img.avatar-sm{ src: report.user.profile_picture.url(:small), loading: :lazy }
+ = render AvatarComponent.new(user: report.user, size: "sm")
= t(".reported_html",
user: user_screen_name(report.user),
content: report.type.sub("Reports::", ""),
diff --git a/app/views/moderation/inbox/_header.html.haml b/app/views/moderation/inbox/_header.html.haml
index eae01b4b..830583cb 100644
--- a/app/views/moderation/inbox/_header.html.haml
+++ b/app/views/moderation/inbox/_header.html.haml
@@ -1,9 +1,9 @@
-.card.question--fixed{ class: hidden ? 'question--hidden' : '', tabindex: hidden ? -1 : '', aria: { hidden: hidden } }
+.card.question--sticky
.container
.card-body
.d-flex
.flex-shrink-0
%a{ href: user_path(user) }
- %img.answerbox__question-user-avatar.avatar-md{ src: user.profile_picture.url(:medium) }
+ = render AvatarComponent.new(user:, size: "md", classes: ["question__avatar"])
.flex-grow-1
= t(".title_html", screen_name: user.screen_name, user_id: user.id)
diff --git a/app/views/moderation/inbox/index.html.haml b/app/views/moderation/inbox/index.html.haml
index 7e400437..3cae722c 100644
--- a/app/views/moderation/inbox/index.html.haml
+++ b/app/views/moderation/inbox/index.html.haml
@@ -1,10 +1,12 @@
- provide(:title, generate_title(t(".title", user: @user.screen_name)))
-= render "header", user: @user, hidden: false
-= render "header", user: @user, hidden: true
+= render "header", user: @user
-.container-lg.container--main
+.container-lg.question-page
#entries
+ - if @inboxes.empty?
+ = render "shared/empty", icon: "fa fa-inbox", translation_key: ".moderation.inbox"
+
- @inboxes.each do |i|
= render "inbox/entry", i: i
diff --git a/app/views/moderation/questions/_header.html.haml b/app/views/moderation/questions/_header.html.haml
index c8984458..e42f6a4e 100644
--- a/app/views/moderation/questions/_header.html.haml
+++ b/app/views/moderation/questions/_header.html.haml
@@ -1,4 +1,4 @@
-.card.question--fixed{ class: hidden ? 'question--hidden' : '', tabindex: hidden ? -1 : '', aria: { hidden: hidden } }
+.card.question--sticky
.container
.card-body
.d-flex
diff --git a/app/views/moderation/questions/show.html.haml b/app/views/moderation/questions/show.html.haml
index 48617bf2..934ada11 100644
--- a/app/views/moderation/questions/show.html.haml
+++ b/app/views/moderation/questions/show.html.haml
@@ -1,8 +1,7 @@
- provide(:title, generate_title(t(".title", author_identifier: params[:author_identifier].truncate(32))))
-= render "header", author_identifier: params[:author_identifier], hidden: false
-= render "header", author_identifier: params[:author_identifier], hidden: true
+= render "header", author_identifier: params[:author_identifier]
-.container-lg.container--main
+.container-lg.question-page
- @questions.each do |q|
= render "shared/question", q:, type: "moderation"
diff --git a/app/views/moderation/reports/index.html.haml b/app/views/moderation/reports/index.html.haml
index bf92cdf7..c7924a3e 100644
--- a/app/views/moderation/reports/index.html.haml
+++ b/app/views/moderation/reports/index.html.haml
@@ -1,4 +1,24 @@
+.card
+ .card-body
+ .dropdown
+ %button.btn.dropdown-toggle{ class: @filter_enabled ? "btn-primary" : "btn-light",
+ type: :button,
+ data: { bs_toggle: :dropdown },
+ aria: { expanded: :false } }
+ %i.fa.fa-filter
+ = t("voc.filter")
+ .dropdown-menu{ style: "min-width: 300px;" }
+ = bootstrap_form_tag url: moderation_reports_path, method: :get, html: { class: "px-3 py-2" } do |f|
+ = f.select :type, options_for_select(@type_options, params[:type]), {}, { class: "form-control" }
+ = f.text_field :user, value: params[:user]
+ = f.text_field :target_user, value: params[:target_user]
+ .d-flex.flex-row-reverse
+ = f.primary t("voc.filter")
+
#reports
+ - if @reports.empty?
+ = render "shared/empty", icon: "fa-regular fa-smile-beam", translation_key: ".moderation.reports"
+
- @reports.each do |r|
= render "moderation/moderationbox", report: r
diff --git a/app/views/navigation/_desktop.html.haml b/app/views/navigation/_desktop.html.haml
index 05e9a6a3..26172183 100644
--- a/app/views/navigation/_desktop.html.haml
+++ b/app/views/navigation/_desktop.html.haml
@@ -10,12 +10,12 @@
DEV
%ul.nav.navbar-nav.me-auto
= nav_entry t("navigation.timeline"), root_path, icon: "home", hotkey: "g t"
- = nav_entry t("navigation.inbox"), "/inbox", icon: "inbox", badge: inbox_count, badge_attr: { data: { controller: "pwa-badge" } }, hotkey: "g i"
+ = nav_entry t("navigation.inbox"), "/inbox", icon: "inbox", badge: inbox_count, badge_attr: { data: { controller: "pwa-badge" } }, hotkey: "g i", id: "nav-inbox-desktop"
- if APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod?
= nav_entry t("navigation.discover"), discover_path, icon: "compass", hotkey: "g d"
%ul.nav.navbar-nav
- if @user.present? && @user != current_user
- %li.nav-item.d-none.d-sm-block{ data: { bs_toggle: 'tooltip', bs_placement: 'bottom' }, title: t(".list") }
+ %li.nav-item.d-none.d-sm-block{ data: { controller: 'tooltip', bs_placement: 'bottom' }, title: t(".list") }
%a.nav-link{ href: '#', data: { bs_target: '#modal-list-memberships', bs_toggle: :modal } }
%i.fa.fa-list.hidden-xs
%span.d-none.d-sm-inline.d-md-none= t(".list")
@@ -23,23 +23,18 @@
%li.nav-item.dropdown.d-none.d-sm-block
%a.nav-link.dropdown-toggle{ href: '#', data: { bs_toggle: :dropdown } }
%turbo-frame#notification-desktop-icon
- - if notification_count.nil?
- %i.fa.fa-bell-o
- - else
- %i.fa.fa-bell
- %span.visually-hidden= t("navigation.notifications")
- %span.badge= notification_count
+ = render "navigation/icons/notifications", notification_count:
.dropdown-menu.dropdown-menu-end.notification-dropdown
%turbo-frame#notifications-dropdown-list
- - cache current_user.notification_dropdown_cache_key do
+ - cache current_user.notification_dropdown_cache_key, expires_in: 12.hours do
- notifications = Notification.for(current_user).where(new: true).includes([:target]).limit(4)
= render "navigation/dropdown/notifications", notifications:, size: "desktop"
- %li.nav-item.d-none.d-sm-block{ data: { bs_toggle: 'tooltip', bs_placement: 'bottom' }, title: t('.ask_question') }
+ %li.nav-item.d-none.d-sm-block{ data: { controller: :tooltip, bs_placement: 'bottom' }, title: t('.ask_question') }
%a.nav-link{ href: "#", name: "toggle-all-ask", data: { bs_target: "#modal-ask-followers", bs_toggle: :modal, hotkey: "n" } }
%i.fa.fa-pencil-square-o
%li.nav-item.dropdown.profile--image-dropdown
%a.nav-link.dropdown-toggle.p-sm-0{ href: "#", data: { bs_toggle: :dropdown } }
- %img.avatar-md.d-none.d-sm-inline{ src: current_user.profile_picture.url(:small) }
+ = render AvatarComponent.new(user: current_user, size: "md", classes: ["d-none", "d-sm-inline"])
%span.d-inline.d-sm-none
= current_user.screen_name
%b.caret
diff --git a/app/views/navigation/_mobile.html.haml b/app/views/navigation/_mobile.html.haml
index 97e37ccc..010d592c 100644
--- a/app/views/navigation/_mobile.html.haml
+++ b/app/views/navigation/_mobile.html.haml
@@ -5,12 +5,12 @@
= nav_entry t("navigation.timeline"), root_path, icon: 'home', icon_only: true
= nav_entry t("navigation.inbox"), '/inbox',
badge: inbox_count, badge_color: 'primary', badge_pill: true,
- icon: 'inbox', icon_only: true
+ icon: 'inbox', icon_only: true, id: "nav-inbox-mobile"
- if APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod?
= nav_entry t("navigation.discover"), discover_path, icon: 'compass', icon_only: true
= nav_entry t("navigation.notifications"), notifications_path("all"), icon: notifications_icon,
badge: notification_count, badge_color: "primary", badge_attr: { id: "notification-mobile-count" }, icon_only: true
%li.nav-item.profile--image-dropdown
%a.nav-link{ href: '#', data: { bs_toggle: 'dropdown', bs_target: '#rs-mobile-nav-profile' }, aria: { controls: 'rs-mobile-nav-profile', expanded: 'false' } }
- %img.avatar-md.d-inline{ src: current_user.profile_picture.url(:small) }
+ = render AvatarComponent.new(user: current_user, size: "md", classes: ["d-inline"])
= render 'navigation/dropdown/profile', size: "mobile"
diff --git a/app/views/navigation/dropdown/_profile.html.haml b/app/views/navigation/dropdown/_profile.html.haml
index aef649a9..1e25574a 100644
--- a/app/views/navigation/dropdown/_profile.html.haml
+++ b/app/views/navigation/dropdown/_profile.html.haml
@@ -16,8 +16,8 @@
= link_to moderation_toggle_unmask_path, class: "dropdown-item", data: { turbo_method: :post } do
%i.fa.fa-toggle-off
= t(".unmask.enable")
- %a.dropdown-item{ href: moderation_reports_path }
- %i.fa.fa-fw.fa-gavel
+ %a.dropdown-item{ class: @has_new_reports ? "text-primary" : nil, href: moderation_reports_path }
+ %i.fa.fa-fw.fa-gavel{ class: @has_new_reports ? "fa-fade" : nil }
= t(".moderation")
- if current_user.has_cached_role?(:administrator)
%a.dropdown-item{ href: admin_dashboard_path }
diff --git a/app/views/navigation/icons/_notifications.html.haml b/app/views/navigation/icons/_notifications.html.haml
new file mode 100644
index 00000000..767ee980
--- /dev/null
+++ b/app/views/navigation/icons/_notifications.html.haml
@@ -0,0 +1,6 @@
+- if notification_count.nil?
+ %i.fa.fa-bell-o
+- else
+ %i.fa.fa-bell
+%span.visually-hidden= t("navigation.notifications")
+%span.badge= notification_count
diff --git a/app/views/notifications/index.turbo_stream.haml b/app/views/notifications/index.turbo_stream.haml
index 4339faae..378a9a44 100644
--- a/app/views/notifications/index.turbo_stream.haml
+++ b/app/views/notifications/index.turbo_stream.haml
@@ -13,3 +13,6 @@
params: { last_id: @notifications_last_id },
data: { controller: :hotkey, hotkey: "." },
form: { data: { turbo_stream: true } }
+
+= turbo_stream.update "notification-desktop-icon" do
+ = render "navigation/icons/notifications", notification_count: current_user.unread_notification_count
diff --git a/app/views/notifications/type/_answer.html.haml b/app/views/notifications/type/_answer.html.haml
index 890fb394..184b58a6 100644
--- a/app/views/notifications/type/_answer.html.haml
+++ b/app/views/notifications/type/_answer.html.haml
@@ -3,15 +3,16 @@
%i.fa.fa-2x.fa-fw.fa-exclamation
.flex-grow-1
.notification__heading
- %img.avatar-xs{ src: notification.target.user.profile_picture.url(:small), loading: :lazy }
+ = render AvatarComponent.new(user: notification.target.user, size: "xs")
= t(".heading_html",
user: user_screen_name(notification.target.user),
- question: link_to(t(".link_text"), answer_path(username: notification.target.user.screen_name, id: notification.target.id), target: "_top"),
- time: time_tooltip(notification.target))
+ question: link_to(t(".link_text"), answer_path(username: notification.target.user.screen_name, id: notification.target.id), target: "_top"))
+ ·
+ = time_tooltip(notification.target)
.list-group
.list-group-item
%h6.notification__list-heading= t("activerecord.models.question.one")
- = markdown notification.target.question.content[0..60] + (notification.target.question.content.length > 60 ? "[…]" : "")
+ = question_markdown notification.target.question.content[0..60] + (notification.target.question.content.length > 60 ? "[…]" : "")
.list-group-item
%h6.notification__list-heading= t("activerecord.models.answer.one")
= markdown notification.target.content[0..60] + (notification.target.content.length > 60 ? "[…]" : "")
diff --git a/app/views/notifications/type/_comment.html.haml b/app/views/notifications/type/_comment.html.haml
index 56c3797c..f591250b 100644
--- a/app/views/notifications/type/_comment.html.haml
+++ b/app/views/notifications/type/_comment.html.haml
@@ -3,23 +3,21 @@
%i.fa.fa-2x.fa-fw.fa-comments
.flex-grow-1
.notification__heading
- %img.avatar-xs{ src: notification.target.user.profile_picture.url(:small), loading: :lazy }
+ = render AvatarComponent.new(user: notification.target.user, size: "xs")
- if notification.target.answer.user == current_user
= t(".heading_html",
user: user_screen_name(notification.target.user),
answer: link_to(t(".active.link_text"),
answer_path(username: notification.target.user.screen_name,
id: notification.target.answer.id),
- target: "_top"),
- time: time_tooltip(notification.target))
+ target: "_top"))
- elsif notification.target.user == notification.target.answer.user
= t(".heading_html",
user: user_screen_name(notification.target.user),
answer: link_to(t(".passive.link_text"),
answer_path(username: notification.target.user.screen_name,
id: notification.target.answer.id),
- target: "_top"),
- time: time_tooltip(notification.target))
+ target: "_top"))
- else
= t(".heading_html",
user: user_screen_name(notification.target.user),
@@ -27,8 +25,9 @@
user: user_screen_name(notification.target.answer.user, url: false)),
answer_path(username: notification.target.user.screen_name,
id: notification.target.answer.id),
- target: "_top"),
- time: time_tooltip(notification.target))
+ target: "_top"))
+ ·
+ = time_tooltip(notification.target)
.list-group
.list-group-item
%h6.notification__list-heading= t("activerecord.models.answer.one")
diff --git a/app/views/notifications/type/_follow.html.haml b/app/views/notifications/type/_follow.html.haml
index 0994b6de..d142943f 100644
--- a/app/views/notifications/type/_follow.html.haml
+++ b/app/views/notifications/type/_follow.html.haml
@@ -1,6 +1,6 @@
.d-flex.notification
.flex-shrink-0.notification__icon
- %img.avatar-sm{ src: notification.target.source.profile_picture.url(:small), loading: :lazy }
+ = render AvatarComponent.new(user: notification.target.source, size: "sm")
.flex-grow-1
%h6.notification__user
= user_screen_name notification.target.source
diff --git a/app/views/notifications/type/_reaction.html.haml b/app/views/notifications/type/_reaction.html.haml
index ae9259c1..4238df29 100644
--- a/app/views/notifications/type/_reaction.html.haml
+++ b/app/views/notifications/type/_reaction.html.haml
@@ -3,23 +3,23 @@
%i.fa.fa-2x.fa-fw.fa-smile-o
.flex-grow-1
.notification__heading
- %img.avatar-xs{ src: notification.target.user.profile_picture.url(:small), loading: :lazy }
+ = render AvatarComponent.new(user: notification.target.user, size: "xs")
- if notification.target.parent_type == "Answer"
= t(".heading_html",
user: user_screen_name(notification.target.user),
type: link_to(t(".#{notification.target.parent_type.downcase}.link_text"),
answer_path(username: notification.target.user.screen_name,
id: notification.target.parent.id),
- target: "_top"),
- time: time_tooltip(notification.target))
+ target: "_top"))
- elsif notification.target.parent_type == "Comment"
= t(".heading_html",
user: user_screen_name(notification.target.user),
type: link_to(t(".#{notification.target.parent_type.downcase}.link_text"),
answer_path(username: notification.target.user.screen_name,
id: notification.target.parent.answer.id),
- target: "_top"),
- time: time_tooltip(notification.target))
+ target: "_top"))
+ ·
+ = time_tooltip(notification.target)
.list-group
.list-group-item
%h6.notification__list-heading= t("activerecord.models.#{notification.target.parent_type.downcase}.one")
diff --git a/app/views/question/_question.html.haml b/app/views/question/_question.html.haml
deleted file mode 100644
index 1128bb5f..00000000
--- a/app/views/question/_question.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-.card.question--fixed{ class: hidden ? 'question--hidden' : '', tabindex: hidden ? -1 : '', aria: { hidden: hidden } }
- .container
- .card-body
- .d-flex
- - unless question.author_is_anonymous
- .flex-shrink-0
- %a{ href: unless hidden then user_path(question.user) end }
- %img.answerbox__question-user-avatar.avatar-md{ src: question.user.profile_picture.url(:small) }
- .flex-grow-1
- %h6.text-muted.answerbox__question-user
- - identifier = question.author_is_anonymous ? question.author_identifier : nil
- - if hidden
- = user_screen_name question.user, author_identifier: identifier, url: false
- - else
- = t("answerbox.header.asked_html", user: user_screen_name(question.user, author_identifier: identifier), time: time_tooltip(question))
- .answerbox__question-body{ data: { controller: question.long? ? "collapse" : nil } }
- .answerbox__question-text{ class: question.long? ? "collapsed" : "", data: { collapse_target: "content" } }
- = question_markdown question.content
- - if question.long?
- = render "shared/collapse", type: "question"
- - if user_signed_in?
- .flex-shrink-0.ms-auto
- .btn-group
- %button.btn.btn-link.btn-sm.dropdown-toggle{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
- %span.caret
- - unless hidden
- = render "actions/question", question: question
diff --git a/app/views/question/show.html.haml b/app/views/question/show.html.haml
index e1d0b238..542b7726 100644
--- a/app/views/question/show.html.haml
+++ b/app/views/question/show.html.haml
@@ -1,12 +1,17 @@
- provide(:title, question_title(@question))
-= render "question", question: @question, hidden: false
-= render "question", question: @question, hidden: true
+.card.question--sticky
+ .container
+ .card-body
+ = render QuestionComponent.new(question: @question)
.container.question-page
#answers{ data: { controller: "navigation" } }
%button.d-none{ data: { hotkey: "j", action: "navigation#down" } }
%button.d-none{ data: { hotkey: "k", action: "navigation#up" } }
+ - if @answers.empty?
+ = render "shared/empty", icon: "fa-regular fa-comments", translation_key: ".question"
+
- @answers.each do |a|
- = render "answerbox", a:, show_question: false, subscribed_answer_ids: @subscribed_answer_ids
+ = render "answerbox", a:, show_question: false
- if @more_data_available
.d-flex.justify-content-center.justify-content-sm-start#paginator
diff --git a/app/views/question/show.turbo_stream.haml b/app/views/question/show.turbo_stream.haml
index 96facf13..335a808b 100644
--- a/app/views/question/show.turbo_stream.haml
+++ b/app/views/question/show.turbo_stream.haml
@@ -1,6 +1,6 @@
= turbo_stream.append "answers" do
- @answers.each do |a|
- = render "answerbox", a:, show_question: false, subscribed_answer_ids: @subscribed_answer_ids
+ = render "answerbox", a:, show_question: false
= turbo_stream.update "paginator" do
- if @more_data_available
diff --git a/app/views/reactions/_create.html.haml b/app/views/reactions/_create.html.haml
new file mode 100644
index 00000000..d7c1d7ec
--- /dev/null
+++ b/app/views/reactions/_create.html.haml
@@ -0,0 +1,19 @@
+- if type == "Answer"
+ = button_to create_reactions_path(id: target.id, username: target.user.screen_name),
+ form: { class: "d-inline-block",
+ id: "reaction-#{type}-#{target.id}",
+ data: { controller: :reaction, action: "turbo:submit-start->reaction#disable turbo:submit-end->reaction#enable" } },
+ class: "btn btn-link answerbox__action smile",
+ data: { reaction_target: :button } do
+ %i.fa.fa-smile-o
+ %span= target.smile_count
+
+- if type == "Comment"
+ = button_to create_comment_reactions_path(id: target.id, username: target.user.screen_name),
+ form: { class: "d-inline-block",
+ id: "reaction-#{type}-#{target.id}",
+ data: { controller: :reaction, action: "turbo:submit-start->reaction#disable turbo:submit-end->reaction#enable" } },
+ class: "btn btn-link answerbox__action smile",
+ data: { reaction_target: :button } do
+ %i.fa.fa-smile-o
+ %span= target.smile_count
diff --git a/app/views/reactions/_destroy.html.haml b/app/views/reactions/_destroy.html.haml
new file mode 100644
index 00000000..dff2520a
--- /dev/null
+++ b/app/views/reactions/_destroy.html.haml
@@ -0,0 +1,21 @@
+- if type == "Answer"
+ = button_to destroy_reactions_path(id: target.id, username: target.user.screen_name),
+ method: :delete,
+ form: { class: "d-inline-block",
+ id: "reaction-#{type}-#{target.id}",
+ data: { controller: :reaction, action: "turbo:submit-start->reaction#disable turbo:submit-end->reaction#enable" } },
+ class: "btn btn-link answerbox__action unsmile",
+ data: { reaction_target: :button } do
+ %i.fa.fa-smile-o
+ %span= target.smile_count
+
+- if type == "Comment"
+ = button_to destroy_comment_reactions_path(id: target.id, username: target.user.screen_name),
+ method: :delete,
+ form: { class: "d-inline-block",
+ id: "reaction-#{type}-#{target.id}",
+ data: { controller: :reaction, action: "turbo:submit-start->reaction#disable turbo:submit-end->reaction#enable" } },
+ class: "btn btn-link answerbox__action unsmile",
+ data: { reaction_target: :button } do
+ %i.fa.fa-smile-o
+ %span= target.smile_count
diff --git a/app/views/reactions/index.html.haml b/app/views/reactions/index.html.haml
new file mode 100644
index 00000000..b717a281
--- /dev/null
+++ b/app/views/reactions/index.html.haml
@@ -0,0 +1,2 @@
+= turbo_frame_tag "ab-reactions-list-#{a.id}" do
+ %div{ id: "ab-smiles-#{a.id}" }= render "answerbox/smiles", a:
diff --git a/app/views/relationships/_create.html.haml b/app/views/relationships/_create.html.haml
new file mode 100644
index 00000000..23c91782
--- /dev/null
+++ b/app/views/relationships/_create.html.haml
@@ -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")
diff --git a/app/views/relationships/_destroy.html.haml b/app/views/relationships/_destroy.html.haml
new file mode 100644
index 00000000..f26ec521
--- /dev/null
+++ b/app/views/relationships/_destroy.html.haml
@@ -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")
diff --git a/app/views/settings/blocks/index.html.haml b/app/views/settings/blocks/index.html.haml
index 51e18a11..e11aa255 100644
--- a/app/views/settings/blocks/index.html.haml
+++ b/app/views/settings/blocks/index.html.haml
@@ -6,7 +6,7 @@
- @blocks.each do |block|
%li.list-group-item
.d-flex
- %img.avatar-md.d-none.d-sm-inline.me-2{ src: block.target.profile_picture.url(:small) }
+ = render AvatarComponent.new(user: block.target, size: "md", classes: ["d-none", "d-sm-inline", "me-2"])
%div
%p.mb-0= user_screen_name(block.target)
%p.text-muted.mb-0= t(".blocked", time: time_ago_in_words(block.created_at))
diff --git a/app/views/settings/mutes/_form.html.haml b/app/views/settings/mutes/_form.html.haml
index 33213d32..06c1e610 100644
--- a/app/views/settings/mutes/_form.html.haml
+++ b/app/views/settings/mutes/_form.html.haml
@@ -1,6 +1,5 @@
#form.form-group
%form{ action: settings_muted_path, method: "post" }
.input-group
- %input.form-control#muted_phrase{ name: :muted_phrase, placeholder: t(".placeholder"), data: { controller: :autofocus } }
- .input-group-append
- %button.btn.btn-primary{ type: "submit" }= t("voc.add")
+ %input.form-control#muted-phrase{ name: :muted_phrase, placeholder: t(".placeholder"), required: true, minlength: 1, data: { controller: :autofocus } }
+ %button.btn.btn-primary{ type: "submit" }= t("voc.add")
diff --git a/app/views/settings/mutes/_rule.html.haml b/app/views/settings/mutes/_rule.html.haml
index 0bee6d13..a6322acc 100644
--- a/app/views/settings/mutes/_rule.html.haml
+++ b/app/views/settings/mutes/_rule.html.haml
@@ -1,6 +1,5 @@
.form-group.mb-3{ id: "rule_#{rule.id}" }
.input-group
%input.form-control{ disabled: true, value: rule.muted_phrase }
- .input-group-append
- = button_to settings_muted_destroy_path(rule.id), method: :delete, class: "btn btn-danger" do
- = t("voc.remove")
+ = button_to settings_muted_destroy_path(rule.id), method: :delete, class: "btn btn-danger" do
+ = t("voc.remove")
diff --git a/app/views/settings/mutes/_user.html.haml b/app/views/settings/mutes/_user.html.haml
index e669270a..a2324bcc 100644
--- a/app/views/settings/mutes/_user.html.haml
+++ b/app/views/settings/mutes/_user.html.haml
@@ -1,5 +1,5 @@
.d-flex.mb-2
- %img.avatar-md.me-2{ src: user.profile_picture.url(:small), loading: :lazy }
+ = render AvatarComponent.new(user:, size: "md", classes: ["me-2"])
%p.align-self-center.m-0= user_screen_name(user, context_user: current_user)
.ms-auto.d-inline-flex
%button.btn.btn-default.align-self-center{ data: { action: :unmute, target: user.screen_name } }
diff --git a/app/views/settings/profile/edit.html.haml b/app/views/settings/profile/edit.html.haml
index 32721590..262ddfaf 100644
--- a/app/views/settings/profile/edit.html.haml
+++ b/app/views/settings/profile/edit.html.haml
@@ -5,9 +5,9 @@
%div{ data: { controller: "cropper", cropper_aspect_ratio_value: "1" } }
.d-flex
.flex-shrink-0
- %img.avatar-lg.me-3{ src: current_user.profile_picture.url(:medium) }
+ = render AvatarComponent.new(user: current_user, size: "lg", classes: ["me-3"])
.flex-grow-1
- = f.file_field :profile_picture, accept: APP_CONFIG[:accepted_image_formats].join(","), data: { cropper_target: "input", action: "cropper#change" }
+ = f.file_field :profile_picture, accept: current_user.profile_picture.content_type_whitelist.join(','), data: { cropper_target: "input", action: "cropper#change" }
.row.d-none{ data: { cropper_target: "controls" } }
.col-sm-10.col-md-8
@@ -22,7 +22,7 @@
.col-xs-12.col-md-6
%img.mw-100.me-3{ src: current_user.profile_header.url(:mobile) }
.col-xs-12.col-md-6.mt-3.mt-sm-0.ps-3.pe-3
- = f.file_field :profile_header, accept: APP_CONFIG[:accepted_image_formats].join(","), data: { cropper_target: "input", action: "cropper#change" }
+ = f.file_field :profile_header, accept: current_user.profile_header.content_type_whitelist.join(','), data: { cropper_target: "input", action: "cropper#change" }
.row.d-none{ data: { cropper_target: "controls" } }
.col-sm-10.col-md-8
diff --git a/app/views/settings/theme/_input.html.haml b/app/views/settings/theme/_input.html.haml
new file mode 100644
index 00000000..81a1c6ee
--- /dev/null
+++ b/app/views/settings/theme/_input.html.haml
@@ -0,0 +1,2 @@
+.col-sm-6
+ = f.text_field field_name, class: "color", data: { default:, theme_target: "color", action: "theme#updatePreview" }, readonly: :readonly
diff --git a/app/views/settings/theme/edit.html.haml b/app/views/settings/theme/edit.html.haml
index a5064c23..1d5c84b0 100644
--- a/app/views/settings/theme/edit.html.haml
+++ b/app/views/settings/theme/edit.html.haml
@@ -17,25 +17,19 @@
%p= t(".general.body")
.row
- .col-sm-6
- = f.text_field :background_color, class: "color", data: { default: 0xF0EDF4, theme_target: "color", action: "theme#updatePreview" }
- .col-sm-6
- = f.text_field :body_text, class: "color", data: { default: 0x000000, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :background_color, default: 0xF0EDF4
+ = render "settings/theme/input", f:, field_name: :body_text, default: 0x000000
.card
.card-body
%h2= t(".raised.heading")
%p= t(".raised.body")
.row
- .col-sm-6
- = f.text_field :raised_background, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
- .col-sm-6
- = f.text_field :raised_text, class: "color", data: { default: 0x000000, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :raised_background, default: 0xFFFFFF
+ = render "settings/theme/input", f:, field_name: :raised_text, default: 0x000000
.row
- .col-sm-6
- = f.text_field :raised_accent, class: "color", data: { default: 0xF7F7F7, theme_target: "color", action: "theme#updatePreview" }
- .col-sm-6
- = f.text_field :raised_accent_text, class: "color", data: { default: 0x000000, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :raised_accent, default: 0xF7F7F7
+ = render "settings/theme/input", f:, field_name: :raised_accent_text, default: 0x000000
.card-footer
%p= t(".raised.accent.example")
.card
@@ -44,57 +38,42 @@
%p= t(".colors.body")
.row
- .col-sm-6
- = f.text_field :primary_color, class: "color", data: { default: 0x5E35B1, theme_target: "color", action: "theme#updatePreview" }
- .col-sm-6
- = f.text_field :primary_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :primary_color, default: 0x5E35B1
+ = render "settings/theme/input", f:, field_name: :primary_text, default: 0xFFFFFF
.col-sm-12
.alert.alert-primary= t(".colors.alert.example", type: t(".colors.alert.type.primary"))
.row
- .col-sm-6
- = f.text_field :danger_color, class: "color", data: { default: 0xDC3545, theme_target: "color", action: "theme#updatePreview" }
- .col-sm-6
- = f.text_field :danger_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :danger_color, default: 0xDC3545
+ = render "settings/theme/input", f:, field_name: :danger_text, default: 0xFFFFFF
.col-sm-12
.alert.alert-danger= t(".colors.alert.example", type: t(".colors.alert.type.danger"))
.row
- .col-sm-6
- = f.text_field :warning_color, class: "color", data: { default: 0xFFC107, theme_target: "color", action: "theme#updatePreview" }
- .col-sm-6
- = f.text_field :warning_text, class: "color", data: { default: 0x292929, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :warning_color, default: 0xFFC107
+ = render "settings/theme/input", f:, field_name: :warning_text, default: 0x292929
.col-sm-12
.alert.alert-warning= t(".colors.alert.example", type: t(".colors.alert.type.warning"))
.row
- .col-sm-6
- = f.text_field :info_color, class: "color", data: { default: 0x17A2B8, theme_target: "color", action: "theme#updatePreview" }
- .col-sm-6
- = f.text_field :info_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :info_color, default: 0x17A2B8
+ = render "settings/theme/input", f:, field_name: :info_text, default: 0xFFFFFF
.col-sm-12
.alert.alert-info= t(".colors.alert.example", type: t(".colors.alert.type.info"))
.row
- .col-sm-6
- = f.text_field :success_color, class: "color", data: { default: 0x28A745, theme_target: "color", action: "theme#updatePreview" }
- .col-sm-6
- = f.text_field :success_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :success_color, default: 0x28A745
+ = render "settings/theme/input", f:, field_name: :success_text, default: 0xFFFFFF
.col-sm-12
.alert.alert-success= t(".colors.alert.example", type: t(".colors.alert.type.success"))
.row
- .col-sm-6
- = f.text_field :dark_color, class: "color", data: { default: 0x343A40, theme_target: "color", action: "theme#updatePreview" }
- .col-sm-6
- = f.text_field :dark_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :dark_color, default: 0x343A40
+ = render "settings/theme/input", f:, field_name: :dark_text, default: 0xFFFFFF
.col-sm-12
%a.btn.btn-dark.mb-3{ href: "#" }= t(".colors.button.example", type: t(".colors.button.type.dark"))
.row
- .col-sm-6
- = f.text_field :light_color, class: "color", data: { default: 0xF8F9FA, theme_target: "color", action: "theme#updatePreview" }
- .col-sm-6
- = f.text_field :light_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :light_color, default: 0xF8F9FA
+ = render "settings/theme/input", f:, field_name: :light_text, default: 0xFFFFFF
.col-sm-12
%a.btn.btn-light.mb-3{ href: "#" }= t(".colors.button.example", type: t(".colors.button.type.light"))
.row
- .col-sm-6
- = f.text_field :muted_text, class: "color", data: { default: 0x6C757D, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :muted_text, default: 0x6C757D
.col-sm-6
%p.pt-4.text-muted= t(".colors.text.example")
.card
@@ -103,14 +82,11 @@
%p= t(".forms.body")
.row
- .col-sm-6
- = f.text_field :input_color, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
- .col-sm-6
- = f.text_field :input_text, class: "color", data: { default: 0x000000, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :input_color, default: 0xFFFFFF
+ = render "settings/theme/input", f:, field_name: :input_text, default: 0x000000
.row
- .col-sm-6
- = f.text_field :input_placeholder, class: "color", data: { default: 0x6C757D, theme_target: "color", action: "theme#updatePreview" }
+ = render "settings/theme/input", f:, field_name: :input_placeholder, default: 0x6C757D
.col-sm-6
.form-group
%label.form-label Example Input
diff --git a/app/views/settings/two_factor_authentication/otp_authentication/_totp_setup.html.haml b/app/views/settings/two_factor_authentication/otp_authentication/_totp_setup.html.haml
index 6c707a48..4388bf05 100644
--- a/app/views/settings/two_factor_authentication/otp_authentication/_totp_setup.html.haml
+++ b/app/views/settings/two_factor_authentication/otp_authentication/_totp_setup.html.haml
@@ -36,6 +36,12 @@
%a{ href: "https://apps.apple.com/gb/app/microsoft-authenticator/id983156458" }= t(".source.app_store")
%li.list-inline-item
%a{ href: "https://play.google.com/store/apps/details?id=com.azure.authenticator" }= t(".source.google_play")
+ %li
+ %i.fa.fa-apple
+ = t(".app.ios")
+ %ul.list-inline
+ %li.list-inline-item
+ %a{ href: "https://support.apple.com/en-gb/guide/iphone/ipha6173c19f/ios" }= t(".source.apple_support")
%p= t(".setup_qr", app_name: APP_CONFIG['site_name'])
= f.text_field :otp_validation, class: "totp-setup__code-field", inputmode: :numeric, label: t(".otp_validation"), autofocus: true
= f.primary
diff --git a/app/views/shared/_empty.html.haml b/app/views/shared/_empty.html.haml
new file mode 100644
index 00000000..eaf5a353
--- /dev/null
+++ b/app/views/shared/_empty.html.haml
@@ -0,0 +1,34 @@
+- type ||= nil
+.card{ class: type == "inbox" ? "empty" : nil }
+ .card-body.py-5.text-center
+ - if type == "timeline"
+ %p.mb-3
+ %i.fa-regular.fa-comments.icon--showcase.text-muted
+ %h3= t(".timeline.heading")
+ %p= t(".timeline.text")
+ %p
+ %a.btn.btn-primary{ href: inbox_path }= t(".timeline.actions.inbox")
+ %a.btn.btn-default{ href: public_timeline_path }= t(".timeline.actions.public")
+ - elsif type == "inbox"
+ %p.mb-3
+ %i.fa.fa-inbox.icon--showcase.text-muted
+ %h3= t(".inbox.heading")
+ %p= t(".inbox.text")
+ .d-block.d-sm-flex.justify-content-center
+ = button_to inbox_create_path, class: "btn btn-info me-auto" do
+ = t("inbox.actions.questions.button")
+ .button-group.ms-1
+ %button.btn.btn-default{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
+ %i.fa.fa-fw.fa-share-alt
+ %span= t("inbox.actions.share.heading")
+ .dropdown-menu.dropdown-menu-end{ role: :menu }
+ %a.dropdown-item{ href: "https://twitter.com/intent/tweet?text=Ask%20me%20anything%21&url=#{user_url(current_user)}", target: "_blank" }
+ %i.fa.fa-fw.fa-twitter
+ = t("inbox.actions.share.button", service: "Twitter")
+ %a.dropdown-item{ href: "https://www.tumblr.com/share/link?url=#{user_url(current_user)}&name=Ask%20me%20anything%21", target: "_blank" }
+ %i.fa.fa-fw.fa-tumblr
+ = t("inbox.actions.share.button", service: "Tumblr")
+ - else
+ %p.mb-3
+ %i.icon--showcase.text-muted{ class: icon }
+ %p= t(translation_key)
diff --git a/app/views/shared/_question.html.haml b/app/views/shared/_question.html.haml
index e1a4df80..022cc71a 100644
--- a/app/views/shared/_question.html.haml
+++ b/app/views/shared/_question.html.haml
@@ -1,30 +1,4 @@
- type ||= nil
.card.questionbox{ data: { id: q.id } }
.card-body{ data: { controller: q.long? ? "collapse" : nil } }
- .d-flex
- - if type == "discover"
- .flex-shrink-0
- %a{ href: user_screen_name(q.user, link_only: true) }
- %img.avatar-md.me-2{ src: q.user&.profile_picture&.url(:small), loading: :lazy }
- .flex-grow-1
- %h6.text-muted.answerbox__question-user
- - if type.nil? && q.direct
- - if user_signed_in? && q.user == current_user
- %i.fa.fa-eye-slash{ data: { bs_toggle: "tooltip", bs_title: t(".visible_to_you") } }
- - elsif moderation_view?
- %i.fa.fa-eye-slash{ data: { bs_toggle: "tooltip", bs_title: t(".visible_mod_mode") } }
- = t("answerbox.header.asked_html", user: user_screen_name(q.user), time: time_tooltip(q))
- - if q.answer_count > 1
- ·
- %a{ href: question_path(q.user.screen_name, q.id) }
- = pluralize(q.answer_count, t("voc.answer"))
- .answerbox__question-text{ class: q.long? ? "collapsed" : "", data: { collapse_target: "content" } }
- = question_markdown q.content
- - if q.long?
- = render "shared/collapse", type: "question"
- - if user_signed_in?
- .flex-shrink-0.ms-auto
- .btn-group
- %button.btn.btn-link.btn-sm.dropdown-toggle{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
- %span.caret
- = render "actions/question", question: q
+ = render QuestionComponent.new(question: q, hide_avatar: type == "discover" ? false : true, profile_question: true)
diff --git a/app/views/subscriptions/_create.html.haml b/app/views/subscriptions/_create.html.haml
new file mode 100644
index 00000000..64d289a9
--- /dev/null
+++ b/app/views/subscriptions/_create.html.haml
@@ -0,0 +1,3 @@
+= button_to subscriptions_path(answer: answer.id), class: "dropdown-item", form: { id: "subscription-#{answer.id}" } do
+ %i.fa.fa-fw.fa-bell
+ = t("voc.subscribe")
diff --git a/app/views/subscriptions/_destroy.html.haml b/app/views/subscriptions/_destroy.html.haml
new file mode 100644
index 00000000..31e6d967
--- /dev/null
+++ b/app/views/subscriptions/_destroy.html.haml
@@ -0,0 +1,3 @@
+= button_to subscriptions_path(answer: answer.id), method: :delete, class: "dropdown-item", form: { id: "subscription-#{answer.id}" } do
+ %i.fa.fa-fw.fa-bell-slash
+ = t("voc.unsubscribe")
diff --git a/app/views/tabs/_feed.html.haml b/app/views/tabs/_feed.html.haml
index 778c1cea..20d06717 100644
--- a/app/views/tabs/_feed.html.haml
+++ b/app/views/tabs/_feed.html.haml
@@ -17,8 +17,8 @@
- else
%p.px-4.pb-2
- list.members.each do |member|
- %a{ href: user_path(member.user), title: member.user.screen_name, data: { bs_toggle: :tooltip, bs_placement: :top } }
- %img.avatar-xs{ src: member.user.profile_picture.url(:small), loading: :lazy }
+ %a{ href: user_path(member.user), title: member.user.screen_name, data: { controller: :tooltip, bs_placement: :top } }
+ = render AvatarComponent.new(user: member.user, size: "xs")
- if !list && lists.empty?
.p-3= t(".lists.notice_html")
- lists.each do |list|
diff --git a/app/views/tabs/_moderation.html.haml b/app/views/tabs/_moderation.html.haml
index bc4b481c..b24dfbbe 100644
--- a/app/views/tabs/_moderation.html.haml
+++ b/app/views/tabs/_moderation.html.haml
@@ -1,13 +1,6 @@
.card
.list-group
- = list_group_item t(".all"), moderation_reports_path
- = list_group_item t(".answers"), moderation_reports_path("answer")
- = list_group_item t(".comments"), moderation_reports_path("comment")
- = list_group_item t(".users"), moderation_reports_path("user")
- = list_group_item t(".questions"), moderation_reports_path("question")
-
-.card
- .list-group
+ = list_group_item t(".reports"), moderation_reports_path
= list_group_item t(".site_wide_blocks"), mod_anon_block_index_path
.d-none.d-sm-block= render "shared/links"
diff --git a/app/views/timeline/timeline.html.haml b/app/views/timeline/timeline.html.haml
index 4ed197a8..10754661 100644
--- a/app/views/timeline/timeline.html.haml
+++ b/app/views/timeline/timeline.html.haml
@@ -1,8 +1,11 @@
#timeline{ data: { controller: "navigation" } }
%button.d-none{ data: { hotkey: "j", action: "navigation#down" } }
%button.d-none{ data: { hotkey: "k", action: "navigation#up" } }
+ - if @timeline.empty?
+ = render "shared/empty", type: "timeline"
+
- @timeline.each do |answer|
- = render "answerbox", a: answer, subscribed_answer_ids: @subscribed_answer_ids
+ = render "answerbox", a: answer
- if @more_data_available
.d-flex.justify-content-center#paginator
diff --git a/app/views/timeline/timeline.turbo_stream.haml b/app/views/timeline/timeline.turbo_stream.haml
index 1802f630..561309e6 100644
--- a/app/views/timeline/timeline.turbo_stream.haml
+++ b/app/views/timeline/timeline.turbo_stream.haml
@@ -1,6 +1,6 @@
= turbo_stream.append "timeline" do
- @timeline.each do |answer|
- = render "answerbox", a: answer, subscribed_answer_ids: @subscribed_answer_ids
+ = render "answerbox", a: answer
= turbo_stream.update "paginator" do
- if @more_data_available
diff --git a/app/views/user/_actions.html.haml b/app/views/user/_actions.html.haml
index 23960a38..c30a482c 100644
--- a/app/views/user/_actions.html.haml
+++ b/app/views/user/_actions.html.haml
@@ -9,11 +9,9 @@
- elsif user_signed_in?
.d-grid.gap-2
- 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 } }
- = t("voc.unfollow")
+ = render "relationships/destroy", type: "follow", screen_name: user.screen_name
- else
- %button.btn.btn-primary{ type: :button, name: 'user-action', data: { action: :follow, type: type, target: user.screen_name } }
- = t("voc.follow")
+ = render "relationships/create", type: "follow", screen_name: user.screen_name
.btn-group
%button.btn.btn-light.btn-sm.dropdown-toggle{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
= t(".title")
@@ -23,28 +21,26 @@
%i.fa.fa-list.fa-fw
= t(".list")
- if own_blocks&.include?(user.id) || current_user.blocking?(user)
- %a.dropdown-item{ href: '#', data: { action: :unblock, target: user.screen_name } }
- %i.fa.fa-minus-circle.fa-fw
- %span.pe-none= t("voc.unblock")
+ = render "relationships/destroy", type: "block", screen_name: user.screen_name
- else
- %a.dropdown-item{ href: '#', data: { action: :block, target: user.screen_name } }
- %i.fa.fa-minus-circle.fa-fw
- %span.pe-none= t("voc.block")
+ = render "relationships/create", type: "block", screen_name: user.screen_name
- if own_mutes&.include?(user.id) || current_user.muting?(user)
- %a.dropdown-item{ href: '#', data: { action: :unmute, target: user.screen_name } }
- %i.fa.fa-volume-off.fa-fw
- %span.pe-none= t("voc.unmute")
+ = render "relationships/destroy", type: "mute", screen_name: user.screen_name
- else
- %a.dropdown-item{ href: '#', data: { action: :mute, target: user.screen_name } }
- %i.fa.fa-volume-off.fa-fw
- %span.pe-none= t("voc.mute")
+ = render "relationships/create", type: "mute", screen_name: user.screen_name
%a.dropdown-item{ href: '#', data: { action: 'report-user', target: user.screen_name } }
%i.fa.fa-exclamation-triangle.fa-fw
= t("voc.report")
- if current_user.mod?
+ %a.dropdown-item{ href: moderation_reports_path(user: user.screen_name) }
+ %i.far.fa-flag.fa-fw
+ = t(".reports_from", user: user.screen_name)
+ %a.dropdown-item{ href: moderation_reports_path(target_user: user.screen_name) }
+ %i.fas.fa-flag.fa-fw
+ = t(".reports_of", user: user.screen_name)
%a.dropdown-item{ href: '#', data: { bs_target: '#modal-privileges', bs_toggle: :modal } }
%i.fa.fa-wrench.fa-fw
- = raw t(".privilege", user: user.screen_name)
+ = t(".privilege", user: user.screen_name)
- unless user.has_cached_role?(:administrator)
%a.dropdown-item{ href: '#', data: { bs_target: '#modal-ban', bs_toggle: :modal } }
%i.fa.fa-ban.fa-fw
diff --git a/app/views/user/_profile.html.haml b/app/views/user/_profile.html.haml
index 7be81298..bd40d9f4 100644
--- a/app/views/user/_profile.html.haml
+++ b/app/views/user/_profile.html.haml
@@ -32,9 +32,10 @@
- unless user.profile.website.blank?
.profile__website
%i.fa.fa-fw.fa-globe
- %a{ href: user.profile.website, target: "_blank", rel: "nofollow me" }= user.profile.display_website
+ %a{ href: user.profile.website, target: "_blank", rel: "me nofollow" }= user.profile.display_website
- unless user.profile.location.blank?
.profile__location
%i.fa.fa-fw.fa-location-arrow
= user.profile.location
- = render "user/actions", user: user, type: :follower
+ - unless user.banned? && !current_user&.mod?
+ = render "user/actions", user: user, type: :follower
diff --git a/app/views/user/questions.html.haml b/app/views/user/questions.html.haml
index aa410ffd..9b94eb34 100644
--- a/app/views/user/questions.html.haml
+++ b/app/views/user/questions.html.haml
@@ -1,4 +1,7 @@
#questions
+ - if @questions.empty?
+ = render "shared/empty", icon: "fa-regular fa-comment", translation_key: ".user.questions"
+
- @questions.each do |q|
= render 'shared/question', q: q, type: nil
diff --git a/app/views/user/show.html.haml b/app/views/user/show.html.haml
index e110ca20..baca6552 100644
--- a/app/views/user/show.html.haml
+++ b/app/views/user/show.html.haml
@@ -1,14 +1,16 @@
-- unless @user.banned?
- %div{ data: { controller: "navigation" } }
- %button.d-none{ data: { hotkey: "j", action: "navigation#down" } }
- %button.d-none{ data: { hotkey: "k", action: "navigation#up" } }
- #pinned-answers
- - @pinned_answers.each do |a|
- = render "answerbox", a:, subscribed_answer_ids: @subscribed_answer_ids
+%div{ data: { controller: "navigation" } }
+ %button.d-none{ data: { hotkey: "j", action: "navigation#down" } }
+ %button.d-none{ data: { hotkey: "k", action: "navigation#up" } }
+ #pinned-answers
+ - @pinned_answers.each do |a|
+ = render "answerbox", a:
- #answers
- - @answers.each do |a|
- = render "answerbox", a:, subscribed_answer_ids: @subscribed_answer_ids
+ #answers
+ - if @answers.empty?
+ = render "shared/empty", icon: "fa-regular fa-comments", translation_key: ".user.answers"
+
+ - @answers.each do |a|
+ = render "answerbox", a:
- if @more_data_available
.d-flex.justify-content-center.justify-content-sm-start#paginator
diff --git a/app/views/user/show.turbo_stream.haml b/app/views/user/show.turbo_stream.haml
index d86873da..ae7889ad 100644
--- a/app/views/user/show.turbo_stream.haml
+++ b/app/views/user/show.turbo_stream.haml
@@ -1,6 +1,6 @@
= turbo_stream.append "answers" do
- @answers.each do |a|
- = render "answerbox", a:, subscribed_answer_ids: @subscribed_answer_ids
+ = render "answerbox", a:
= turbo_stream.update "paginator" do
- if @more_data_available
diff --git a/app/views/user/show_follow.html.haml b/app/views/user/show_follow.html.haml
index ca284ab8..1497f1bb 100644
--- a/app/views/user/show_follow.html.haml
+++ b/app/views/user/show_follow.html.haml
@@ -1,3 +1,6 @@
+- if @users.empty?
+ = render "shared/empty", icon: "fa-regular fa-user", translation_key: ".user.#{type}"
+
.row.row-cols-1.row-cols-sm-2.row-cols-md-3#users
- @users.each do |user|
.col.pb-3
diff --git a/app/workers/question_worker.rb b/app/workers/question_worker.rb
index 969b725f..304bd2d3 100644
--- a/app/workers/question_worker.rb
+++ b/app/workers/question_worker.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# @deprecated This is to be replaced by SendToInboxJob. Remaining here so that remaining QuestionWorker jobs can finish.
class QuestionWorker
include Sidekiq::Worker
@@ -15,7 +16,7 @@ class QuestionWorker
user.followers.each do |f|
next if skip_inbox?(f, question, user)
- inbox = Inbox.create(user_id: f.id, question_id:, new: true)
+ inbox = InboxEntry.create(user_id: f.id, question_id:, new: true)
f.push_notification(webpush_app, inbox) if webpush_app
end
rescue StandardError => e
@@ -35,7 +36,5 @@ class QuestionWorker
false
end
- def muted?(user, question)
- MuteRule.where(user:).any? { |rule| rule.applies_to? question }
- end
+ def muted?(user, question) = MuteRule.where(user:).any? { |rule| rule.applies_to? question }
end
diff --git a/app/workers/scheduler/inbox_cleanup_scheduler.rb b/app/workers/scheduler/inbox_cleanup_scheduler.rb
new file mode 100644
index 00000000..6f436ecf
--- /dev/null
+++ b/app/workers/scheduler/inbox_cleanup_scheduler.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Scheduler::InboxCleanupScheduler
+ include Sidekiq::Worker
+
+ sidekiq_options retry: false
+
+ def perform
+ orphaned_entries = InboxEntry.where(question_id: nil).includes(:user)
+ orphaned_entries.each do |inbox|
+ logger.info "Deleting orphaned inbox entry #{inbox.id} from user #{inbox.user.id}"
+ inbox.destroy
+ end
+ end
+end
diff --git a/app/workers/send_to_inbox_job.rb b/app/workers/send_to_inbox_job.rb
new file mode 100644
index 00000000..aa640763
--- /dev/null
+++ b/app/workers/send_to_inbox_job.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class SendToInboxJob
+ include Sidekiq::Job
+
+ sidekiq_options queue: :question, retry: false
+
+ # @param follower_id [Integer] user id passed from Devise
+ # @param question_id [Integer] newly created question id
+ def perform(follower_id, question_id)
+ follower = User.includes(:web_push_subscriptions, :mute_rules, :muted_users).find(follower_id)
+ question = Question.includes(:user).find(question_id)
+ webpush_app = Rpush::App.find_by(name: "webpush")
+
+ return if skip_inbox?(follower, question)
+
+ inbox = InboxEntry.create(user_id: follower.id, question_id:, new: true)
+ follower.push_notification(webpush_app, inbox) if webpush_app
+ end
+
+ private
+
+ def skip_inbox?(follower, question)
+ return true if follower.inbox_locked?
+ return true if follower.banned?
+ return true if muted?(follower, question)
+ return true if follower.muting?(question.user)
+ return true if question.long? && !follower.profile.allow_long_questions
+
+ false
+ end
+
+ # @param [User] user
+ # @param [Question] question
+ def muted?(user, question) = user.mute_rules.any? { |rule| rule.applies_to? question }
+end
diff --git a/config/application.rb b/config/application.rb
index e832eb9e..a92cf4cb 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -27,12 +27,14 @@ module Justask
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
- config.load_defaults 6.0
+ config.load_defaults 7.0
# add `lib/` to the autoload paths so zeitwerk can find e.g. our `UseCase`s
# without an explicit `require`, and also take care of hot reloading the code
# (really useful in development!)
- config.autoload_paths << config.root.join("lib")
+ config.autoload_once_paths << config.root.join("lib")
config.eager_load_paths << config.root.join("lib")
+ # This lowers memory usage from Bootsnap
+ config.add_autoload_paths_to_load_path = false
# Use Sidekiq for background jobs
config.active_job.queue_adapter = :sidekiq
diff --git a/config/environments/development.rb b/config/environments/development.rb
index c2434c8c..609ec5dc 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -72,9 +72,13 @@ Rails.application.configure do
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
+ # annotate rendered view with file names, really useful in dev!
+ config.action_view.annotate_rendered_view_with_filenames = true
+
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
# config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+ config.hosts += ENV["EXTRA_HOSTS"].split(':') if ENV["EXTRA_HOSTS"].present?
end
# For better_errors to work inside Docker we need
diff --git a/config/initializers/cookie_rotator.rb b/config/initializers/cookie_rotator.rb
new file mode 100644
index 00000000..65a33147
--- /dev/null
+++ b/config/initializers/cookie_rotator.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+Rails.application.config.after_initialize do
+ Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
+ salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
+ secret_key_base = Rails.application.secret_key_base
+
+ key_generator = ActiveSupport::KeyGenerator.new(
+ secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1
+ )
+ key_len = ActiveSupport::MessageEncryptor.key_len
+ secret = key_generator.generate_key(salt, key_len)
+
+ cookies.rotate :encrypted, secret
+ end
+end
diff --git a/config/initializers/rails_admin.rb b/config/initializers/rails_admin.rb
index edc5eebb..881a087e 100644
--- a/config/initializers/rails_admin.rb
+++ b/config/initializers/rails_admin.rb
@@ -29,14 +29,13 @@ RailsAdmin.config do |config|
end
config.included_models = %w[
- Appendable
- Appendable::Reaction
+ Reaction
Answer
AnonymousBlock
Comment
List
ListMember
- Inbox
+ InboxEntry
MuteRule
Notification
Profile
@@ -58,10 +57,9 @@ RailsAdmin.config do |config|
{
"AnonymousBlock" => "user-secret",
"Answer" => "exclamation",
- "Appendable" => "paperclip",
- "Appendable::Reaction" => "smile",
+ "Reaction" => "smile",
"Comment" => "comment",
- "Inbox" => "inbox",
+ "InboxEntry" => "inbox",
"List" => "list",
"ListMember" => "users",
"MuteRule" => "volume-mute",
@@ -88,7 +86,7 @@ RailsAdmin.config do |config|
# set up custom parents for certain models to group them nicely together
{
"AnonymousBlock" => User,
- "Inbox" => User,
+ "InboxEntry" => User,
"List" => User,
"MuteRule" => User,
"Notification" => User,
@@ -111,6 +109,7 @@ RailsAdmin.config do |config|
Answer
Appendable
Comment
+ Reaction
Question
User
],
diff --git a/config/justask.yml.example b/config/justask.yml.example
index 97616e4c..22f309a4 100644
--- a/config/justask.yml.example
+++ b/config/justask.yml.example
@@ -81,19 +81,6 @@ hcaptcha:
# TOTP Drift period in seconds
otp_drift_period: 30
-# This list controls the "accept" attribute on file upload fields
-# This ensures mobile users get an appropriate file picker (one for only images)
-# as well as preventing the upload of videos or formats we don't support
-# including making iOS automatically convert HEIC files to JPEG
-accepted_image_formats:
- - image/jpeg
- - .jpg
- - .jpeg
- - image/png
- - .png
- - image/gif
- - .gif
-
# This list controls which hosts are excempt from the linkfilter
# Note: `hostname` is always included by default
allowed_hosts_in_markdown:
diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml
index d7a879ae..30bb48f4 100644
--- a/config/locales/activerecord.en.yml
+++ b/config/locales/activerecord.en.yml
@@ -54,6 +54,8 @@ en:
success_text: "Success text colour"
warning_color: "Warning colour"
warning_text: "Warning text colour"
+ reaction:
+ parent_id: "Target"
user:
created_at: "Account created at"
current_password: "Current password"
@@ -107,6 +109,11 @@ en:
errors:
messages:
invalid_url: "does not look like a valid URL"
+ models:
+ reaction:
+ attributes:
+ parent_id:
+ taken: "already smiled"
helpers:
submit:
user:
diff --git a/config/locales/controllers.en.yml b/config/locales/controllers.en.yml
index b0347cc9..3287e6fe 100644
--- a/config/locales/controllers.en.yml
+++ b/config/locales/controllers.en.yml
@@ -101,46 +101,12 @@ en:
notfound: "Question does not exist."
noauth: "You are not allowed to delete this 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:
create:
noauth: :ajax.noauth
unknown: "You can't report this entity."
notfound: "Could not find %{parameter}"
success: "%{parameter} reported. A moderator will decide what happens with the %{parameter}."
- smile:
- create:
- success: "Successfully smiled answer."
- error: "You have already smiled that answer."
- create_comment:
- success: "Successfully smiled comment."
- error: "You have already smiled that comment."
- destroy:
- success: "Successfully unsmiled answer."
- error: "You have not smiled that answer."
- destroy_comment:
- success: "Successfully unsmiled comment."
- error: "You have not smiled that comment."
web_push:
subscription_count:
zero: "You are not currently subscribed to push notifications on any devices."
@@ -196,10 +162,18 @@ en:
error: :errors.invalid_otp
destroy:
success: "Two factor authentication has been disabled for your account."
+ subscriptions:
+ create:
+ success: "Successfully subscribed."
+ error: "Failed to subscribe to answer."
+ destroy:
+ success: "Successfully unsubscribed."
+ error: "Failed to unsubscribe from answer."
user:
sessions:
create:
banned: "I'm sorry, %{name}, I'm afraid I can't do that."
+ permanent: "You are banned permanently."
reason: "Ban reason: %{reason}"
until: "Banned until: %{time}"
info:
@@ -209,6 +183,42 @@ en:
registrations:
destroy:
export_pending: "You may not delete your account while account data is currently being exported."
+ reactions:
+ create:
+ answer:
+ success: "Successfully smiled answer."
+ error: "You have already smiled that answer."
+ comment:
+ success: "Successfully smiled comment."
+ error: "You have already smiled that comment."
+ destroy:
+ answer:
+ success: "Successfully unsmiled answer."
+ error: "You have not smiled that answer."
+ comment:
+ success: "Successfully unsmiled comment."
+ error: "You have not smiled that comment."
+ 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:
public:
title: "Public Timeline"
diff --git a/config/locales/errors.en.yml b/config/locales/errors.en.yml
index eae9157d..7b260eff 100644
--- a/config/locales/errors.en.yml
+++ b/config/locales/errors.en.yml
@@ -37,3 +37,5 @@ en:
record_not_found: "Record not found"
not_authorized: "You need to be logged in to perform this action"
+
+ question_too_long: "Question is too long"
diff --git a/config/locales/frontend.en.yml b/config/locales/frontend.en.yml
index 96228659..fe55281a 100644
--- a/config/locales/frontend.en.yml
+++ b/config/locales/frontend.en.yml
@@ -6,12 +6,6 @@ en:
error:
title: "Uh-oh…"
message: "An error occurred, a developer should check the console for details"
- subscription:
- subscribe: "Successfully subscribed."
- unsubscribe: "Successfully unsubscribed."
- fail:
- subscribe: "Failed to subscribe to answer."
- unsubscribe: "Failed to unsubscribe from answer."
list:
confirm:
title: "Are you sure?"
@@ -68,3 +62,6 @@ en:
title: "Are you sure you want to report this %{type}?"
text: "A moderator will review your report and decide what happens.\nYou can optionally specify a reason."
input: "Specify a reason…"
+ clipboard_copy:
+ success: "Content copied to clipboard."
+ error: "Failed to copy content to clipboard."
diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml
index 20994bce..f918d3e4 100644
--- a/config/locales/views.en.yml
+++ b/config/locales/views.en.yml
@@ -79,6 +79,8 @@ en:
pin: "Pin to Profile"
unpin: "Unpin from Profile"
share:
+ bluesky: "Share on Bluesky"
+ copy: "Copy to Clipboard"
twitter: "Share on Twitter"
tumblr: "Share on Tumblr"
telegram: "Share on Telegram"
@@ -107,28 +109,22 @@ en:
retries: "Retries"
dead: "Dead"
answerbox:
- header:
- anon_hint: :inbox.entry.anon_hint
- answers:
- zero: "0 answers"
- one: "1 answer"
- other: "%{count} answers"
- asked_html: "%{user} asked %{time} ago"
actions:
share:
title: "Share"
- comments:
- none: "There are no comments yet."
- placeholder: "Comment..."
- action: "Post comment"
smiles:
none: "No one smiled this yet."
+ comments:
+ none: "There are no comments yet."
application:
answerbox:
read: "Read the entire answer"
answered: "%{hide} %{user}" # resolves into "Answered by %{user}"
hide: "Answered by"
pinned: "Pinned"
+ comments:
+ action: "Post comment"
+ placeholder: "Comment…"
questionbox:
title: "Ask something!"
placeholder: "Type your question here…"
@@ -150,6 +146,10 @@ en:
non_anonymous_html: |
This user does not want to receive anonymous questions.
(%{sign_in} or %{sign_up})
+ comment:
+ show_reactions:
+ title: "People who smiled this comment"
+ none: "No one has smiled this comment yet."
devise:
registrations:
edit:
@@ -213,11 +213,6 @@ en:
title: "Feature Requests – Feedback"
inbox:
entry:
- asked_html: "%{user} asked %{time} ago"
- anon_hint: "This question was asked anonymously."
- answers:
- one: "1 answer"
- other: "%{count} answers"
options: "Sharing Options"
placeholder: "Write your answer here…"
sharing:
@@ -257,14 +252,20 @@ en:
confirm: "I understand the risk, proceed!"
modal:
ask:
+ user_note_html: |
+ Do you want to ask %{user} a question?
+ This dialog asks a question to all your followers.
+ Click here
+ to ask them a question directly!
+ follower_note_html: |
+ You don't have any followers!
+ Questions asked through this dialog will not arrive in anyone's inbox.
title: "Ask your followers"
placeholder: "Type your question here…"
action: "Ask"
loading: "Asking…"
long_question_warning: "This question will only be sent to those who allow long questions in their profile settings."
- comment_smiles:
- title: "People who smiled this comment"
- none: "No one has smiled this comment yet."
+ send_to_own_inbox: "Send copy to my inbox"
list:
title: "Manage list memberships"
tab:
@@ -342,10 +343,10 @@ en:
none: "No new notifications."
type:
answer:
- heading_html: "%{user} answered %{question} %{time} ago"
+ heading_html: "%{user} answered %{question}"
link_text: "your question"
comment:
- heading_html: "%{user} commented on %{answer} %{time} ago"
+ heading_html: "%{user} commented on %{answer}"
active:
link_text: "your answer"
passive:
@@ -357,7 +358,7 @@ en:
text_html: "Head over to %{settings_export} to download it."
settings_export: "the settings page"
reaction:
- heading_html: "%{user} smiled %{type} %{time} ago"
+ heading_html: "%{user} smiled %{type}"
answer:
link_text: "your answer"
comment:
@@ -492,7 +493,9 @@ en:
aegis: "Aegis Authenticator for Android"
strongbox: "Strongbox Authenticator for iOS"
microsoft: "Microsoft Authenticator"
+ ios: "iOS (Passwords/Camera)"
source:
+ apple_support: "Apple Support"
app_store: "App Store"
fdroid: "F-Droid"
google_play: "Google Play"
@@ -568,6 +571,25 @@ en:
question:
show: "Show full question"
hide: "Hide full question"
+ empty:
+ user:
+ answers: "This user hasn't answered any questions yet."
+ questions: "This user hasn't asked any questions yet."
+ follower: "This user has no followers."
+ friend: "This user is not following anyone."
+ moderation:
+ reports: "There are no open reports right now!"
+ inbox: "This users inbox is empty."
+ question: "No one answered this question yet."
+ timeline:
+ heading: "Your feed is empty!"
+ text: "Start answering questions or follow some people to fill your feed with answers."
+ actions:
+ inbox: "Go to your inbox"
+ public: "Go to the public timeline"
+ inbox:
+ heading: "Your inbox is empty!"
+ text: "To start answering, generate a question or share your profile on other sites to get questions."
formatting:
body_html: |
%{app_name} uses Markdown for formatting
@@ -582,9 +604,6 @@ en:
anonymous_block:
deleted_question: "Deleted question"
blocked: "blocked %{time} ago"
- question:
- visible_to_you: "Only visible to you as it was asked directly"
- visible_mod_mode: "You can see this because you are in moderation view"
hotkeys:
navigation:
title: "Navigation"
@@ -626,11 +645,7 @@ en:
title: "Members"
none: "No members yet."
moderation:
- all: "All reports"
- answers: :activerecord.models.answer.other
- comments: :activerecord.models.comment.other
- users: :activerecord.models.user.other
- questions: :activerecord.models.question.other
+ reports: "Reports"
site_wide_blocks: "Site-wide anonymous blocks"
notifications:
all: "All notifications"
@@ -676,7 +691,7 @@ en:
questions:
header:
title_html: "Viewing all questions from
%{short}"
- index:
+ show:
title: "Questions from %{author_identifier}"
user:
show_follow:
@@ -689,6 +704,8 @@ en:
ban: "Ban Control"
title: "Actions"
list: "Manage list memberships"
+ reports_from: "Reports from %{user}"
+ reports_of: "Reports of %{user}"
profile:
badge:
admin: "Admin"
diff --git a/config/locales/voc.en.yml b/config/locales/voc.en.yml
index a05407eb..0603cf05 100644
--- a/config/locales/voc.en.yml
+++ b/config/locales/voc.en.yml
@@ -1,6 +1,7 @@
en:
voc:
add: "Add"
+ all: "All"
answer: "Answer"
block: "Block"
block_site_wide: "Block user site-wide"
@@ -9,9 +10,11 @@ en:
confirm: "Are you sure?"
delete: "Delete"
edit: "Edit"
+ filter: "Filter"
follow: "Follow"
format_markdown: "Styling with Markdown is supported"
load: "Load more"
+ loading: "Loading…"
login: "Sign in"
logout: "Sign out"
mute: "Mute"
@@ -39,3 +42,43 @@ en:
days: "days"
weeks: "weeks"
months: "months"
+ datetime:
+ distance_in_words:
+ short:
+ about_x_hours:
+ one: "1h"
+ other: "%{count}h"
+ about_x_months:
+ one: "1mo"
+ other: "%{count}mo"
+ about_x_years:
+ one: "1y"
+ other: "%{count}y"
+ almost_x_years:
+ one: "1y"
+ other: "%{count}y"
+ half_a_minute: 1m
+ less_than_x_seconds:
+ one: "1s"
+ other: "%{count}s"
+ less_than_x_minutes:
+ one: "1m"
+ other: "%{count}m"
+ over_x_years:
+ one: "1y"
+ other: "%{count}y"
+ x_seconds:
+ one: "1s"
+ other: "%{count}s"
+ x_minutes:
+ one: "1m"
+ other: "%{count}m"
+ x_days:
+ one: "1d"
+ other: "%{count}d"
+ x_months:
+ one: "1mo"
+ other: "%{count}mo"
+ x_years:
+ one: "1y"
+ other: "%{count}y"
diff --git a/config/routes.rb b/config/routes.rb
index 3af4b3a8..52d1f53b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -26,7 +26,7 @@ Rails.application.routes.draw do
post "/moderation/unmask", to: "moderation#toggle_unmask", as: :moderation_toggle_unmask
get "/moderation/blocks", to: "moderation/anonymous_block#index", as: :mod_anon_block_index
get "/moderation/inbox/:user", to: "moderation/inbox#index", as: :mod_inbox_index
- get "/moderation/reports(/:type)", to: "moderation/reports#index", as: :moderation_reports, defaults: { type: "all" }
+ get "/moderation/reports(/:type)", to: "moderation/reports#index", as: :moderation_reports
get "/moderation/questions/:author_identifier", to: "moderation/questions#show", as: :moderation_questions
namespace :ajax do
post "/mod/destroy_report", to: "moderation#destroy_report", as: :mod_destroy_report
@@ -118,18 +118,12 @@ Rails.application.routes.draw do
post "/destroy_answer", to: "answer#destroy", as: :destroy_answer
post "/create_relationship", to: "relationship#create", as: :create_relationship
post "/destroy_relationship", to: "relationship#destroy", as: :destroy_relationship
- post "/create_smile", to: "smile#create", as: :create_smile
- post "/destroy_smile", to: "smile#destroy", as: :destroy_smile
- post "/create_comment_smile", to: "smile#create_comment", as: :create_comment_smile
- post "/destroy_comment_smile", to: "smile#destroy_comment", as: :destroy_comment_smile
post "/create_comment", to: "comment#create", as: :create_comment
post "/destroy_comment", to: "comment#destroy", as: :destroy_comment
post "/report", to: "report#create", as: :report
post "/create_list", to: "list#create", as: :create_list
post "/destroy_list", to: "list#destroy", as: :destroy_list
post "/list_membership", to: "list#membership", as: :list_membership
- post "/subscribe", to: "subscription#subscribe", as: :subscribe_answer
- post "/unsubscribe", to: "subscription#unsubscribe", as: :unsubscribe_answer
get "/webpush/key", to: "web_push#key", as: :webpush_key
post "/webpush/check", to: "web_push#check", as: :webpush_check
post "/webpush", to: "web_push#subscribe", as: :webpush_subscribe
@@ -148,12 +142,22 @@ Rails.application.routes.draw do
post "/inbox/create", to: "inbox#create", as: :inbox_create
get "/inbox", to: "inbox#show", as: :inbox
+ resource :subscriptions, controller: :subscriptions, only: %i[create destroy]
+ resource :relationships, only: %i[create destroy]
+
get "/user/:username", to: "user#show"
get "/@:username", to: "user#show", as: :user
get "/@:username/a/:id", to: "answer#show", as: :answer
post "/@:username/a/:id/pin", to: "answer#pin", as: :pin_answer
delete "/@:username/a/:id/pin", to: "answer#unpin", as: :unpin_answer
+ get "/@:username/a/:id/comments", to: "comments#index", as: :comments
+ get "/@:username/a/:id/reactions", to: "reactions#index", as: :reactions
+ post "/@:username/a/:id/reactions", to: "reactions#create", as: :create_reactions, defaults: { type: "Answer" }
+ delete "/@:username/a/:id/reactions", to: "reactions#destroy", as: :destroy_reactions, defaults: { type: "Answer" }
get "/@:username/q/:id", to: "question#show", as: :question
+ get "/@:username/c/:id/reactions", to: "comments/reactions#index", as: :comment_reactions
+ post "/@:username/c/:id/reactions", to: "reactions#create", as: :create_comment_reactions, defaults: { type: "Comment" }
+ delete "/@:username/c/:id/reactions", to: "reactions#destroy", as: :destroy_comment_reactions, defaults: { type: "Comment" }
get "/@:username/followers", to: "user#followers", as: :show_user_followers
get "/@:username/followings", to: "user#followings", as: :show_user_followings
get "/@:username/friends", to: redirect("/@%{username}/followings")
@@ -178,5 +182,7 @@ Rails.application.routes.draw do
get "/nodeinfo/2.1", to: "well_known/node_info#nodeinfo", as: :node_info
+ get "/modal/close", to: "modal#close", as: :modal_close
+
puts "processing time of routes.rb: #{"#{(Time.zone.now - start).round(3).to_s.ljust(5, '0')}s".light_green}"
end
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index b9c163be..b224130a 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -13,4 +13,11 @@ production:
- question
- export
- push_notification
+ - scheduler
+:scheduler:
+ :schedule:
+ inbox_cleanup:
+ every: 1m
+ class: Scheduler::InboxCleanupScheduler
+ queue: scheduler
diff --git a/db/migrate/20140801095807_devise_create_users.rb b/db/migrate/20140801095807_devise_create_users.rb
index 0115292e..85ab1dfb 100644
--- a/db/migrate/20140801095807_devise_create_users.rb
+++ b/db/migrate/20140801095807_devise_create_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DeviseCreateUsers < ActiveRecord::Migration[4.2]
def change
create_table(:users) do |t|
@@ -30,7 +32,6 @@ class DeviseCreateUsers < ActiveRecord::Migration[4.2]
# t.string :unlock_token # Only if unlock strategy is :email or :both
# t.datetime :locked_at
-
t.timestamps
end
diff --git a/db/migrate/20140801103309_add_screen_name_to_users.rb b/db/migrate/20140801103309_add_screen_name_to_users.rb
index ad028c8f..a54abe42 100644
--- a/db/migrate/20140801103309_add_screen_name_to_users.rb
+++ b/db/migrate/20140801103309_add_screen_name_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddScreenNameToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :screen_name, :string
diff --git a/db/migrate/20140801174930_create_questions.rb b/db/migrate/20140801174930_create_questions.rb
index ae872a7e..962748db 100644
--- a/db/migrate/20140801174930_create_questions.rb
+++ b/db/migrate/20140801174930_create_questions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateQuestions < ActiveRecord::Migration[4.2]
def change
create_table :questions do |t|
@@ -9,6 +11,6 @@ class CreateQuestions < ActiveRecord::Migration[4.2]
t.timestamps
end
- add_index :questions, [:user_id, :created_at]
+ add_index :questions, %i[user_id created_at]
end
end
diff --git a/db/migrate/20140801175112_create_answers.rb b/db/migrate/20140801175112_create_answers.rb
index 510d73bc..21eff7a1 100644
--- a/db/migrate/20140801175112_create_answers.rb
+++ b/db/migrate/20140801175112_create_answers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateAnswers < ActiveRecord::Migration[4.2]
def change
create_table :answers do |t|
@@ -9,6 +11,6 @@ class CreateAnswers < ActiveRecord::Migration[4.2]
t.timestamps
end
- add_index :answers, [:user_id, :created_at]
+ add_index :answers, %i[user_id created_at]
end
end
diff --git a/db/migrate/20140801175137_create_comments.rb b/db/migrate/20140801175137_create_comments.rb
index cb785fff..9d90f710 100644
--- a/db/migrate/20140801175137_create_comments.rb
+++ b/db/migrate/20140801175137_create_comments.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateComments < ActiveRecord::Migration[4.2]
def change
create_table :comments do |t|
@@ -7,6 +9,6 @@ class CreateComments < ActiveRecord::Migration[4.2]
t.timestamps
end
- add_index :comments, [:user_id, :created_at]
+ add_index :comments, %i[user_id created_at]
end
end
diff --git a/db/migrate/20141102153520_add_counts_to_users.rb b/db/migrate/20141102153520_add_counts_to_users.rb
index 37db7152..92f934d3 100644
--- a/db/migrate/20141102153520_add_counts_to_users.rb
+++ b/db/migrate/20141102153520_add_counts_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddCountsToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :friend_count, :integer, default: 0, null: false
diff --git a/db/migrate/20141104143648_add_display_name_to_users.rb b/db/migrate/20141104143648_add_display_name_to_users.rb
index 6db1c1c1..0ce6171c 100644
--- a/db/migrate/20141104143648_add_display_name_to_users.rb
+++ b/db/migrate/20141104143648_add_display_name_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddDisplayNameToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :display_name, :string
diff --git a/db/migrate/20141110210154_create_inboxes.rb b/db/migrate/20141110210154_create_inboxes.rb
index bfb140d0..cbed4da6 100644
--- a/db/migrate/20141110210154_create_inboxes.rb
+++ b/db/migrate/20141110210154_create_inboxes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateInboxes < ActiveRecord::Migration[4.2]
def change
create_table :inboxes do |t|
diff --git a/db/migrate/20141113164048_add_smile_count_to_answers.rb b/db/migrate/20141113164048_add_smile_count_to_answers.rb
index c40b7166..16346497 100644
--- a/db/migrate/20141113164048_add_smile_count_to_answers.rb
+++ b/db/migrate/20141113164048_add_smile_count_to_answers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddSmileCountToAnswers < ActiveRecord::Migration[4.2]
def change
add_column :answers, :smile_count, :integer, default: 0, null: false
diff --git a/db/migrate/20141113164314_add_smiled_count_to_users.rb b/db/migrate/20141113164314_add_smiled_count_to_users.rb
index dda9e08b..b88e3e4d 100644
--- a/db/migrate/20141113164314_add_smiled_count_to_users.rb
+++ b/db/migrate/20141113164314_add_smiled_count_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddSmiledCountToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :smiled_count, :integer, default: 0, null: false
diff --git a/db/migrate/20141126154451_add_admin_to_users.rb b/db/migrate/20141126154451_add_admin_to_users.rb
index d225ea61..3a745490 100644
--- a/db/migrate/20141126154451_add_admin_to_users.rb
+++ b/db/migrate/20141126154451_add_admin_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddAdminToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :admin, :boolean, default: false, null: false
diff --git a/db/migrate/20141129211448_add_motivation_header_to_users.rb b/db/migrate/20141129211448_add_motivation_header_to_users.rb
index 7cd1d6dd..b87c1382 100644
--- a/db/migrate/20141129211448_add_motivation_header_to_users.rb
+++ b/db/migrate/20141129211448_add_motivation_header_to_users.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class AddMotivationHeaderToUsers < ActiveRecord::Migration[4.2]
def change
- add_column :users, :motivation_header, :string, default: '', null: false
+ add_column :users, :motivation_header, :string, default: "", null: false
end
end
diff --git a/db/migrate/20141130130221_create_relationships.rb b/db/migrate/20141130130221_create_relationships.rb
index 98ab31e8..6b28185d 100644
--- a/db/migrate/20141130130221_create_relationships.rb
+++ b/db/migrate/20141130130221_create_relationships.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateRelationships < ActiveRecord::Migration[4.2]
def change
create_table :relationships do |t|
@@ -9,6 +11,6 @@ class CreateRelationships < ActiveRecord::Migration[4.2]
add_index :relationships, :source_id
add_index :relationships, :target_id
- add_index :relationships, [:source_id, :target_id], unique: true
+ add_index :relationships, %i[source_id target_id], unique: true
end
end
diff --git a/db/migrate/20141130175749_create_smiles.rb b/db/migrate/20141130175749_create_smiles.rb
index de5004b4..3650c00d 100644
--- a/db/migrate/20141130175749_create_smiles.rb
+++ b/db/migrate/20141130175749_create_smiles.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateSmiles < ActiveRecord::Migration[4.2]
def change
create_table :smiles do |t|
@@ -9,6 +11,6 @@ class CreateSmiles < ActiveRecord::Migration[4.2]
add_index :smiles, :user_id
add_index :smiles, :answer_id
- add_index :smiles, [:user_id, :answer_id], unique: true
+ add_index :smiles, %i[user_id answer_id], unique: true
end
end
diff --git a/db/migrate/20141130180152_rename_columns_in_answers.rb b/db/migrate/20141130180152_rename_columns_in_answers.rb
index 0a3fac1f..a94551c5 100644
--- a/db/migrate/20141130180152_rename_columns_in_answers.rb
+++ b/db/migrate/20141130180152_rename_columns_in_answers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RenameColumnsInAnswers < ActiveRecord::Migration[4.2]
def change
rename_column :answers, :comments, :comment_count
diff --git a/db/migrate/20141201191324_add_fields_to_users.rb b/db/migrate/20141201191324_add_fields_to_users.rb
index 6ba0d729..38a18cf6 100644
--- a/db/migrate/20141201191324_add_fields_to_users.rb
+++ b/db/migrate/20141201191324_add_fields_to_users.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class AddFieldsToUsers < ActiveRecord::Migration[4.2]
def change
- add_column :users, :website, :string, default: '', null: false
- add_column :users, :location, :string, default: '', null: false
- add_column :users, :bio, :text, default: '', null: false
+ add_column :users, :website, :string, default: "", null: false
+ add_column :users, :location, :string, default: "", null: false
+ add_column :users, :bio, :text, default: "", null: false
end
end
diff --git a/db/migrate/20141207194424_add_answer_count_to_questions.rb b/db/migrate/20141207194424_add_answer_count_to_questions.rb
index ff3acb3e..1ad3ee56 100644
--- a/db/migrate/20141207194424_add_answer_count_to_questions.rb
+++ b/db/migrate/20141207194424_add_answer_count_to_questions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddAnswerCountToQuestions < ActiveRecord::Migration[4.2]
def change
add_column :questions, :answer_count, :integer, default: 0, null: false
diff --git a/db/migrate/20141208111714_change_answer_content_column_type.rb b/db/migrate/20141208111714_change_answer_content_column_type.rb
index 454b897c..f704a5d5 100644
--- a/db/migrate/20141208111714_change_answer_content_column_type.rb
+++ b/db/migrate/20141208111714_change_answer_content_column_type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ChangeAnswerContentColumnType < ActiveRecord::Migration[4.2]
def change
change_table :answers do |t|
diff --git a/db/migrate/20141212193625_create_services.rb b/db/migrate/20141212193625_create_services.rb
index 7426d605..471a51d0 100644
--- a/db/migrate/20141212193625_create_services.rb
+++ b/db/migrate/20141212193625_create_services.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateServices < ActiveRecord::Migration[4.2]
def change
create_table :services do |t|
diff --git a/db/migrate/20141213182609_create_notifications.rb b/db/migrate/20141213182609_create_notifications.rb
index a3fa3bcc..c8652cb9 100644
--- a/db/migrate/20141213182609_create_notifications.rb
+++ b/db/migrate/20141213182609_create_notifications.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateNotifications < ActiveRecord::Migration[4.2]
def change
create_table :notifications do |t|
diff --git a/db/migrate/20141226115905_add_moderator_to_users.rb b/db/migrate/20141226115905_add_moderator_to_users.rb
index 1ef1db05..db9b3d8e 100644
--- a/db/migrate/20141226115905_add_moderator_to_users.rb
+++ b/db/migrate/20141226115905_add_moderator_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddModeratorToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :moderator, :boolean, default: false, null: false
diff --git a/db/migrate/20141227130438_create_reports.rb b/db/migrate/20141227130438_create_reports.rb
index ce3c795c..932bf9b4 100644
--- a/db/migrate/20141227130438_create_reports.rb
+++ b/db/migrate/20141227130438_create_reports.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateReports < ActiveRecord::Migration[4.2]
def change
create_table :reports do |t|
diff --git a/db/migrate/20141227130545_create_moderation_votes.rb b/db/migrate/20141227130545_create_moderation_votes.rb
index 4e73c91a..14a3543e 100644
--- a/db/migrate/20141227130545_create_moderation_votes.rb
+++ b/db/migrate/20141227130545_create_moderation_votes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateModerationVotes < ActiveRecord::Migration[4.2]
def change
create_table :moderation_votes do |t|
@@ -10,6 +12,6 @@ class CreateModerationVotes < ActiveRecord::Migration[4.2]
add_index :moderation_votes, :user_id
add_index :moderation_votes, :report_id
- add_index :moderation_votes, [:user_id, :report_id], unique: true
+ add_index :moderation_votes, %i[user_id report_id], unique: true
end
end
diff --git a/db/migrate/20141227130618_create_moderation_comments.rb b/db/migrate/20141227130618_create_moderation_comments.rb
index c06d8410..8c1025a7 100644
--- a/db/migrate/20141227130618_create_moderation_comments.rb
+++ b/db/migrate/20141227130618_create_moderation_comments.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateModerationComments < ActiveRecord::Migration[4.2]
def change
create_table :moderation_comments do |t|
@@ -8,6 +10,6 @@ class CreateModerationComments < ActiveRecord::Migration[4.2]
t.timestamps
end
- add_index :moderation_comments, [:user_id, :created_at]
+ add_index :moderation_comments, %i[user_id created_at]
end
end
diff --git a/db/migrate/20141228202825_add_deleted_to_reports.rb b/db/migrate/20141228202825_add_deleted_to_reports.rb
index c8c6d343..d849baa8 100644
--- a/db/migrate/20141228202825_add_deleted_to_reports.rb
+++ b/db/migrate/20141228202825_add_deleted_to_reports.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddDeletedToReports < ActiveRecord::Migration[4.2]
def change
add_column :reports, :deleted, :boolean, default: false
diff --git a/db/migrate/20141229085904_add_attachment_profile_picture_to_users.rb b/db/migrate/20141229085904_add_attachment_profile_picture_to_users.rb
index ff05535d..24d3accf 100644
--- a/db/migrate/20141229085904_add_attachment_profile_picture_to_users.rb
+++ b/db/migrate/20141229085904_add_attachment_profile_picture_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddAttachmentProfilePictureToUsers < ActiveRecord::Migration[4.2]
def self.up
change_table :users do |t|
diff --git a/db/migrate/20141229105013_add_profile_picture_processing_to_users.rb b/db/migrate/20141229105013_add_profile_picture_processing_to_users.rb
index 0832cc17..47d882e8 100644
--- a/db/migrate/20141229105013_add_profile_picture_processing_to_users.rb
+++ b/db/migrate/20141229105013_add_profile_picture_processing_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddProfilePictureProcessingToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :profile_picture_processing, :boolean
diff --git a/db/migrate/20141229133149_add_crop_values_to_users.rb b/db/migrate/20141229133149_add_crop_values_to_users.rb
index 8da16793..a41eab5d 100644
--- a/db/migrate/20141229133149_add_crop_values_to_users.rb
+++ b/db/migrate/20141229133149_add_crop_values_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddCropValuesToUsers < ActiveRecord::Migration[4.2]
def change
# this is a ugly hack and will stay until I find a way to pass parameters
diff --git a/db/migrate/20150102231343_add_supporter_to_users.rb b/db/migrate/20150102231343_add_supporter_to_users.rb
index e23d5eb2..1cc574a9 100644
--- a/db/migrate/20150102231343_add_supporter_to_users.rb
+++ b/db/migrate/20150102231343_add_supporter_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddSupporterToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :supporter, :boolean, default: false
diff --git a/db/migrate/20150103200732_add_privacy_options_to_users.rb b/db/migrate/20150103200732_add_privacy_options_to_users.rb
index 2d7ec342..cfe87cda 100644
--- a/db/migrate/20150103200732_add_privacy_options_to_users.rb
+++ b/db/migrate/20150103200732_add_privacy_options_to_users.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
class AddPrivacyOptionsToUsers < ActiveRecord::Migration[4.2]
def change
- %i{
+ %i[
privacy_allow_anonymous_questions
privacy_allow_public_timeline
privacy_allow_stranger_answers
privacy_show_in_search
- }.each do |sym|
+ ].each do |sym|
add_column :users, sym, :boolean, default: true
end
end
diff --git a/db/migrate/20150112210754_add_banned_to_users.rb b/db/migrate/20150112210754_add_banned_to_users.rb
index 7a857608..5c256971 100644
--- a/db/migrate/20150112210754_add_banned_to_users.rb
+++ b/db/migrate/20150112210754_add_banned_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddBannedToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :banned, :boolean, default: false
diff --git a/db/migrate/20150112210755_create_groups.rb b/db/migrate/20150112210755_create_groups.rb
index 9c44ef5c..ba7436e0 100644
--- a/db/migrate/20150112210755_create_groups.rb
+++ b/db/migrate/20150112210755_create_groups.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateGroups < ActiveRecord::Migration[4.2]
def change
create_table :groups do |t|
@@ -11,7 +13,7 @@ class CreateGroups < ActiveRecord::Migration[4.2]
add_index :groups, :user_id
add_index :groups, :name
- add_index :groups, [:user_id, :name], unique: true
+ add_index :groups, %i[user_id name], unique: true
create_table :group_members do |t|
t.integer :group_id, null: false
@@ -22,6 +24,6 @@ class CreateGroups < ActiveRecord::Migration[4.2]
add_index :group_members, :group_id
add_index :group_members, :user_id
- add_index :group_members, [:group_id, :user_id], unique: true
+ add_index :group_members, %i[group_id user_id], unique: true
end
end
diff --git a/db/migrate/20150125191224_add_blogger_to_users.rb b/db/migrate/20150125191224_add_blogger_to_users.rb
index 05edee57..f2a89b12 100644
--- a/db/migrate/20150125191224_add_blogger_to_users.rb
+++ b/db/migrate/20150125191224_add_blogger_to_users.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class AddBloggerToUsers < ActiveRecord::Migration[4.2]
def change
- add_column :users, :blogger, :boolean, default: :false
+ add_column :users, :blogger, :boolean, default: false
end
end
diff --git a/db/migrate/20150419201122_add_contributor_to_users.rb b/db/migrate/20150419201122_add_contributor_to_users.rb
index 91a548aa..72f5488c 100644
--- a/db/migrate/20150419201122_add_contributor_to_users.rb
+++ b/db/migrate/20150419201122_add_contributor_to_users.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class AddContributorToUsers < ActiveRecord::Migration[4.2]
def change
- add_column :users, :contributor, :boolean, default: :false
+ add_column :users, :contributor, :boolean, default: false
end
end
diff --git a/db/migrate/20150420232305_create_subscriptions.rb b/db/migrate/20150420232305_create_subscriptions.rb
index 72061a84..26180ede 100644
--- a/db/migrate/20150420232305_create_subscriptions.rb
+++ b/db/migrate/20150420232305_create_subscriptions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateSubscriptions < ActiveRecord::Migration[4.2]
def change
create_table :subscriptions do |t|
diff --git a/db/migrate/20150421120557_add_is_active_to_subscriptions.rb b/db/migrate/20150421120557_add_is_active_to_subscriptions.rb
index 059d4a7b..50372127 100644
--- a/db/migrate/20150421120557_add_is_active_to_subscriptions.rb
+++ b/db/migrate/20150421120557_add_is_active_to_subscriptions.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class AddIsActiveToSubscriptions < ActiveRecord::Migration[4.2]
def change
- add_column :subscriptions, :is_active, :boolean, default: :true
+ add_column :subscriptions, :is_active, :boolean, default: true
end
end
diff --git a/db/migrate/20150422024104_add_reason_to_report.rb b/db/migrate/20150422024104_add_reason_to_report.rb
index 51531b65..e3bef6df 100644
--- a/db/migrate/20150422024104_add_reason_to_report.rb
+++ b/db/migrate/20150422024104_add_reason_to_report.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddReasonToReport < ActiveRecord::Migration[4.2]
def change
add_column :reports, :reason, :string, default: nil
diff --git a/db/migrate/20150422224203_rename_banned_to_permanently_banned_in_users.rb b/db/migrate/20150422224203_rename_banned_to_permanently_banned_in_users.rb
index 81954d3f..3e74cb2d 100644
--- a/db/migrate/20150422224203_rename_banned_to_permanently_banned_in_users.rb
+++ b/db/migrate/20150422224203_rename_banned_to_permanently_banned_in_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RenameBannedToPermanentlyBannedInUsers < ActiveRecord::Migration[4.2]
def up
rename_column :users, :banned, :permanently_banned
diff --git a/db/migrate/20150422224225_add_ban_reason_and_banned_until_to_users.rb b/db/migrate/20150422224225_add_ban_reason_and_banned_until_to_users.rb
index 55936ff8..d312f8ca 100644
--- a/db/migrate/20150422224225_add_ban_reason_and_banned_until_to_users.rb
+++ b/db/migrate/20150422224225_add_ban_reason_and_banned_until_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddBanReasonAndBannedUntilToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :ban_reason, :string, default: nil
diff --git a/db/migrate/20150504004931_create_comment_smiles.rb b/db/migrate/20150504004931_create_comment_smiles.rb
index ea86bca2..064cc1f0 100644
--- a/db/migrate/20150504004931_create_comment_smiles.rb
+++ b/db/migrate/20150504004931_create_comment_smiles.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateCommentSmiles < ActiveRecord::Migration[4.2]
def change
create_table :comment_smiles do |t|
@@ -9,7 +11,7 @@ class CreateCommentSmiles < ActiveRecord::Migration[4.2]
add_index :comment_smiles, :user_id
add_index :comment_smiles, :comment_id
- add_index :comment_smiles, [:user_id, :comment_id], unique: true
+ add_index :comment_smiles, %i[user_id comment_id], unique: true
add_column :users, :comment_smiled_count, :integer, default: 0, null: false
add_column :comments, :smile_count, :integer, default: 0, null: false
diff --git a/db/migrate/20150508144336_add_attachment_profile_header_to_users.rb b/db/migrate/20150508144336_add_attachment_profile_header_to_users.rb
index fabf823d..2f704186 100644
--- a/db/migrate/20150508144336_add_attachment_profile_header_to_users.rb
+++ b/db/migrate/20150508144336_add_attachment_profile_header_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddAttachmentProfileHeaderToUsers < ActiveRecord::Migration[4.2]
def change
change_table :users do |t|
diff --git a/db/migrate/20150526031159_add_locale_to_user.rb b/db/migrate/20150526031159_add_locale_to_user.rb
index 76c828c5..090c66a7 100644
--- a/db/migrate/20150526031159_add_locale_to_user.rb
+++ b/db/migrate/20150526031159_add_locale_to_user.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddLocaleToUser < ActiveRecord::Migration[4.2]
def change
add_column :users, :locale, :string
diff --git a/db/migrate/20150619123121_add_translator_to_users.rb b/db/migrate/20150619123121_add_translator_to_users.rb
index 8f8da49b..c340278a 100644
--- a/db/migrate/20150619123121_add_translator_to_users.rb
+++ b/db/migrate/20150619123121_add_translator_to_users.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class AddTranslatorToUsers < ActiveRecord::Migration[4.2]
def change
- add_column :users, :translator, :boolean, default: :false
+ add_column :users, :translator, :boolean, default: false
end
end
diff --git a/db/migrate/20150704072402_change_default_value_of_locale.rb b/db/migrate/20150704072402_change_default_value_of_locale.rb
index ed8f0fe8..aa45d331 100644
--- a/db/migrate/20150704072402_change_default_value_of_locale.rb
+++ b/db/migrate/20150704072402_change_default_value_of_locale.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class ChangeDefaultValueOfLocale < ActiveRecord::Migration[4.2]
def change
- change_column :users, :locale, :string, :default => 'en'
+ change_column :users, :locale, :string, default: "en"
end
end
diff --git a/db/migrate/20150721154255_add_confirmable_to_devise.rb b/db/migrate/20150721154255_add_confirmable_to_devise.rb
index fb435503..8167abab 100644
--- a/db/migrate/20150721154255_add_confirmable_to_devise.rb
+++ b/db/migrate/20150721154255_add_confirmable_to_devise.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddConfirmableToDevise < ActiveRecord::Migration[4.2]
def up
add_column :users, :confirmation_token, :string
diff --git a/db/migrate/20150825073030_create_themes.rb b/db/migrate/20150825073030_create_themes.rb
index ac8da0e8..b556f09e 100644
--- a/db/migrate/20150825073030_create_themes.rb
+++ b/db/migrate/20150825073030_create_themes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateThemes < ActiveRecord::Migration[4.2]
def change
create_table :themes do |t|
@@ -38,6 +40,6 @@ class CreateThemes < ActiveRecord::Migration[4.2]
t.timestamps null: false
end
- add_index :themes, [:user_id, :created_at]
+ add_index :themes, %i[user_id created_at]
end
end
diff --git a/db/migrate/20150825180139_add_show_foreign_themes_to_users.rb b/db/migrate/20150825180139_add_show_foreign_themes_to_users.rb
index 9d00452e..96164b0c 100644
--- a/db/migrate/20150825180139_add_show_foreign_themes_to_users.rb
+++ b/db/migrate/20150825180139_add_show_foreign_themes_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddShowForeignThemesToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :show_foreign_themes, :boolean, default: true, null: false
diff --git a/db/migrate/20150826224857_add_input_and_outline_to_theme.rb b/db/migrate/20150826224857_add_input_and_outline_to_theme.rb
index e4d24e3c..69d3516c 100644
--- a/db/migrate/20150826224857_add_input_and_outline_to_theme.rb
+++ b/db/migrate/20150826224857_add_input_and_outline_to_theme.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddInputAndOutlineToTheme < ActiveRecord::Migration[4.2]
def change
add_column :themes, :input_color, :integer, default: 0xFFFFFF, null: false
diff --git a/db/migrate/20160105165913_add_export_fields_to_users.rb b/db/migrate/20160105165913_add_export_fields_to_users.rb
index fe4a4fe8..7ffbbbcb 100644
--- a/db/migrate/20160105165913_add_export_fields_to_users.rb
+++ b/db/migrate/20160105165913_add_export_fields_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddExportFieldsToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :export_url, :string
diff --git a/db/migrate/20200419183714_create_announcements.rb b/db/migrate/20200419183714_create_announcements.rb
index 1aedd713..8fe9990c 100644
--- a/db/migrate/20200419183714_create_announcements.rb
+++ b/db/migrate/20200419183714_create_announcements.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateAnnouncements < ActiveRecord::Migration[5.2]
def change
create_table :announcements do |t|
diff --git a/db/migrate/20200419185535_create_initial_roles.rb b/db/migrate/20200419185535_create_initial_roles.rb
index f3f0788c..0b13b04b 100644
--- a/db/migrate/20200419185535_create_initial_roles.rb
+++ b/db/migrate/20200419185535_create_initial_roles.rb
@@ -7,11 +7,11 @@ class CreateInitialRoles < ActiveRecord::Migration[5.2]
end
{
- admin: :administrator,
- moderator: :moderator
+ admin: :administrator,
+ moderator: :moderator,
}.each do |legacy_role, new_role|
- User.where(legacy_role => true).each do |u|
- puts "-- migrating #{u.screen_name} (#{u.id}) from field:#{legacy_role} to role:#{new_role}"
+ User.where(legacy_role => true).find_each do |u|
+ Rails.logger.debug { "-- migrating #{u.screen_name} (#{u.id}) from field:#{legacy_role} to role:#{new_role}" }
u.add_role new_role
u.public_send("#{legacy_role}=", false)
u.save!
@@ -22,10 +22,10 @@ class CreateInitialRoles < ActiveRecord::Migration[5.2]
def down
{
administrator: :admin,
- moderator: :moderator
+ moderator: :moderator,
}.each do |new_role, legacy_role|
User.with_role(new_role).each do |u|
- puts "-- migrating #{u.screen_name} (#{u.id}) from role:#{new_role} to field:#{legacy_role}"
+ Rails.logger.debug { "-- migrating #{u.screen_name} (#{u.id}) from role:#{new_role} to field:#{legacy_role}" }
u.public_send("#{legacy_role}=", true)
u.save!
end
diff --git a/db/migrate/20200425194536_remove_unused_profile_flags.rb b/db/migrate/20200425194536_remove_unused_profile_flags.rb
index 8ce20806..9804b080 100644
--- a/db/migrate/20200425194536_remove_unused_profile_flags.rb
+++ b/db/migrate/20200425194536_remove_unused_profile_flags.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RemoveUnusedProfileFlags < ActiveRecord::Migration[5.2]
def change
remove_column :users, :admin
diff --git a/db/migrate/20200504214933_update_theme_fields.rb b/db/migrate/20200504214933_update_theme_fields.rb
index ec577f1d..beda7b69 100644
--- a/db/migrate/20200504214933_update_theme_fields.rb
+++ b/db/migrate/20200504214933_update_theme_fields.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UpdateThemeFields < ActiveRecord::Migration[5.2]
def up
# CSS file related fields
diff --git a/db/migrate/20200517190138_rename_crop_fields.rb b/db/migrate/20200517190138_rename_crop_fields.rb
index 12e81325..54781a64 100644
--- a/db/migrate/20200517190138_rename_crop_fields.rb
+++ b/db/migrate/20200517190138_rename_crop_fields.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RenameCropFields < ActiveRecord::Migration[5.2]
def change
rename_column :users, :crop_h, :profile_picture_h
diff --git a/db/migrate/20200517192431_remove_paperclip_fields.rb b/db/migrate/20200517192431_remove_paperclip_fields.rb
index 00d4181e..7f4a4d02 100644
--- a/db/migrate/20200517192431_remove_paperclip_fields.rb
+++ b/db/migrate/20200517192431_remove_paperclip_fields.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RemovePaperclipFields < ActiveRecord::Migration[5.2]
def change
remove_column :users, :profile_picture_content_type
diff --git a/db/migrate/20200704163504_use_timestamped_ids.rb b/db/migrate/20200704163504_use_timestamped_ids.rb
index 49b44bd7..fa52f40d 100644
--- a/db/migrate/20200704163504_use_timestamped_ids.rb
+++ b/db/migrate/20200704163504_use_timestamped_ids.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'securerandom'
+require "securerandom"
# This migration changes the IDs of several tables from serial to a
# timestamped/"snowflake" one.
@@ -30,15 +30,15 @@ class UseTimestampedIds < ActiveRecord::Migration[5.2]
# we need to migrate related columns to bigints for this to work
{
- question: %i[answers inboxes],
- answer: %i[comments smiles subscriptions],
- comment: %i[comment_smiles],
- user: %i[announcements answers comment_smiles comments inboxes list_members lists moderation_comments moderation_votes questions reports services smiles subscriptions themes users_roles],
+ question: %i[answers inboxes],
+ answer: %i[comments smiles subscriptions],
+ comment: %i[comment_smiles],
+ user: %i[announcements answers comment_smiles comments inboxes list_members lists moderation_comments moderation_votes questions reports services smiles subscriptions themes users_roles],
# polymorphic tables go brrr
recipient: %i[notifications],
- source: %i[relationships],
- target: %i[notifications relationships reports],
+ source: %i[relationships],
+ target: %i[notifications relationships reports],
}.each do |ref, tbls|
tbls.each do |tbl|
say "Migrating #{tbl}.#{ref}_id to bigint"
diff --git a/db/migrate/20201001172537_add_otp_secret_key_to_users.rb b/db/migrate/20201001172537_add_otp_secret_key_to_users.rb
index 59a33b48..31dc4ed6 100644
--- a/db/migrate/20201001172537_add_otp_secret_key_to_users.rb
+++ b/db/migrate/20201001172537_add_otp_secret_key_to_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddOtpSecretKeyToUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :otp_secret_key, :string
diff --git a/db/migrate/20201101155648_create_totp_recovery_codes.rb b/db/migrate/20201101155648_create_totp_recovery_codes.rb
index 3b506256..1d7fac98 100644
--- a/db/migrate/20201101155648_create_totp_recovery_codes.rb
+++ b/db/migrate/20201101155648_create_totp_recovery_codes.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
class CreateTotpRecoveryCodes < ActiveRecord::Migration[5.2]
def change
create_table :totp_recovery_codes do |t|
t.bigint :user_id
t.string :code, limit: 8
end
- add_index :totp_recovery_codes, [:user_id, :code]
+ add_index :totp_recovery_codes, %i[user_id code]
end
end
diff --git a/db/migrate/20210811133004_add_direct_to_questions.rb b/db/migrate/20210811133004_add_direct_to_questions.rb
index 092426b9..30632fd6 100644
--- a/db/migrate/20210811133004_add_direct_to_questions.rb
+++ b/db/migrate/20210811133004_add_direct_to_questions.rb
@@ -5,7 +5,7 @@ class AddDirectToQuestions < ActiveRecord::Migration[5.2]
add_column :questions, :direct, :boolean, null: false, default: false
# default all legacy questions to direct
- execute 'UPDATE questions SET direct = true;'
+ execute "UPDATE questions SET direct = true;"
# All questions where
# - the author is not 'justask' (generated questions), and
diff --git a/db/migrate/20210814134115_create_user_bans.rb b/db/migrate/20210814134115_create_user_bans.rb
index a5146c90..7778d590 100644
--- a/db/migrate/20210814134115_create_user_bans.rb
+++ b/db/migrate/20210814134115_create_user_bans.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateUserBans < ActiveRecord::Migration[5.2]
def up
create_table :user_bans do |t|
@@ -15,7 +17,6 @@ class CreateUserBans < ActiveRecord::Migration[5.2]
SELECT users.id, users.ban_reason, users.banned_until, users.updated_at, NOW() FROM users
WHERE banned_until IS NOT NULL AND NOT permanently_banned;"
-
execute "INSERT INTO user_bans
(user_id, reason, expires_at, created_at, updated_at)
SELECT users.id, users.ban_reason, NULL, users.updated_at, NOW() FROM users
diff --git a/db/migrate/20211219153054_create_profiles.rb b/db/migrate/20211219153054_create_profiles.rb
index d6480af9..933be933 100644
--- a/db/migrate/20211219153054_create_profiles.rb
+++ b/db/migrate/20211219153054_create_profiles.rb
@@ -1,18 +1,20 @@
+# frozen_string_literal: true
+
class CreateProfiles < ActiveRecord::Migration[5.2]
def change
create_table :profiles do |t|
t.references :user, index: true, foreign_key: true
t.string :display_name, length: 50
- t.string :description, length: 200, null: false, default: ''
- t.string :location, length: 72, null: false, default: ''
- t.string :website, null: false, default: ''
- t.string :motivation_header, null: false, default: ''
+ t.string :description, length: 200, null: false, default: ""
+ t.string :location, length: 72, null: false, default: ""
+ t.string :website, null: false, default: ""
+ t.string :motivation_header, null: false, default: ""
t.timestamps
end
transaction do
- execute 'INSERT INTO profiles (user_id, display_name, description, location, website, motivation_header, created_at, updated_at) SELECT users.id as user_id, users.display_name, users.bio as description, users.location, users.website, users.motivation_header, users.created_at, users.updated_at FROM users;'
+ execute "INSERT INTO profiles (user_id, display_name, description, location, website, motivation_header, created_at, updated_at) SELECT users.id as user_id, users.display_name, users.bio as description, users.location, users.website, users.motivation_header, users.created_at, users.updated_at FROM users;"
remove_column :users, :display_name
remove_column :users, :bio
diff --git a/db/migrate/20211222165159_create_mute_rules.rb b/db/migrate/20211222165159_create_mute_rules.rb
index 0e548bc5..57057fc4 100644
--- a/db/migrate/20211222165159_create_mute_rules.rb
+++ b/db/migrate/20211222165159_create_mute_rules.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateMuteRules < ActiveRecord::Migration[5.2]
def change
create_table :mute_rules do |t|
diff --git a/db/migrate/20211228135426_add_indexes_to_notifications.rb b/db/migrate/20211228135426_add_indexes_to_notifications.rb
index 15288920..46c9bae9 100644
--- a/db/migrate/20211228135426_add_indexes_to_notifications.rb
+++ b/db/migrate/20211228135426_add_indexes_to_notifications.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddIndexesToNotifications < ActiveRecord::Migration[5.2]
def change
add_index :notifications, :recipient_id
diff --git a/db/migrate/20220104180510_add_post_tag_to_services.rb b/db/migrate/20220104180510_add_post_tag_to_services.rb
index ab09c201..85789e8e 100644
--- a/db/migrate/20220104180510_add_post_tag_to_services.rb
+++ b/db/migrate/20220104180510_add_post_tag_to_services.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddPostTagToServices < ActiveRecord::Migration[5.2]
def change
add_column :services, :post_tag, :string, limit: 20
diff --git a/db/migrate/20220909220449_add_webpush_app.rb b/db/migrate/20220909220449_add_webpush_app.rb
index 91b1d32d..f48a688b 100644
--- a/db/migrate/20220909220449_add_webpush_app.rb
+++ b/db/migrate/20220909220449_add_webpush_app.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
-require "webpush"
+require "web-push"
class AddWebpushApp < ActiveRecord::Migration[6.1]
def up
- vapid_keypair = Webpush.generate_key.to_hash
+ vapid_keypair = WebPush.generate_key.to_hash
app = Rpush::Webpush::App.new
app.name = "webpush"
app.certificate = vapid_keypair.merge(subject: APP_CONFIG.fetch("contact_email")).to_json
diff --git a/db/migrate/20231018172518_include_type_in_relationship_unique_constraint.rb b/db/migrate/20231018172518_include_type_in_relationship_unique_constraint.rb
new file mode 100644
index 00000000..171fe00f
--- /dev/null
+++ b/db/migrate/20231018172518_include_type_in_relationship_unique_constraint.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class IncludeTypeInRelationshipUniqueConstraint < ActiveRecord::Migration[6.1]
+ def change
+ change_table :relationships do |t|
+ t.remove_index(%i[source_id target_id])
+ t.index(%i[source_id target_id type], unique: true)
+ end
+ end
+end
diff --git a/db/migrate/20231026032527_rename_appendable_to_reaction.rb b/db/migrate/20231026032527_rename_appendable_to_reaction.rb
new file mode 100644
index 00000000..6a7b5d9d
--- /dev/null
+++ b/db/migrate/20231026032527_rename_appendable_to_reaction.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class RenameAppendableToReaction < ActiveRecord::Migration[7.0]
+ def change
+ rename_table :appendables, :reactions
+ remove_column :reactions, :type, :string
+ end
+end
diff --git a/db/migrate/20231028091613_move_appendable_notifications.rb b/db/migrate/20231028091613_move_appendable_notifications.rb
new file mode 100644
index 00000000..0763ed42
--- /dev/null
+++ b/db/migrate/20231028091613_move_appendable_notifications.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class MoveAppendableNotifications < ActiveRecord::Migration[7.0]
+ def up
+ Notification::where(target_type: "Appendable").update_all(target_type: "Reaction") # rubocop:disable Rails/SkipsModelValidations
+ end
+
+ def down
+ Notification::where(target_type: "Reaction").update_all(type: "Appendable") # rubocop:disable Rails/SkipsModelValidations
+ end
+end
diff --git a/db/migrate/20231107200845_optimise_indices.rb b/db/migrate/20231107200845_optimise_indices.rb
new file mode 100644
index 00000000..955a5b0f
--- /dev/null
+++ b/db/migrate/20231107200845_optimise_indices.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class OptimiseIndices < ActiveRecord::Migration[7.0]
+ def change
+ add_index :users, "LOWER(screen_name)", order: :desc, unique: true
+ remove_index :users, :screen_name, unique: true
+ add_index :user_bans, :expires_at, order: :desc
+ add_index :announcements, %i[starts_at ends_at], order: :desc
+ remove_index :themes, %i[user_id created_at]
+ add_index :themes, :user_id
+ end
+end
diff --git a/db/migrate/20231209212629_add_index_on_user_bans_user_id.rb b/db/migrate/20231209212629_add_index_on_user_bans_user_id.rb
new file mode 100644
index 00000000..cb6101d9
--- /dev/null
+++ b/db/migrate/20231209212629_add_index_on_user_bans_user_id.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddIndexOnUserBansUserId < ActiveRecord::Migration[7.0]
+ def change
+ add_index :user_bans, :user_id
+ end
+end
diff --git a/db/migrate/20231220100445_remove_duplicate_reactions.rb b/db/migrate/20231220100445_remove_duplicate_reactions.rb
new file mode 100644
index 00000000..d949e40c
--- /dev/null
+++ b/db/migrate/20231220100445_remove_duplicate_reactions.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class RemoveDuplicateReactions < ActiveRecord::Migration[7.0]
+ def up
+ execute <<~SQUIRREL
+ DELETE FROM reactions
+ WHERE id IN (
+ SELECT id FROM (
+ SELECT id, row_number() over (PARTITION BY parent_type, parent_id, user_id ORDER BY id) AS row_number FROM reactions
+ )s WHERE row_number >= 2
+ )
+ SQUIRREL
+
+ add_index :reactions, %i[parent_type parent_id user_id], unique: true
+ end
+
+ def down
+ remove_index :reactions, %i[parent_type parent_id user_id]
+ end
+end
diff --git a/db/migrate/20240123182422_add_target_user_to_reports.rb b/db/migrate/20240123182422_add_target_user_to_reports.rb
new file mode 100644
index 00000000..a91ed9e6
--- /dev/null
+++ b/db/migrate/20240123182422_add_target_user_to_reports.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class AddTargetUserToReports < ActiveRecord::Migration[7.0]
+ def up
+ add_reference :reports, :target_user, null: true, foreign_key: false
+
+ execute <<~SQL.squish
+ UPDATE reports
+ SET target_user_id = users.id
+ FROM users
+ WHERE users.id = reports.target_id AND reports.type = 'Reports::User'
+ SQL
+
+ execute <<~SQL.squish
+ UPDATE reports
+ SET target_user_id = users.id
+ FROM users, comments
+ WHERE users.id = comments.user_id AND comments.id = reports.target_id AND reports.type = 'Reports::Comment'
+ SQL
+
+ execute <<~SQL.squish
+ UPDATE reports
+ SET target_user_id = users.id
+ FROM users, answers
+ WHERE users.id = answers.user_id AND answers.id = reports.target_id AND reports.type = 'Reports::Answer'
+ SQL
+
+ execute <<~SQL.squish
+ UPDATE reports
+ SET target_user_id = users.id
+ FROM users, questions
+ WHERE users.id = questions.user_id AND questions.id = reports.target_id AND reports.type = 'Reports::Question'
+ SQL
+ end
+
+ def down
+ remove_reference :reports, :target_user, null: true, foreign_key: false
+ end
+end
diff --git a/db/migrate/20240127112216_rename_inboxes_to_inbox_entries.rb b/db/migrate/20240127112216_rename_inboxes_to_inbox_entries.rb
new file mode 100644
index 00000000..03a8f55b
--- /dev/null
+++ b/db/migrate/20240127112216_rename_inboxes_to_inbox_entries.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class RenameInboxesToInboxEntries < ActiveRecord::Migration[7.0]
+ def change
+ rename_table :inboxes, :inbox_entries
+ end
+end
diff --git a/db/migrate/20240301203930_add_last_reports_visit_to_users.rb b/db/migrate/20240301203930_add_last_reports_visit_to_users.rb
new file mode 100644
index 00000000..90b01615
--- /dev/null
+++ b/db/migrate/20240301203930_add_last_reports_visit_to_users.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddLastReportsVisitToUsers < ActiveRecord::Migration[7.0]
+ def change
+ add_column :users, :last_reports_visit, :datetime
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c052e79d..3d1fdcaf 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,8 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2023_05_26_181715) do
-
+ActiveRecord::Schema[7.0].define(version: 2024_03_01_203930) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -19,11 +18,12 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.text "content", null: false
t.string "link_text"
t.string "link_href"
- t.datetime "starts_at", null: false
- t.datetime "ends_at", null: false
+ t.datetime "starts_at", precision: nil, null: false
+ t.datetime "ends_at", precision: nil, null: false
t.bigint "user_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
+ t.index ["starts_at", "ends_at"], name: "index_announcements_on_starts_at_and_ends_at", order: :desc
t.index ["user_id"], name: "index_announcements_on_user_id"
end
@@ -31,8 +31,8 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.bigint "user_id"
t.string "identifier"
t.bigint "question_id"
- t.datetime "created_at", precision: 6, null: false
- t.datetime "updated_at", precision: 6, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.bigint "target_user_id"
t.index ["identifier"], name: "index_anonymous_blocks_on_identifier"
t.index ["question_id"], name: "index_anonymous_blocks_on_question_id"
@@ -45,54 +45,42 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.bigint "question_id"
t.integer "comment_count", default: 0, null: false
t.bigint "user_id"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.integer "smile_count", default: 0, null: false
- t.datetime "pinned_at"
+ t.datetime "pinned_at", precision: nil
t.index ["created_at"], name: "index_answers_on_created_at", order: :desc
t.index ["question_id"], name: "index_answers_on_question_id"
t.index ["user_id", "created_at"], name: "index_answers_on_user_id_and_created_at"
t.index ["user_id", "pinned_at"], name: "index_answers_on_user_id_and_pinned_at"
end
- create_table "appendables", force: :cascade do |t|
- t.string "type", null: false
- t.bigint "user_id", null: false
- t.bigint "parent_id", null: false
- t.string "parent_type", null: false
- t.text "content"
- t.datetime "created_at", precision: 6, null: false
- t.datetime "updated_at", precision: 6, null: false
- t.index ["parent_id", "parent_type"], name: "index_appendables_on_parent_id_and_parent_type"
- t.index ["user_id", "created_at"], name: "index_appendables_on_user_id_and_created_at"
- end
-
create_table "comments", id: :bigint, default: -> { "gen_timestamp_id('comments'::text)" }, force: :cascade do |t|
t.string "content"
t.bigint "answer_id"
t.bigint "user_id"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.integer "smile_count", default: 0, null: false
t.index ["answer_id"], name: "index_comments_on_answer_id"
t.index ["user_id", "created_at"], name: "index_comments_on_user_id_and_created_at"
end
- create_table "inboxes", id: :serial, force: :cascade do |t|
+ create_table "inbox_entries", id: :serial, force: :cascade do |t|
t.bigint "user_id"
t.bigint "question_id"
t.boolean "new"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.index ["question_id"], name: "index_inboxes_on_question_id"
- t.index ["user_id"], name: "index_inboxes_on_user_id"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
+ t.index ["question_id"], name: "index_inbox_entries_on_question_id"
+ t.index ["user_id"], name: "index_inbox_entries_on_user_id"
end
create_table "list_members", id: :serial, force: :cascade do |t|
t.integer "list_id", null: false
t.bigint "user_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.index ["list_id", "user_id"], name: "index_list_members_on_list_id_and_user_id", unique: true
end
@@ -101,16 +89,16 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.string "name"
t.string "display_name"
t.boolean "private", default: true
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.index ["user_id", "name"], name: "index_lists_on_user_id_and_name", unique: true
end
create_table "mute_rules", id: :bigint, default: -> { "gen_timestamp_id('mute_rules'::text)" }, force: :cascade do |t|
t.bigint "user_id"
t.string "muted_phrase"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.index ["user_id"], name: "index_mute_rules_on_user_id"
end
@@ -119,8 +107,8 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.bigint "target_id"
t.bigint "recipient_id"
t.boolean "new"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.string "type", null: false
t.index ["new"], name: "index_notifications_on_new"
t.index ["recipient_id"], name: "index_notifications_on_recipient_id"
@@ -134,8 +122,8 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.string "location", default: "", null: false
t.string "website", default: "", null: false
t.string "motivation_header", default: "", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.string "anon_display_name"
t.boolean "allow_long_questions", default: true
t.index ["user_id"], name: "index_profiles_on_user_id"
@@ -146,20 +134,32 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.boolean "author_is_anonymous"
t.string "author_identifier"
t.bigint "user_id"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.integer "answer_count", default: 0, null: false
t.boolean "direct", default: false, null: false
t.index ["user_id", "created_at"], name: "index_questions_on_user_id_and_created_at"
end
+ create_table "reactions", force: :cascade do |t|
+ t.bigint "user_id", null: false
+ t.bigint "parent_id", null: false
+ t.string "parent_type", null: false
+ t.text "content"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["parent_id", "parent_type"], name: "index_reactions_on_parent_id_and_parent_type"
+ t.index ["parent_type", "parent_id", "user_id"], name: "index_reactions_on_parent_type_and_parent_id_and_user_id", unique: true
+ t.index ["user_id", "created_at"], name: "index_reactions_on_user_id_and_created_at"
+ end
+
create_table "relationships", id: :serial, force: :cascade do |t|
t.bigint "source_id"
t.bigint "target_id"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.string "type", null: false
- t.index ["source_id", "target_id"], name: "index_relationships_on_source_id_and_target_id", unique: true
+ t.index ["source_id", "target_id", "type"], name: "index_relationships_on_source_id_and_target_id_and_type", unique: true
t.index ["source_id"], name: "index_relationships_on_source_id"
t.index ["target_id"], name: "index_relationships_on_target_id"
t.index ["type"], name: "index_relationships_on_type"
@@ -169,10 +169,12 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.string "type", null: false
t.bigint "target_id", null: false
t.bigint "user_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.boolean "deleted", default: false
t.string "reason"
+ t.bigint "target_user_id"
+ t.index ["target_user_id"], name: "index_reports_on_target_user_id"
t.index ["type", "target_id"], name: "index_reports_on_type_and_target_id"
t.index ["user_id", "created_at"], name: "index_reports_on_user_id_and_created_at"
end
@@ -181,10 +183,10 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.string "name"
t.string "resource_type"
t.bigint "resource_id"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
- t.index ["resource_type", "resource_id"], name: "index_roles_on_resource_type_and_resource_id"
+ t.index ["resource_type", "resource_id"], name: "index_roles_on_resource"
end
create_table "rpush_apps", force: :cascade do |t|
@@ -193,14 +195,14 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.text "certificate"
t.string "password"
t.integer "connections", default: 1, null: false
- t.datetime "created_at", precision: 6, null: false
- t.datetime "updated_at", precision: 6, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.string "type", null: false
t.string "auth_key"
t.string "client_id"
t.string "client_secret"
t.string "access_token"
- t.datetime "access_token_expiration"
+ t.datetime "access_token_expiration", precision: nil
t.text "apn_key"
t.string "apn_key_id"
t.string "team_id"
@@ -210,9 +212,9 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
create_table "rpush_feedback", force: :cascade do |t|
t.string "device_token"
- t.datetime "failed_at", null: false
- t.datetime "created_at", precision: 6, null: false
- t.datetime "updated_at", precision: 6, null: false
+ t.datetime "failed_at", precision: nil, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.integer "app_id"
t.index ["device_token"], name: "index_rpush_feedback_on_device_token"
end
@@ -225,14 +227,14 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.text "data"
t.integer "expiry", default: 86400
t.boolean "delivered", default: false, null: false
- t.datetime "delivered_at"
+ t.datetime "delivered_at", precision: nil
t.boolean "failed", default: false, null: false
- t.datetime "failed_at"
+ t.datetime "failed_at", precision: nil
t.integer "error_code"
t.text "error_description"
- t.datetime "deliver_after"
- t.datetime "created_at", precision: 6, null: false
- t.datetime "updated_at", precision: 6, null: false
+ t.datetime "deliver_after", precision: nil
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.boolean "alert_is_json", default: false, null: false
t.string "type", null: false
t.string "collapse_key"
@@ -241,7 +243,7 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.integer "app_id", null: false
t.integer "retries", default: 0
t.string "uri"
- t.datetime "fail_after"
+ t.datetime "fail_after", precision: nil
t.boolean "processing", default: false, null: false
t.integer "priority"
t.text "url_args"
@@ -259,8 +261,8 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
create_table "subscriptions", id: :serial, force: :cascade do |t|
t.bigint "user_id", null: false
t.bigint "answer_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.index ["user_id", "answer_id"], name: "index_subscriptions_on_user_id_and_answer_id"
end
@@ -282,8 +284,8 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.integer "background_color", default: 15789556
t.integer "body_text", default: 0
t.integer "muted_text", default: 7107965
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.integer "input_color", default: 15789556, null: false
t.integer "input_text", default: 0, null: false
t.integer "raised_accent", default: 16250871
@@ -292,7 +294,7 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.integer "input_placeholder", default: 7107965, null: false
t.integer "raised_text", default: 0, null: false
t.integer "raised_accent_text", default: 0, null: false
- t.index ["user_id", "created_at"], name: "index_themes_on_user_id_and_created_at"
+ t.index ["user_id"], name: "index_themes_on_user_id"
end
create_table "totp_recovery_codes", force: :cascade do |t|
@@ -304,25 +306,27 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
create_table "user_bans", force: :cascade do |t|
t.bigint "user_id"
t.string "reason"
- t.datetime "expires_at"
+ t.datetime "expires_at", precision: nil
t.bigint "banned_by_id"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
+ t.index ["expires_at"], name: "index_user_bans_on_expires_at", order: :desc
+ t.index ["user_id"], name: "index_user_bans_on_user_id"
end
create_table "users", id: :bigint, default: -> { "gen_timestamp_id('users'::text)" }, force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
- t.datetime "reset_password_sent_at"
- t.datetime "remember_created_at"
+ t.datetime "reset_password_sent_at", precision: nil
+ t.datetime "remember_created_at", precision: nil
t.integer "sign_in_count", default: 0, null: false
- t.datetime "current_sign_in_at"
- t.datetime "last_sign_in_at"
+ t.datetime "current_sign_in_at", precision: nil
+ t.datetime "last_sign_in_at", precision: nil
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.string "screen_name"
t.integer "asked_count", default: 0, null: false
t.integer "answered_count", default: 0, null: false
@@ -347,13 +351,13 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.integer "profile_header_h"
t.string "locale", default: "en"
t.string "confirmation_token"
- t.datetime "confirmed_at"
- t.datetime "confirmation_sent_at"
+ t.datetime "confirmed_at", precision: nil
+ t.datetime "confirmation_sent_at", precision: nil
t.string "unconfirmed_email"
t.boolean "show_foreign_themes", default: true, null: false
t.string "export_url"
t.boolean "export_processing", default: false, null: false
- t.datetime "export_created_at"
+ t.datetime "export_created_at", precision: nil
t.string "otp_secret_key"
t.integer "otp_module", default: 0, null: false
t.boolean "privacy_lock_inbox", default: false
@@ -363,12 +367,13 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
t.boolean "sharing_enabled", default: false
t.boolean "sharing_autoclose", default: false
t.string "sharing_custom_url"
- t.datetime "notifications_updated_at"
- t.datetime "inbox_updated_at"
+ t.datetime "notifications_updated_at", precision: nil
+ t.datetime "inbox_updated_at", precision: nil
+ t.datetime "last_reports_visit"
+ t.index "lower((screen_name)::text)", name: "index_users_on_LOWER_screen_name", unique: true
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
- t.index ["screen_name"], name: "index_users_on_screen_name", unique: true
end
create_table "users_roles", id: false, force: :cascade do |t|
@@ -382,8 +387,8 @@ ActiveRecord::Schema.define(version: 2023_05_26_181715) do
create_table "web_push_subscriptions", force: :cascade do |t|
t.bigint "user_id", null: false
t.json "subscription"
- t.datetime "created_at", precision: 6, null: false
- t.datetime "updated_at", precision: 6, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.integer "failures", default: 0
t.index ["user_id"], name: "index_web_push_subscriptions_on_user_id"
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 9874789c..8136d5c0 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
#
diff --git a/lib/exporter.rb b/lib/exporter.rb
index 07f2ae46..dca0361c 100644
--- a/lib/exporter.rb
+++ b/lib/exporter.rb
@@ -7,7 +7,7 @@ require "zip/filesystem"
# require all data export use cases via Zeitwerk
# rubocop:disable Lint/Void
UseCase::DataExport::Answers
-UseCase::DataExport::Appendables
+UseCase::DataExport::Reactions
UseCase::DataExport::Comments
UseCase::DataExport::MuteRules
UseCase::DataExport::Questions
diff --git a/lib/retrospring/version.rb b/lib/retrospring/version.rb
index 7a679293..a104f1e7 100644
--- a/lib/retrospring/version.rb
+++ b/lib/retrospring/version.rb
@@ -13,11 +13,11 @@ module Retrospring
module Version
module_function
- def year = 2023
+ def year = 2024
- def month = 9
+ def month = 3
- def day = 1
+ def day = 19
def patch = 0
diff --git a/lib/use_case/data_export/appendables.rb b/lib/use_case/data_export/appendables.rb
deleted file mode 100644
index 19221aee..00000000
--- a/lib/use_case/data_export/appendables.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module UseCase
- module DataExport
- class Appendables < UseCase::DataExport::Base
- def files = {
- "appendables.json" => json_file!(
- appendables: [
- *user.smiles.map(&method(:collect_appendable))
- ]
- )
- }
-
- def collect_appendable(appendable)
- {}.tap do |h|
- column_names(::Appendable).each do |field|
- h[field] = appendable[field]
- end
- end
- end
- end
- end
-end
diff --git a/lib/use_case/data_export/reactions.rb b/lib/use_case/data_export/reactions.rb
new file mode 100644
index 00000000..7710f8e7
--- /dev/null
+++ b/lib/use_case/data_export/reactions.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module UseCase
+ module DataExport
+ class Reactions < UseCase::DataExport::Base
+ def files = {
+ "reactions.json" => json_file!(
+ reactions: [
+ *user.smiles.map(&method(:collect_reaction))
+ ],
+ ),
+ }
+
+ def collect_reaction(reaction)
+ {}.tap do |h|
+ column_names(::Reaction).each do |field|
+ h[field] = reaction[field]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/use_case/data_export/user.rb b/lib/use_case/data_export/user.rb
index 2bd4ceb1..49094cc1 100644
--- a/lib/use_case/data_export/user.rb
+++ b/lib/use_case/data_export/user.rb
@@ -15,6 +15,7 @@ module UseCase
reset_password_token
inbox_updated_at
notifications_updated_at
+ last_reports_visit
].freeze
IGNORED_FIELDS_PROFILES = %i[
diff --git a/lib/use_case/mute_rule/create.rb b/lib/use_case/mute_rule/create.rb
index 68a587aa..eee45dd6 100644
--- a/lib/use_case/mute_rule/create.rb
+++ b/lib/use_case/mute_rule/create.rb
@@ -7,7 +7,7 @@ module UseCase
option :phrase, type: Types::Coercible::String
def call
- rule = ::MuteRule.create(
+ rule = ::MuteRule.create!(
user:,
muted_phrase: phrase
)
diff --git a/lib/use_case/question/create.rb b/lib/use_case/question/create.rb
index 17987a1d..2f228816 100644
--- a/lib/use_case/question/create.rb
+++ b/lib/use_case/question/create.rb
@@ -20,7 +20,7 @@ module UseCase
increment_asked_count
increment_metric
- inbox = ::Inbox.create!(user: target_user, question:, new: true)
+ inbox = ::InboxEntry.create!(user: target_user, question:, new: true)
notify(inbox)
{
@@ -65,6 +65,7 @@ module UseCase
def check_user
raise Errors::NotAuthorized if target_user.privacy_require_user && !source_user_id
raise Errors::QuestionTooLong if content.length > ::Question::SHORT_QUESTION_MAX_LENGTH && !target_user.profile.allow_long_questions
+ raise Errors::QuestionTooLong if content.length > ::Question::LONG_QUESTION_MAX_LENGTH && target_user.profile.allow_long_questions
end
def create_question
diff --git a/lib/use_case/question/create_followers.rb b/lib/use_case/question/create_followers.rb
index 04fe3844..1c88a5ff 100644
--- a/lib/use_case/question/create_followers.rb
+++ b/lib/use_case/question/create_followers.rb
@@ -6,20 +6,25 @@ module UseCase
option :source_user_id, type: Types::Coercible::Integer
option :content, type: Types::Coercible::String
option :author_identifier, type: Types::Coercible::String | Types::Nil
+ option :send_to_own_inbox, type: Types::Params::Bool, default: proc { false }
def call
+ check_question
+
question = ::Question.create!(
content:,
author_is_anonymous: false,
author_identifier:,
user: source_user,
- direct: false
+ direct: false,
)
increment_asked_count
increment_metric
- QuestionWorker.perform_async(source_user_id, question.id)
+ args = source_user.followers.map { |f| [f.id, question.id] }
+ SendToInboxJob.perform_async(source_user_id, question.id) if send_to_own_inbox
+ SendToInboxJob.perform_bulk(args)
{
status: 201,
@@ -29,6 +34,10 @@ module UseCase
private
+ def check_question
+ raise Errors::QuestionTooLong if content.length > ::Question::LONG_QUESTION_MAX_LENGTH
+ end
+
def increment_asked_count
source_user.increment(:asked_count)
source_user.save
@@ -40,12 +49,12 @@ module UseCase
anonymous: false,
followers: true,
generated: false,
- }
+ },
)
end
def source_user
- @source_user ||= ::User.find(source_user_id)
+ @source_user ||= ::User.includes(:followers).find(source_user_id)
end
end
end
diff --git a/lib/use_case/reaction/create.rb b/lib/use_case/reaction/create.rb
new file mode 100644
index 00000000..1939f9d6
--- /dev/null
+++ b/lib/use_case/reaction/create.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module UseCase
+ module Reaction
+ class Create < UseCase::Base
+ option :source_user_id, type: Types::Coercible::Integer
+ option :target, type: Types.Instance(::Answer) | Types.Instance(::Comment)
+ option :content, type: Types::Coercible::String, optional: true
+
+ def call
+ reaction = source_user.smile target
+
+ {
+ status: 201,
+ resource: reaction,
+ }
+ end
+
+ private
+
+ def source_user
+ @source_user ||= ::User.find(source_user_id)
+ end
+ end
+ end
+end
diff --git a/lib/use_case/reaction/destroy.rb b/lib/use_case/reaction/destroy.rb
new file mode 100644
index 00000000..8d77219a
--- /dev/null
+++ b/lib/use_case/reaction/destroy.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module UseCase
+ module Reaction
+ class Destroy < UseCase::Base
+ option :source_user_id, type: Types::Coercible::Integer
+ option :target, type: Types.Instance(::Answer) | Types.Instance(::Comment)
+
+ def call
+ source_user.unsmile target
+
+ {
+ status: 204,
+ resource: nil,
+ }
+ end
+
+ private
+
+ def source_user
+ @source_user ||= ::User.find(source_user_id)
+ end
+ end
+ end
+end
diff --git a/package.json b/package.json
index 49f471aa..80cc1712 100644
--- a/package.json
+++ b/package.json
@@ -8,32 +8,32 @@
},
"dependencies": {
"@fontsource/lexend": "^4.5.15",
- "@fortawesome/fontawesome-free": "^6.4.2",
- "@github/hotkey": "^2.0.1",
+ "@fortawesome/fontawesome-free": "^6.5.2",
+ "@github/hotkey": "^3.1.1",
"@hotwired/stimulus": "^3.2.2",
- "@hotwired/turbo-rails": "^7.3.0",
- "@melloware/coloris": "^0.21.1",
+ "@hotwired/turbo-rails": "^8.0.4",
+ "@melloware/coloris": "^0.24.0",
"@popperjs/core": "^2.11",
- "@rails/request.js": "^0.0.8",
+ "@rails/request.js": "^0.0.9",
"bootstrap": "^5.2",
"buffer": "^6.0.3",
"cheet.js": "^0.3.3",
"croppr": "^2.3.1",
"i18n-js": "^4.0",
"js-cookie": "2.2.1",
- "sass": "^1.66.1",
+ "sass": "^1.77.6",
"sweetalert": "1.1.3",
"toastify-js": "^1.12.0",
- "typescript": "^5.2.2"
+ "typescript": "^5.5.2"
},
"devDependencies": {
- "@typescript-eslint/eslint-plugin": "^4.11.0",
- "@typescript-eslint/parser": "^4.11.0",
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
+ "@typescript-eslint/parser": "^6.21.0",
"esbuild": "^0.17.0",
- "eslint": "^7.16.0",
- "eslint-plugin-import": "^2.28.1",
- "stylelint": "^15.10.3",
- "stylelint-config-standard-scss": "^11.0.0",
- "stylelint-scss": "^5.1.0"
+ "eslint": "^8.57.0",
+ "eslint-plugin-import": "^2.29.1",
+ "stylelint": "^15.11.0",
+ "stylelint-config-standard-scss": "^11.1.0",
+ "stylelint-scss": "^5.3.2"
}
}
diff --git a/spec/components/avatar_component_spec.rb b/spec/components/avatar_component_spec.rb
new file mode 100644
index 00000000..922c52d0
--- /dev/null
+++ b/spec/components/avatar_component_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe AvatarComponent, type: :component do
+ let(:user) { FactoryBot.create(:user) }
+
+ it "renders an avatar" do
+ expect(
+ render_inline(described_class.new(user:, size: "sm")).to_html,
+ ).to include(
+ "no_avatar.png",
+ )
+ end
+
+ it "gets the medium version of a profile picture if requested" do
+ expect(
+ render_inline(described_class.new(user:, size: "md")).to_html,
+ ).to include(
+ "medium/",
+ )
+ end
+
+ it "gets the large version of a profile picture if requested" do
+ expect(
+ render_inline(described_class.new(user:, size: "xl")).to_html,
+ ).to include(
+ "large/",
+ )
+ end
+
+ it "includes additionally passed classes" do
+ expect(
+ render_inline(described_class.new(user:, size: "md", classes: %w[first-class second-class])).to_html,
+ ).to include(
+ 'class="avatar-md first-class second-class"',
+ )
+ end
+end
diff --git a/spec/controllers/ajax/answer_controller_spec.rb b/spec/controllers/ajax/answer_controller_spec.rb
index 18c65354..68218c2a 100644
--- a/spec/controllers/ajax/answer_controller_spec.rb
+++ b/spec/controllers/ajax/answer_controller_spec.rb
@@ -4,6 +4,8 @@
require "rails_helper"
describe Ajax::AnswerController, :ajax_controller, type: :controller do
+ include ActiveSupport::Testing::TimeHelpers
+
let(:question) { FactoryBot.create(:question, user: FactoryBot.build(:user, privacy_allow_stranger_answers: asker_allows_strangers)) }
let(:asker_allows_strangers) { true }
@@ -26,6 +28,8 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do
end
include_examples "returns the expected response"
+
+ include_examples "touches user timestamp", :inbox_updated_at
end
shared_examples "does not create the answer" do
@@ -61,7 +65,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do
let(:shared_services) { %w[twitter] }
context "when inbox is true" do
- let(:id) { FactoryBot.create(:inbox, user: inbox_user, question:).id }
+ let(:id) { FactoryBot.create(:inbox_entry, user: inbox_user, question:).id }
let(:inbox) { true }
context "when the inbox entry belongs to the user" do
@@ -87,7 +91,10 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do
let(:expected_response) do
super().merge(
"sharing" => {
+ "url" => a_string_matching("https://#{APP_CONFIG['hostname']}/"),
+ "text" => a_string_matching("Werfen Sie nicht länger das Fenster zum Geld hinaus!"),
"twitter" => a_string_matching("https://twitter.com/"),
+ "bluesky" => a_string_matching("https://bsky.app/"),
"tumblr" => a_string_matching("https://www.tumblr.com/"),
"telegram" => a_string_matching("https://t.me/"),
"custom" => a_string_matching(/Werfen\+Sie\+nicht\+l%C3%A4nger\+das\+Fenster\+zum\+Geld\+hinaus%21/),
@@ -166,7 +173,10 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do
let(:expected_response) do
super().merge(
"sharing" => {
+ "url" => a_string_matching("https://#{APP_CONFIG['hostname']}/"),
+ "text" => a_string_matching("Werfen Sie nicht länger das Fenster zum Geld hinaus!"),
"twitter" => a_string_matching("https://twitter.com/"),
+ "bluesky" => a_string_matching("https://bsky.app/"),
"tumblr" => a_string_matching("https://www.tumblr.com/"),
"telegram" => a_string_matching("https://t.me/"),
"custom" => a_string_matching(/Werfen\+Sie\+nicht\+l%C3%A4nger\+das\+Fenster\+zum\+Geld\+hinaus%21/),
@@ -317,13 +327,22 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do
include_examples "deletes the answer"
it "returns the question back to the user's inbox" do
- expect { subject }.to(change { Inbox.where(question_id: answer.question.id, user_id: user.id).count }.by(1))
+ expect { subject }.to(change { InboxEntry.where(question_id: answer.question.id, user_id: user.id).count }.by(1))
end
it "returns the question back to the user's inbox when the user has anonymous questions disabled" do
user.privacy_allow_anonymous_questions = false
user.save
- expect { subject }.to(change { Inbox.where(question_id: answer.question.id, user_id: user.id).count }.by(1))
+ expect { subject }.to(change { InboxEntry.where(question_id: answer.question.id, user_id: user.id).count }.by(1))
+ end
+
+ it "updates the inbox caching timestamp for the user who answered" do
+ initial_timestamp = 1.day.ago
+ answer.user.update(inbox_updated_at: initial_timestamp)
+ travel_to 1.day.from_now do
+ # using string representation to avoid precision issues
+ expect { subject }.to(change { answer.user.reload.inbox_updated_at.to_s }.from(initial_timestamp.to_s).to(DateTime.now))
+ end
end
end
diff --git a/spec/controllers/ajax/comment_controller_spec.rb b/spec/controllers/ajax/comment_controller_spec.rb
index 0f4189b8..ace88ce6 100644
--- a/spec/controllers/ajax/comment_controller_spec.rb
+++ b/spec/controllers/ajax/comment_controller_spec.rb
@@ -4,6 +4,8 @@
require "rails_helper"
describe Ajax::CommentController, :ajax_controller, type: :controller do
+ include ActiveSupport::Testing::TimeHelpers
+
let(:answer) { FactoryBot.create(:answer, user: FactoryBot.create(:user)) }
describe "#create" do
@@ -23,6 +25,18 @@ describe Ajax::CommentController, :ajax_controller, type: :controller do
expect(answer.reload.comments.ids).to include(Comment.last.id)
end
+ context "a user is subscribed to the answer" do
+ let(:subscribed_user) { FactoryBot.create(:user) }
+
+ it "updates the notification caching timestamp for a subscribed user" do
+ Subscription.subscribe(subscribed_user, answer)
+
+ travel_to(1.day.from_now) do
+ expect { subject }.to change { subscribed_user.reload.notifications_updated_at }.to(DateTime.now)
+ end
+ end
+ end
+
include_examples "returns the expected response"
end
diff --git a/spec/controllers/ajax/inbox_controller_spec.rb b/spec/controllers/ajax/inbox_controller_spec.rb
index 3b50c43d..6e48e53e 100644
--- a/spec/controllers/ajax/inbox_controller_spec.rb
+++ b/spec/controllers/ajax/inbox_controller_spec.rb
@@ -7,17 +7,17 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
describe "#remove" do
let(:params) do
{
- id: inbox_entry_id
+ id: inbox_entry_id,
}
end
- subject { delete(:remove, params: params) }
+ subject { delete(:remove, params:) }
context "when user is signed in" do
before(:each) { sign_in(user) }
context "when inbox entry exists" do
- let(:inbox_entry) { FactoryBot.create(:inbox, user: inbox_user) }
+ let(:inbox_entry) { FactoryBot.create(:inbox_entry, user: inbox_user) }
let(:inbox_entry_id) { inbox_entry.id }
# ensure the inbox entry exists
@@ -28,14 +28,14 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
let(:expected_response) do
{
"success" => true,
- "status" => "okay",
- "message" => anything
+ "status" => "okay",
+ "message" => anything,
}
end
it "removes the inbox entry" do
- expect { subject }.to(change { user.inboxes.count }.by(-1))
- expect { Inbox.find(inbox_entry.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { subject }.to(change { user.inbox_entries.count }.by(-1))
+ expect { InboxEntry.find(inbox_entry.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
include_examples "returns the expected response"
@@ -46,14 +46,14 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
let(:expected_response) do
{
"success" => false,
- "status" => "fail",
- "message" => anything
+ "status" => "fail",
+ "message" => anything,
}
end
it "does not remove the inbox entry" do
- expect { subject }.not_to(change { Inbox.count })
- expect { Inbox.find(inbox_entry.id) }.not_to raise_error
+ expect { subject }.not_to(change { InboxEntry.count })
+ expect { InboxEntry.find(inbox_entry.id) }.not_to raise_error
end
include_examples "returns the expected response"
@@ -65,8 +65,8 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
let(:expected_response) do
{
"success" => false,
- "status" => "not_found",
- "message" => anything
+ "status" => "not_found",
+ "message" => anything,
}
end
@@ -79,8 +79,8 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
let(:expected_response) do
{
"success" => false,
- "status" => "not_found",
- "message" => anything
+ "status" => "not_found",
+ "message" => anything,
}
end
@@ -97,8 +97,8 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
let(:expected_response) do
{
"success" => true,
- "status" => "okay",
- "message" => anything
+ "status" => "okay",
+ "message" => anything,
}
end
@@ -107,12 +107,12 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
context "when user has some inbox entries" do
let(:some_other_user) { FactoryBot.create(:user) }
before do
- 10.times { FactoryBot.create(:inbox, user: user) }
- 10.times { FactoryBot.create(:inbox, user: some_other_user) }
+ 10.times { FactoryBot.create(:inbox_entry, user:) }
+ 10.times { FactoryBot.create(:inbox_entry, user: some_other_user) }
end
it "deletes all the entries from the user's inbox" do
- expect { subject }.to(change { [Inbox.count, user.inboxes.count] }.from([20, 10]).to([10, 0]))
+ expect { subject }.to(change { [InboxEntry.count, user.inbox_entries.count] }.from([20, 10]).to([10, 0]))
end
include_examples "returns the expected response"
@@ -123,8 +123,8 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
let(:expected_response) do
{
"success" => false,
- "status" => "err",
- "message" => anything
+ "status" => "err",
+ "message" => anything,
}
end
@@ -135,11 +135,11 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
describe "#remove_all_author" do
let(:params) do
{
- author: author
+ author:,
}
end
- subject { delete(:remove_all_author, params: params) }
+ subject { delete(:remove_all_author, params:) }
context "when user is signed in" do
before(:each) { sign_in(user) }
@@ -148,8 +148,8 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
let(:expected_response) do
{
"success" => true,
- "status" => "okay",
- "message" => anything
+ "status" => "okay",
+ "message" => anything,
}
end
@@ -162,13 +162,13 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
normal_question = FactoryBot.create(:question, user: some_other_user, author_is_anonymous: false)
anon_question = FactoryBot.create(:question, user: some_other_user, author_is_anonymous: true)
- 10.times { FactoryBot.create(:inbox, user: user) }
- 3.times { FactoryBot.create(:inbox, user: user, question: normal_question) }
- 2.times { FactoryBot.create(:inbox, user: user, question: anon_question) }
+ 10.times { FactoryBot.create(:inbox_entry, user:) }
+ 3.times { FactoryBot.create(:inbox_entry, user:, question: normal_question) }
+ 2.times { FactoryBot.create(:inbox_entry, user:, question: anon_question) }
end
it "deletes all the entries asked by some other user which are not anonymous from the user's inbox" do
- expect { subject }.to(change { user.inboxes.count }.from(15).to(12))
+ expect { subject }.to(change { user.inbox_entries.count }.from(15).to(12))
end
include_examples "returns the expected response"
@@ -179,8 +179,8 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
let(:expected_response) do
{
"success" => false,
- "status" => "err",
- "message" => anything
+ "status" => "err",
+ "message" => anything,
}
end
@@ -193,8 +193,8 @@ describe Ajax::InboxController, :ajax_controller, type: :controller do
let(:expected_response) do
{
"success" => false,
- "status" => "err",
- "message" => anything
+ "status" => "err",
+ "message" => anything,
}
end
diff --git a/spec/controllers/ajax/moderation_controller_spec.rb b/spec/controllers/ajax/moderation_controller_spec.rb
index 97462e81..33035467 100644
--- a/spec/controllers/ajax/moderation_controller_spec.rb
+++ b/spec/controllers/ajax/moderation_controller_spec.rb
@@ -225,7 +225,7 @@ describe Ajax::ModerationController, :ajax_controller, type: :controller do
describe "#privilege" do
valid_role_pairs = {
moderator: :moderator,
- admin: :administrator
+ administrator: :administrator,
}.freeze
let(:params) do
diff --git a/spec/controllers/ajax/question_controller_spec.rb b/spec/controllers/ajax/question_controller_spec.rb
index 021a4172..077ace60 100644
--- a/spec/controllers/ajax/question_controller_spec.rb
+++ b/spec/controllers/ajax/question_controller_spec.rb
@@ -14,8 +14,8 @@ describe Ajax::QuestionController, :ajax_controller, type: :controller do
if check_for_inbox
it "adds the question to the target users' inbox" do
- expect { subject }.to(change { target_user.inboxes.count }.by(1))
- expect(target_user.inboxes.last.question.content).to eq(question_content)
+ expect { subject }.to(change { target_user.inbox_entries.count }.by(1))
+ expect(target_user.inbox_entries.last.question.content).to eq(question_content)
end
end
@@ -29,7 +29,7 @@ describe Ajax::QuestionController, :ajax_controller, type: :controller do
if check_for_inbox
it "does not add the question to the target users' inbox" do
- expect { subject }.not_to(change { target_user.inboxes.count })
+ expect { subject }.not_to(change { target_user.inbox_entries.count })
end
end
@@ -42,27 +42,31 @@ describe Ajax::QuestionController, :ajax_controller, type: :controller do
end
it "does not add the question to the target users' inbox" do
- expect { subject }.not_to(change { target_user.inboxes.count })
+ expect { subject }.not_to(change { target_user.inbox_entries.count })
end
include_examples "returns the expected response"
end
- shared_examples "enqueues a QuestionWorker job" do |_expected_rcpt|
- it "enqueues a QuestionWorker job" do
- allow(QuestionWorker).to receive(:perform_async)
+ shared_examples "enqueues SendToInboxJob jobs" do
+ it "enqueues a SendToInboxJob job" do
+ allow(SendToInboxJob).to receive(:perform_bulk)
subject
- expect(QuestionWorker).to have_received(:perform_async).with(user.id, Question.last.id)
+ question_id = Question.last.id
+ bulk_args = followers.map { |f| [f.id, question_id] }
+ expect(SendToInboxJob).to have_received(:perform_bulk).with(bulk_args)
end
include_examples "returns the expected response"
end
- shared_examples "does not enqueue a QuestionWorker job" do
- it "does not enqueue a QuestionWorker job" do
- allow(QuestionWorker).to receive(:perform_async)
+ shared_examples "does not enqueue a SendToInboxJob job" do
+ it "does not enqueue a SendToInboxJob job" do
+ allow(SendToInboxJob).to receive(:perform_async)
+ allow(SendToInboxJob).to receive(:perform_bulk)
subject
- expect(QuestionWorker).not_to have_received(:perform_async)
+ expect(SendToInboxJob).not_to have_received(:perform_async)
+ expect(SendToInboxJob).not_to have_received(:perform_bulk)
end
include_examples "returns the expected response"
@@ -73,9 +77,11 @@ describe Ajax::QuestionController, :ajax_controller, type: :controller do
{
question: question_content,
anonymousQuestion: anonymous_question,
- rcpt: rcpt
+ rcpt:,
+ sendToOwnInbox: send_to_own_inbox,
}
end
+ let(:send_to_own_inbox) { "false" }
subject { post(:create, params: params) }
@@ -94,6 +100,7 @@ describe Ajax::QuestionController, :ajax_controller, type: :controller do
context "when rcpt is a valid user" do
let(:rcpt) { target_user.id }
+ let(:send_to_own_inbox) { false }
context "when user allows anonymous questions" do
let(:user_allows_anonymous_questions) { true }
@@ -194,13 +201,18 @@ describe Ajax::QuestionController, :ajax_controller, type: :controller do
context "when rcpt is followers" do
let(:rcpt) { "followers" }
+ let(:followers) { FactoryBot.create_list(:user, 3) }
+
+ before do
+ followers.each { |follower| follower.follow(user) }
+ end
context "when anonymousQuestion is true" do
let(:anonymous_question) { "true" }
let(:expected_question_anonymous) { false }
include_examples "creates the question", false
- include_examples "enqueues a QuestionWorker job", "followers"
+ include_examples "enqueues SendToInboxJob jobs"
end
context "when anonymousQuestion is false" do
@@ -208,12 +220,43 @@ describe Ajax::QuestionController, :ajax_controller, type: :controller do
let(:expected_question_anonymous) { false }
include_examples "creates the question", false
- include_examples "enqueues a QuestionWorker job", "followers"
+ include_examples "enqueues SendToInboxJob jobs"
+ end
+
+ context "when sendToOwnInbox is true" do
+ let(:anonymous_question) { "false" }
+ let(:expected_question_anonymous) { false }
+ let(:user_allows_anonymous_questions) { false }
+ let(:send_to_own_inbox) { "true" }
+
+ include_examples "creates the question", false
+
+ it "sends question to the current user" do
+ allow(SendToInboxJob).to receive(:perform_async)
+ subject
+ expect(SendToInboxJob).to have_received(:perform_async).with(user.id, Question.last.id)
+ end
+ end
+
+ context "when sendToOwnInbox is false" do
+ let(:anonymous_question) { "false" }
+ let(:expected_question_anonymous) { false }
+ let(:user_allows_anonymous_questions) { false }
+ let(:send_to_own_inbox) { "false" }
+
+ include_examples "creates the question", false
+
+ it "sends question to the current user" do
+ allow(SendToInboxJob).to receive(:perform_async)
+ subject
+ expect(SendToInboxJob).not_to have_received(:perform_async).with(user.id, Question.last.id)
+ end
end
end
context "when rcpt is an invalid value" do
let(:rcpt) { "tripmeister_eder" }
+ let(:send_to_own_inbox) { false }
let(:anonymous_question) { "false" }
let(:expected_response) do
{
@@ -230,6 +273,7 @@ describe Ajax::QuestionController, :ajax_controller, type: :controller do
context "when rcpt is a non-existent user" do
let(:rcpt) { "-1" }
+ let(:send_to_own_inbox) { false }
let(:anonymous_question) { "false" }
let(:expected_response) do
{
@@ -375,7 +419,7 @@ describe Ajax::QuestionController, :ajax_controller, type: :controller do
}
end
- include_examples "does not enqueue a QuestionWorker job"
+ include_examples "does not enqueue a SendToInboxJob job"
end
context "when rcpt is an invalid value" do
diff --git a/spec/controllers/ajax/smile_controller_spec.rb b/spec/controllers/ajax/smile_controller_spec.rb
deleted file mode 100644
index 9d6d0506..00000000
--- a/spec/controllers/ajax/smile_controller_spec.rb
+++ /dev/null
@@ -1,340 +0,0 @@
-# coding: utf-8
-# frozen_string_literal: true
-
-require "rails_helper"
-
-describe Ajax::SmileController, :ajax_controller, type: :controller do
- describe "#create" do
- let(:params) do
- {
- id: answer_id
- }.compact
- end
- let(:answer) { FactoryBot.create(:answer, user: user) }
-
- subject { post(:create, params: params) }
-
- context "when user is signed in" do
- before(:each) { sign_in(user) }
-
- context "when answer exists" do
- let(:answer_id) { answer.id }
- let(:expected_response) do
- {
- "success" => true,
- "status" => "okay",
- "message" => anything
- }
- end
-
- it "creates a smile to the answer" do
- expect { subject }.to(change { Appendable::Reaction.count }.by(1))
- expect(answer.reload.smiles.ids).to include(Appendable::Reaction.last.id)
- end
-
- include_examples "returns the expected response"
- end
-
- context "when answer does not exist" do
- let(:answer_id) { "nein!" }
-
- let(:expected_response) do
- {
- "success" => false,
- "status" => anything,
- "message" => anything
- }
- end
-
- it "does not create a smile" do
- expect { subject }.not_to(change { Appendable::Reaction.count })
- end
-
- include_examples "returns the expected response"
- end
-
- context "when some parameters are missing" do
- let(:answer_id) { nil }
-
- let(:expected_response) do
- {
- "success" => false,
- "status" => "parameter_error",
- "message" => anything
- }
- end
-
- include_examples "returns the expected response"
- end
- end
-
- context "when user is not signed in" do
- let(:answer_id) { answer.id }
-
- let(:expected_response) do
- {
- "success" => false,
- "status" => "fail",
- "message" => anything
- }
- end
-
- include_examples "returns the expected response"
- end
-
- context "when blocked by the answer's author" do
- let(:other_user) { FactoryBot.create(:user) }
- let(:answer) { FactoryBot.create(:answer, user: other_user) }
- let(:answer_id) { answer.id }
-
- before do
- other_user.block(user)
- end
-
- let(:expected_response) do
- {
- "success" => false,
- "status" => "fail",
- "message" => anything
- }
- end
-
- it "does not create a smile" do
- expect { subject }.not_to(change { Appendable::Reaction.count })
- end
-
- include_examples "returns the expected response"
- end
-
- context "when blocking the answer's author" do
- let(:other_user) { FactoryBot.create(:user) }
- let(:answer) { FactoryBot.create(:answer, user: user) }
- let(:answer_id) { answer.id }
-
- before do
- user.block(other_user)
- end
-
- let(:expected_response) do
- {
- "success" => false,
- "status" => "fail",
- "message" => anything
- }
- end
-
- it "does not create a smile" do
- expect { subject }.not_to(change { Appendable::Reaction.count })
- end
-
- include_examples "returns the expected response"
- end
- end
-
- describe "#destroy" do
- let(:answer) { FactoryBot.create(:answer, user: user) }
- let(:smile) { FactoryBot.create(:smile, user: user, parent: answer) }
- let(:answer_id) { answer.id }
-
- let(:params) do
- {
- id: answer_id
- }
- end
-
- subject { delete(:destroy, params: params) }
-
- context "when user is signed in" do
- before(:each) { sign_in(user) }
-
- context "when the smile exists" do
- # ensure we already have it in the db
- before(:each) { smile }
-
- let(:expected_response) do
- {
- "success" => true,
- "status" => "okay",
- "message" => anything
- }
- end
-
- it "deletes the smile" do
- expect { subject }.to(change { Appendable::Reaction.count }.by(-1))
- end
-
- include_examples "returns the expected response"
- end
-
- context "when the smile does not exist" do
- let(:answer_id) { "sonic_the_hedgehog" }
-
- let(:expected_response) do
- {
- "success" => false,
- "status" => anything,
- "message" => anything
- }
- end
-
- include_examples "returns the expected response"
- end
- end
-
- context "when user is not signed in" do
- let(:expected_response) do
- {
- "success" => false,
- "status" => "fail",
- "message" => anything
- }
- end
-
- include_examples "returns the expected response"
- end
- end
-
- describe "#create_comment" do
- let(:params) do
- {
- id: comment_id
- }.compact
- end
- let(:answer) { FactoryBot.create(:answer, user: user) }
- let(:comment) { FactoryBot.create(:comment, user: user, answer: answer) }
-
- subject { post(:create_comment, params: params) }
-
- context "when user is signed in" do
- before(:each) { sign_in(user) }
-
- context "when comment exists" do
- let(:comment_id) { comment.id }
- let(:expected_response) do
- {
- "success" => true,
- "status" => "okay",
- "message" => anything
- }
- end
-
- it "creates a smile to the comment" do
- expect { subject }.to(change { Appendable::Reaction.count }.by(1))
- expect(comment.reload.smiles.ids).to include(Appendable::Reaction.last.id)
- end
-
- include_examples "returns the expected response"
- end
-
- context "when comment does not exist" do
- let(:comment_id) { "nein!" }
-
- let(:expected_response) do
- {
- "success" => false,
- "status" => anything,
- "message" => anything
- }
- end
-
- it "does not create a smile" do
- expect { subject }.not_to(change { Appendable::Reaction.count })
- end
-
- include_examples "returns the expected response"
- end
-
- context "when some parameters are missing" do
- let(:comment_id) { nil }
-
- let(:expected_response) do
- {
- "success" => false,
- "status" => "parameter_error",
- "message" => anything
- }
- end
-
- include_examples "returns the expected response"
- end
- end
-
- context "when user is not signed in" do
- let(:comment_id) { comment.id }
-
- let(:expected_response) do
- {
- "success" => false,
- "status" => "fail",
- "message" => anything
- }
- end
-
- include_examples "returns the expected response"
- end
- end
-
- describe "#destroy_comment" do
- let(:answer) { FactoryBot.create(:answer, user: user) }
- let(:comment) { FactoryBot.create(:comment, user: user, answer: answer) }
- let(:comment_smile) { FactoryBot.create(:comment_smile, user: user, parent: comment) }
- let(:comment_id) { comment.id }
-
- let(:params) do
- {
- id: comment_id
- }
- end
-
- subject { delete(:destroy_comment, params: params) }
-
- context "when user is signed in" do
- before(:each) { sign_in(user) }
-
- context "when the smile exists" do
- # ensure we already have it in the db
- before(:each) { comment_smile }
-
- let(:expected_response) do
- {
- "success" => true,
- "status" => "okay",
- "message" => anything
- }
- end
-
- it "deletes the smile" do
- expect { subject }.to(change { Appendable::Reaction.count }.by(-1))
- end
-
- include_examples "returns the expected response"
- end
-
- context "when the smile does not exist" do
- let(:answer_id) { "sonic_the_hedgehog" }
-
- let(:expected_response) do
- {
- "success" => false,
- "status" => anything,
- "message" => anything
- }
- end
-
- include_examples "returns the expected response"
- end
- end
-
- context "when user is not signed in" do
- let(:expected_response) do
- {
- "success" => false,
- "status" => "fail",
- "message" => anything
- }
- end
-
- include_examples "returns the expected response"
- end
- end
-end
diff --git a/spec/controllers/anonymous_block_controller_spec.rb b/spec/controllers/anonymous_block_controller_spec.rb
index a1619a51..b024651c 100644
--- a/spec/controllers/anonymous_block_controller_spec.rb
+++ b/spec/controllers/anonymous_block_controller_spec.rb
@@ -15,7 +15,7 @@ describe AnonymousBlockController, type: :controller do
context "when all required parameters are given" do
let(:question) { FactoryBot.create(:question, author_identifier: "someidentifier") }
- let!(:inbox) { FactoryBot.create(:inbox, user:, question:) }
+ let!(:inbox) { FactoryBot.create(:inbox_entry, user:, question:) }
let(:params) do
{ question: question.id }
end
@@ -50,7 +50,7 @@ describe AnonymousBlockController, type: :controller do
context "when blocking a user globally" do
let(:question) { FactoryBot.create(:question, author_identifier: "someidentifier") }
- let!(:inbox) { FactoryBot.create(:inbox, user:, question:) }
+ let!(:inbox) { FactoryBot.create(:inbox_entry, user:, question:) }
let(:params) do
{ question: question.id, global: "true" }
end
diff --git a/spec/controllers/comments/reactions_controller_spec.rb b/spec/controllers/comments/reactions_controller_spec.rb
new file mode 100644
index 00000000..083b89b5
--- /dev/null
+++ b/spec/controllers/comments/reactions_controller_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe Comments::ReactionsController, type: :controller do
+ describe "#index" do
+ let(:answer_author) { FactoryBot.create(:user) }
+ let(:answer) { FactoryBot.create(:answer, user: answer_author) }
+ let(:commenter) { FactoryBot.create(:user) }
+ let(:comment) { FactoryBot.create(:comment, answer:, user: commenter) }
+
+ context "a regular web navigation request" do
+ subject { get :index, params: { username: commenter.screen_name, id: comment.id } }
+
+ it "should redirect to the answer page" do
+ subject
+
+ expect(response).to redirect_to answer_path(username: answer_author.screen_name, id: answer.id)
+ end
+ end
+
+ context "a Turbo Frame request" do
+ subject { get :index, params: { username: commenter.screen_name, id: comment.id } }
+
+ it "renders the index template" do
+ @request.headers["Turbo-Frame"] = "some_id"
+
+ subject
+
+ expect(response).to render_template(:index)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/comments_controller_spec.rb b/spec/controllers/comments_controller_spec.rb
new file mode 100644
index 00000000..209c1886
--- /dev/null
+++ b/spec/controllers/comments_controller_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe CommentsController, type: :controller do
+ describe "#index" do
+ shared_examples_for "succeeds" do
+ it "returns the correct response" do
+ subject
+ expect(response).to have_rendered :index
+ expect(response).to have_http_status(200)
+ expect(assigns(:comments)).to eq(comments)
+ expect(assigns(:comments)).to_not include(unrelated_comment)
+ end
+ end
+
+ subject { get :index, params: { username: answer_author.screen_name, id: answer.id } }
+
+ let(:answer_author) { FactoryBot.create(:user) }
+ let(:answer) { FactoryBot.create(:answer, user: answer_author) }
+ let(:commenter) { FactoryBot.create(:user) }
+ let!(:comments) { FactoryBot.create_list(:comment, num_comments, answer:, user: commenter) }
+ let!(:unrelated_comment) do
+ FactoryBot.create(:comment,
+ answer: FactoryBot.create(:answer, user: FactoryBot.create(:user)),
+ user: commenter,)
+ end
+
+ [0, 1, 5, 30].each do |num_comments|
+ context "#{num_comments} comments" do
+ let(:num_comments) { num_comments }
+
+ include_examples "succeeds"
+ end
+ end
+ end
+end
diff --git a/spec/controllers/concerns/turbo_streamable_spec.rb b/spec/controllers/concerns/turbo_streamable_spec.rb
index 3a2c9c60..19e0079c 100644
--- a/spec/controllers/concerns/turbo_streamable_spec.rb
+++ b/spec/controllers/concerns/turbo_streamable_spec.rb
@@ -6,7 +6,7 @@ describe TurboStreamable, type: :controller do
controller do
include TurboStreamable
- turbo_stream_actions :create, :blocked, :not_found
+ turbo_stream_actions :create, :blocked, :not_found, :invalid_record
def create
params.require :message
@@ -25,6 +25,10 @@ describe TurboStreamable, type: :controller do
def not_found
raise ActiveRecord::RecordNotFound
end
+
+ def invalid_record
+ MuteRule.create!(muted_phrase: "", user: FactoryBot.create(:user))
+ end
end
before do
@@ -32,6 +36,7 @@ describe TurboStreamable, type: :controller do
get "create" => "anonymous#create"
get "blocked" => "anonymous#blocked"
get "not_found" => "anonymous#not_found"
+ get "invalid_record" => "anonymous#invalid_record"
end
end
@@ -68,4 +73,5 @@ describe TurboStreamable, type: :controller do
it_behaves_like "it returns a toast as Turbo Stream response", :create, "Message is required"
it_behaves_like "it returns a toast as Turbo Stream response", :blocked, "You have been blocked from performing this request"
it_behaves_like "it returns a toast as Turbo Stream response", :not_found, "Record not found"
+ it_behaves_like "it returns a toast as Turbo Stream response", :invalid_record, "too short"
end
diff --git a/spec/controllers/inbox_controller_spec.rb b/spec/controllers/inbox_controller_spec.rb
index 94a8accf..689a70ae 100644
--- a/spec/controllers/inbox_controller_spec.rb
+++ b/spec/controllers/inbox_controller_spec.rb
@@ -46,7 +46,7 @@ describe InboxController, type: :controller do
end
context "when inbox has an amount of questions less than page size" do
- let!(:inbox_entry) { Inbox.create(user:, new: true, question: FactoryBot.create(:question)) }
+ let!(:inbox_entry) { InboxEntry.create(user:, new: true, question: FactoryBot.create(:question)) }
include_examples "sets the expected ivars" do
let(:expected_assigns) do
@@ -65,12 +65,7 @@ describe InboxController, type: :controller do
expect { subject }.to change { inbox_entry.reload.new? }.from(true).to(false)
end
- it "updates the the timestamp used for caching" do
- user.update(inbox_updated_at: original_inbox_updated_at)
- travel 1.second do
- expect { subject }.to change { user.reload.inbox_updated_at.floor }.from(original_inbox_updated_at.floor).to(Time.now.utc.floor)
- end
- end
+ include_examples "touches user timestamp", :inbox_updated_at
context "when requested the turbo stream format" do
subject { get :show, format: :turbo_stream }
@@ -84,13 +79,13 @@ describe InboxController, type: :controller do
context "when inbox has an amount of questions more than page size" do
let(:inbox_entry_fillers_page1) do
# 9 times => 1 entry less than default page size
- 9.times.map { Inbox.create(user:, question: FactoryBot.create(:question)) }
+ 9.times.map { InboxEntry.create(user:, question: FactoryBot.create(:question)) }
end
- let(:last_inbox_entry_page1) { Inbox.create(user:, question: FactoryBot.create(:question)) }
+ let(:last_inbox_entry_page1) { InboxEntry.create(user:, question: FactoryBot.create(:question)) }
let(:inbox_entry_fillers_page2) do
- 5.times.map { Inbox.create(user:, question: FactoryBot.create(:question)) }
+ 5.times.map { InboxEntry.create(user:, question: FactoryBot.create(:question)) }
end
- let(:last_inbox_entry_page2) { Inbox.create(user:, question: FactoryBot.create(:question)) }
+ let(:last_inbox_entry_page2) { InboxEntry.create(user:, question: FactoryBot.create(:question)) }
before do
# create inbox entries in reverse so pagination works as expected
@@ -108,7 +103,7 @@ describe InboxController, type: :controller do
more_data_available: true,
inbox_count: 16,
delete_id: "ib-delete-all",
- disabled: nil
+ disabled: nil,
}
end
end
@@ -124,7 +119,7 @@ describe InboxController, type: :controller do
more_data_available: false,
inbox_count: 16,
delete_id: "ib-delete-all",
- disabled: nil
+ disabled: nil,
}
end
end
@@ -136,52 +131,23 @@ describe InboxController, type: :controller do
let!(:unrelated_user) { FactoryBot.create(:user) }
let!(:generic_inbox_entry1) do
- Inbox.create(
+ InboxEntry.create(
user:,
question: FactoryBot.create(
:question,
user: unrelated_user,
- author_is_anonymous: false
- )
+ author_is_anonymous: false,
+ ),
)
end
- let!(:generic_inbox_entry2) { Inbox.create(user:, question: FactoryBot.create(:question)) }
+ let!(:generic_inbox_entry2) { InboxEntry.create(user:, question: FactoryBot.create(:question)) }
subject { get :show, params: { author: author_param } }
- context "with a nonexisting screen name" do
- let(:author_param) { "xXx420MegaGamer2003xXx" }
-
- it "sets the error flash" do
- subject
- expect(flash[:error]).to eq "No user with the name @xXx420MegaGamer2003xXx found, showing entries from all users instead!"
- end
-
- include_examples "sets the expected ivars" do
- let(:expected_assigns) do
- {
- inbox: [generic_inbox_entry2, generic_inbox_entry1],
- inbox_last_id: generic_inbox_entry1.id,
- more_data_available: false,
- inbox_count: 2,
- delete_id: "ib-delete-all",
- disabled: nil
- }
- end
- end
- end
-
context "with an existing screen name" do
let(:author_param) { other_user.screen_name }
context "with no questions from the other user in the inbox" do
- it { is_expected.to redirect_to inbox_path }
-
- it "sets the info flash" do
- subject
- expect(flash[:info]).to eq "No questions from @#{other_user.screen_name} found, showing entries from all users instead!"
- end
-
include_examples "sets the expected ivars" do
# these are the ivars set before the redirect happened
let(:expected_assigns) do
@@ -189,7 +155,7 @@ describe InboxController, type: :controller do
inbox: [],
inbox_last_id: nil,
more_data_available: false,
- inbox_count: 0
+ inbox_count: 0,
}
end
end
@@ -197,23 +163,16 @@ describe InboxController, type: :controller do
context "with no non-anonymous questions from the other user in the inbox" do
let!(:anonymous_inbox_entry) do
- Inbox.create(
+ InboxEntry.create(
user:,
question: FactoryBot.create(
:question,
user: other_user,
- author_is_anonymous: true
- )
+ author_is_anonymous: true,
+ ),
)
end
- it { is_expected.to redirect_to inbox_path }
-
- it "sets the info flash" do
- subject
- expect(flash[:info]).to eq "No questions from @#{other_user.screen_name} found, showing entries from all users instead!"
- end
-
include_examples "sets the expected ivars" do
# these are the ivars set before the redirect happened
let(:expected_assigns) do
@@ -221,7 +180,7 @@ describe InboxController, type: :controller do
inbox: [],
inbox_last_id: nil,
more_data_available: false,
- inbox_count: 0
+ inbox_count: 0,
}
end
end
@@ -229,23 +188,23 @@ describe InboxController, type: :controller do
context "with both non-anonymous and anonymous questions from the other user in the inbox" do
let!(:non_anonymous_inbox_entry) do
- Inbox.create(
+ InboxEntry.create(
user:,
question: FactoryBot.create(
:question,
user: other_user,
- author_is_anonymous: false
- )
+ author_is_anonymous: false,
+ ),
)
end
let!(:anonymous_inbox_entry) do
- Inbox.create(
+ InboxEntry.create(
user:,
question: FactoryBot.create(
:question,
user: other_user,
- author_is_anonymous: true
- )
+ author_is_anonymous: true,
+ ),
)
end
@@ -257,13 +216,75 @@ describe InboxController, type: :controller do
more_data_available: false,
inbox_count: 1,
delete_id: "ib-delete-all-author",
- disabled: nil
+ disabled: nil,
}
end
end
end
end
end
+
+ context "when passed the anonymous param" do
+ let!(:other_user) { FactoryBot.create(:user) }
+ let!(:generic_inbox_entry) do
+ InboxEntry.create(
+ user:,
+ question: FactoryBot.create(
+ :question,
+ user: other_user,
+ author_is_anonymous: false,
+ ),
+ )
+ end
+
+ let!(:inbox_entry_fillers) do
+ # 9 times => 1 entry less than default page size
+ 9.times.map { InboxEntry.create(user:, question: FactoryBot.create(:question, author_is_anonymous: true)) }
+ end
+
+ subject { get :show, params: { anonymous: true } }
+
+ include_examples "sets the expected ivars" do
+ let(:expected_assigns) do
+ {
+ inbox: [*inbox_entry_fillers.reverse],
+ more_data_available: false,
+ inbox_count: 9,
+ }
+ end
+ end
+ end
+
+ context "when passed the anonymous and the author param" do
+ let!(:other_user) { FactoryBot.create(:user) }
+ let!(:generic_inbox_entry) do
+ InboxEntry.create(
+ user:,
+ question: FactoryBot.create(
+ :question,
+ user: other_user,
+ author_is_anonymous: false,
+ ),
+ )
+ end
+
+ let!(:inbox_entry_fillers) do
+ # 9 times => 1 entry less than default page size
+ 9.times.map { InboxEntry.create(user:, question: FactoryBot.create(:question, author_is_anonymous: true)) }
+ end
+
+ subject { get :show, params: { anonymous: true, author: "some_name" } }
+
+ include_examples "sets the expected ivars" do
+ let(:expected_assigns) do
+ {
+ inbox: [],
+ more_data_available: false,
+ inbox_count: 0,
+ }
+ end
+ end
+ end
end
end
@@ -278,8 +299,10 @@ describe InboxController, type: :controller do
before(:each) { sign_in(user) }
it "creates an inbox entry" do
- expect { subject }.to(change { user.inboxes.count }.by(1))
+ expect { subject }.to(change { user.inbox_entries.count }.by(1))
end
+
+ include_examples "touches user timestamp", :inbox_updated_at
end
end
end
diff --git a/spec/controllers/modal_controller_spec.rb b/spec/controllers/modal_controller_spec.rb
new file mode 100644
index 00000000..d927173a
--- /dev/null
+++ b/spec/controllers/modal_controller_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe ModalController, type: :controller do
+ describe "#close" do
+ context "a regular web navigation request" do
+ subject { get :close }
+
+ it "should redirect to the root page" do
+ subject
+
+ expect(response).to redirect_to root_path
+ end
+ end
+
+ context "a Turbo Frame request" do
+ subject { get :close }
+
+ it "renders the show_reaction template" do
+ @request.headers["Turbo-Frame"] = "some_id"
+
+ subject
+
+ expect(response.body).to include('')
+ end
+ end
+ end
+end
diff --git a/spec/controllers/moderation/inbox_controller_spec.rb b/spec/controllers/moderation/inbox_controller_spec.rb
index 0cf89d2d..70b8f7ed 100644
--- a/spec/controllers/moderation/inbox_controller_spec.rb
+++ b/spec/controllers/moderation/inbox_controller_spec.rb
@@ -7,7 +7,7 @@ describe Moderation::InboxController do
subject { get :index, params: params }
let(:target_user) { FactoryBot.create(:user) }
- let!(:inboxes) { FactoryBot.create_list(:inbox, 60, user: target_user) }
+ let!(:inboxes) { FactoryBot.create_list(:inbox_entry, 60, user: target_user) }
let(:params) { { user: target_user.screen_name } }
context "moderator signed in" do
diff --git a/spec/controllers/moderation/reports_controller_spec.rb b/spec/controllers/moderation/reports_controller_spec.rb
index 0a845606..daed0ab8 100644
--- a/spec/controllers/moderation/reports_controller_spec.rb
+++ b/spec/controllers/moderation/reports_controller_spec.rb
@@ -6,15 +6,116 @@ describe Moderation::ReportsController, type: :controller do
let(:user) { FactoryBot.create :user, roles: ["moderator"] }
describe "#index" do
- subject { get :index }
+ shared_examples_for "sets the expected ivars" do
+ let(:expected_assigns) { {} }
- before do
- sign_in user
+ it "sets the expected ivars" do
+ subject
+
+ expected_assigns.each do |name, value|
+ expect(assigns[name]).to eq(value)
+ end
+ end
end
- it "renders the moderation/reports/index template" do
- subject
- expect(response).to render_template("moderation/reports/index")
+ context "template rendering" do
+ let(:other_user) { FactoryBot.create :user }
+ let(:report) { Report.create(user:, target_id: other_user.id, type: "Reports::User") }
+
+ subject { get :index }
+
+ before do
+ report
+ sign_in user
+ end
+
+ it "renders the moderation/reports/index template" do
+ subject
+ expect(response).to render_template("moderation/reports/index")
+ end
+
+ include_examples "sets the expected ivars" do
+ let(:expected_assigns) do
+ {
+ reports: [report],
+ reports_last_id: report.id,
+ }
+ end
+ end
+ end
+
+ context "filtering for target users" do
+ let(:other_user) { FactoryBot.create :user }
+ let(:question) { FactoryBot.create :question }
+ let(:report) { Report.create(user:, target_id: other_user.id, target_user_id: other_user.id, type: "Reports::User") }
+ let(:report2) { Report.create(user:, target_id: question.id, target_user_id: nil, type: "Reports::Question") }
+
+ subject { get :index, params: { target_user: other_user.screen_name } }
+
+ before do
+ report
+ report2
+ sign_in user
+ end
+
+ include_examples "sets the expected ivars" do
+ let(:expected_assigns) do
+ {
+ reports: [report],
+ reports_last_id: report.id,
+ }
+ end
+ end
+ end
+
+ context "filtering for users" do
+ let(:report_user) { FactoryBot.create :user }
+ let(:other_user) { FactoryBot.create :user }
+ let(:question) { FactoryBot.create :question }
+ let(:report) { Report.create(user:, target_id: other_user.id, target_user_id: other_user.id, type: "Reports::User") }
+ let(:report2) { Report.create(user: report_user, target_id: question.id, target_user_id: nil, type: "Reports::Question") }
+
+ subject { get :index, params: { user: report_user.screen_name } }
+
+ before do
+ report
+ report2
+ sign_in user
+ end
+
+ include_examples "sets the expected ivars" do
+ let(:expected_assigns) do
+ {
+ reports: [report2],
+ reports_last_id: report2.id,
+ }
+ end
+ end
+ end
+
+ context "filtering for type" do
+ let(:report_user) { FactoryBot.create :user }
+ let(:other_user) { FactoryBot.create :user }
+ let(:question) { FactoryBot.create :question }
+ let(:report) { Report.create(user:, target_id: other_user.id, target_user_id: other_user.id, type: "Reports::User") }
+ let(:report2) { Report.create(user: report_user, target_id: question.id, target_user_id: nil, type: "Reports::Question") }
+
+ subject { get :index, params: { type: "question" } }
+
+ before do
+ report
+ report2
+ sign_in user
+ end
+
+ include_examples "sets the expected ivars" do
+ let(:expected_assigns) do
+ {
+ reports: [report2],
+ reports_last_id: report2.id,
+ }
+ end
+ end
end
end
end
diff --git a/spec/controllers/reactions_controller_spec.rb b/spec/controllers/reactions_controller_spec.rb
new file mode 100644
index 00000000..1f391e40
--- /dev/null
+++ b/spec/controllers/reactions_controller_spec.rb
@@ -0,0 +1,211 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe ReactionsController, type: :controller do
+ render_views
+
+ describe "#index" do
+ shared_examples_for "succeeds" do
+ it "returns the correct response" do
+ subject
+ expect(response).to have_rendered :index
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ subject { get :index, params: { username: answer_author.screen_name, id: answer.id } }
+
+ let(:answer_author) { FactoryBot.create(:user) }
+ let(:answer) { FactoryBot.create(:answer, user: answer_author) }
+ let!(:reactees) { FactoryBot.create_list(:user, num_comments) }
+
+ [0, 1, 5, 30].each do |num_comments|
+ context "#{num_comments} reactions" do
+ let(:num_comments) { num_comments }
+
+ before do
+ reactees.each { _1.smile(answer) }
+ end
+
+ include_examples "succeeds"
+ end
+ end
+ end
+
+ describe "#create" do
+ let(:user) { FactoryBot.create(:user) }
+
+ context "target type is Answer" do
+ let(:params) do
+ {
+ username: user.screen_name,
+ id: answer_id,
+ type: "Answer",
+ }.compact
+ end
+ let(:answer) { FactoryBot.create(:answer, user:) }
+
+ subject { post(:create, params:, format: :turbo_stream) }
+
+ context "when user is signed in" do
+ before(:each) { sign_in(user) }
+
+ context "when answer exists" do
+ let(:answer_id) { answer.id }
+
+ it "creates a reaction to the answer" do
+ expect { subject }.to(change { Reaction.count }.by(1))
+ expect(answer.reload.smiles.ids).to include(Reaction.last.id)
+ end
+ end
+
+ context "when answer does not exist" do
+ let(:answer_id) { "nein!" }
+
+ it "does not create a reaction" do
+ expect { subject }.not_to(change { Reaction.count })
+ end
+ end
+ end
+
+ context "when blocked by the answer's author" do
+ let(:other_user) { FactoryBot.create(:user) }
+ let(:answer) { FactoryBot.create(:answer, user: other_user) }
+ let(:answer_id) { answer.id }
+
+ before do
+ other_user.block(user)
+ end
+
+ it "does not create a reaction" do
+ expect { subject }.not_to(change { Reaction.count })
+ end
+ end
+
+ context "when blocking the answer's author" do
+ let(:other_user) { FactoryBot.create(:user) }
+ let(:answer) { FactoryBot.create(:answer, user:) }
+ let(:answer_id) { answer.id }
+
+ before do
+ user.block(other_user)
+ end
+
+ it "does not create a reaction" do
+ expect { subject }.not_to(change { Reaction.count })
+ end
+ end
+ end
+
+ context "target type is Comment" do
+ let(:params) do
+ {
+ username: user.screen_name,
+ id: comment_id,
+ type: "Comment",
+ }.compact
+ end
+ let(:answer) { FactoryBot.create(:answer, user:) }
+ let(:comment) { FactoryBot.create(:comment, user:, answer:) }
+
+ subject { post(:create, params:, format: :turbo_stream) }
+
+ context "when user is signed in" do
+ before(:each) { sign_in(user) }
+
+ context "when comment exists" do
+ let(:comment_id) { comment.id }
+
+ it "creates a smile to the comment" do
+ expect { subject }.to(change { Reaction.count }.by(1))
+ expect(comment.reload.smiles.ids).to include(Reaction.last.id)
+ end
+ end
+
+ context "when comment does not exist" do
+ let(:comment_id) { "nein!" }
+
+ it "does not create a smile" do
+ expect { subject }.not_to(change { Reaction.count })
+ end
+ end
+ end
+ end
+ end
+
+ describe "#destroy" do
+ let(:user) { FactoryBot.create(:user) }
+
+ context "target type is Answer" do
+ let(:answer) { FactoryBot.create(:answer, user:) }
+ let(:smile) { FactoryBot.create(:smile, user:, parent: answer) }
+ let(:answer_id) { answer.id }
+
+ let(:params) do
+ {
+ username: user.screen_name,
+ id: answer_id,
+ type: "Answer",
+ }
+ end
+
+ subject { delete(:destroy, params:, format: :turbo_stream) }
+
+ context "when user is signed in" do
+ before(:each) { sign_in(user) }
+
+ context "when the smile exists" do
+ # ensure we already have it in the db
+ before(:each) { smile }
+
+ it "deletes the reaction" do
+ expect { subject }.to(change { Reaction.count }.by(-1))
+ end
+ end
+
+ context "when the reaction does not exist" do
+ let(:answer_id) { "sonic_the_hedgehog" }
+
+ include_examples "turbo does not succeed", "Record not found"
+ end
+ end
+ end
+
+ context "target type is Comment" do
+ let(:answer) { FactoryBot.create(:answer, user:) }
+ let(:comment) { FactoryBot.create(:comment, user:, answer:) }
+ let(:comment_smile) { FactoryBot.create(:comment_smile, user:, parent: comment) }
+ let(:comment_id) { comment.id }
+
+ let(:params) do
+ {
+ username: user.screen_name,
+ id: comment_id,
+ type: "Comment",
+ }
+ end
+
+ subject { delete(:destroy, params:, format: :turbo_stream) }
+
+ context "when user is signed in" do
+ before(:each) { sign_in(user) }
+
+ context "when the reaction exists" do
+ # ensure we already have it in the db
+ before(:each) { comment_smile }
+
+ it "deletes the reaction" do
+ expect { subject }.to(change { Reaction.count }.by(-1))
+ end
+ end
+
+ context "when the reaction does not exist" do
+ let(:answer_id) { "sonic_the_hedgehog" }
+
+ include_examples "turbo does not succeed", "Record not found"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/ajax/relationship_controller_spec.rb b/spec/controllers/relationships_controller_spec.rb
similarity index 82%
rename from spec/controllers/ajax/relationship_controller_spec.rb
rename to spec/controllers/relationships_controller_spec.rb
index 11d2ddbd..9642b34b 100644
--- a/spec/controllers/ajax/relationship_controller_spec.rb
+++ b/spec/controllers/relationships_controller_spec.rb
@@ -3,11 +3,13 @@
require "rails_helper"
-describe Ajax::RelationshipController, type: :controller do
+describe RelationshipsController, type: :controller do
+ render_views
+
shared_examples_for "params is empty" do
let(:params) { {} }
- include_examples "ajax does not succeed", "is required"
+ include_examples "turbo does not succeed", "is required"
end
let!(:user) { FactoryBot.create(:user) }
@@ -20,13 +22,13 @@ describe Ajax::RelationshipController, type: :controller do
context "screen_name does not exist" do
let(:screen_name) { "peter-witzig" }
- include_examples "ajax does not succeed", "not found"
+ include_examples "turbo does not succeed", "not found"
end
context "screen_name is current user" do
let(:screen_name) { user.screen_name }
- include_examples "ajax does not succeed", "yourself"
+ include_examples "turbo does not succeed", "yourself"
end
context "screen_name is different from current_user" do
@@ -41,9 +43,9 @@ describe Ajax::RelationshipController, type: :controller do
let(:type) { "Sauerkraut" }
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"
@@ -82,7 +84,7 @@ describe Ajax::RelationshipController, type: :controller do
let(:type) { "dick" }
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
@@ -110,15 +112,15 @@ describe Ajax::RelationshipController, type: :controller do
context "screen_name does not exist" do
let(:screen_name) { "peter-witzig" }
- include_examples "ajax does not succeed", "not found"
+ include_examples "turbo does not succeed", "not found"
end
end
let(:type) { "Sauerkraut" }
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"
@@ -146,7 +148,7 @@ describe Ajax::RelationshipController, type: :controller do
context "type = 'dick'" do
let(:type) { "dick" }
- include_examples "ajax does not succeed", "Invalid parameter"
+ include_examples "turbo does not succeed", "Invalid parameter"
end
end
end
diff --git a/spec/controllers/settings/export_controller_spec.rb b/spec/controllers/settings/export_controller_spec.rb
index 0750ed6e..0bc87796 100644
--- a/spec/controllers/settings/export_controller_spec.rb
+++ b/spec/controllers/settings/export_controller_spec.rb
@@ -17,18 +17,20 @@ describe Settings::ExportController, type: :controller do
end
context "when user has a new DataExported notification" do
- let(:notification) do
+ let!(:notification) do
Notification::DataExported.create(
target_id: user.id,
target_type: "User::DataExport",
recipient: user,
- new: true
+ new: true,
)
end
it "marks the notification as read" do
expect { subject }.to change { notification.reload.new }.from(true).to(false)
end
+
+ include_examples "touches user timestamp", :notifications_updated_at
end
end
end
diff --git a/spec/controllers/settings/privacy_controller_spec.rb b/spec/controllers/settings/privacy_controller_spec.rb
index ad47b751..6ca5a979 100644
--- a/spec/controllers/settings/privacy_controller_spec.rb
+++ b/spec/controllers/settings/privacy_controller_spec.rb
@@ -13,7 +13,7 @@ describe Settings::PrivacyController, type: :controller do
it "renders the edit template" do
subject
- expect(response).to render_template("edit")
+ expect(response).to render_template(:edit)
end
end
end
@@ -43,7 +43,7 @@ describe Settings::PrivacyController, type: :controller do
it "redirects to the privacy settings page" do
subject
- expect(response).to redirect_to(:settings_privacy)
+ expect(response).to render_template(:edit)
end
end
end
diff --git a/spec/controllers/settings/profile_controller_spec.rb b/spec/controllers/settings/profile_controller_spec.rb
index 299fecf7..f91b7eb4 100644
--- a/spec/controllers/settings/profile_controller_spec.rb
+++ b/spec/controllers/settings/profile_controller_spec.rb
@@ -37,7 +37,7 @@ describe Settings::ProfileController, type: :controller do
it "redirects to the edit_user_profile page" do
subject
- expect(response).to redirect_to(:settings_profile)
+ expect(response).to render_template(:edit)
end
end
end
diff --git a/spec/controllers/settings/profile_picture_controller_spec.rb b/spec/controllers/settings/profile_picture_controller_spec.rb
index 727e8f61..bb3f464d 100644
--- a/spec/controllers/settings/profile_picture_controller_spec.rb
+++ b/spec/controllers/settings/profile_picture_controller_spec.rb
@@ -32,7 +32,8 @@ describe Settings::ProfilePictureController, type: :controller do
it "redirects to the edit_user_profile page" do
subject
- expect(response).to redirect_to(:settings_profile)
+ expect(response).to have_http_status(:ok)
+ expect(response).to have_rendered(:edit)
end
end
end
diff --git a/spec/controllers/settings/theme_controller_spec.rb b/spec/controllers/settings/theme_controller_spec.rb
index a2d4451e..bf25644f 100644
--- a/spec/controllers/settings/theme_controller_spec.rb
+++ b/spec/controllers/settings/theme_controller_spec.rb
@@ -61,7 +61,7 @@ describe Settings::ThemeController, type: :controller do
it "renders the edit template" do
subject
- expect(response).to redirect_to(:settings_theme)
+ expect(response).to render_template(:edit)
end
end
@@ -75,7 +75,7 @@ describe Settings::ThemeController, type: :controller do
it "renders the edit template" do
subject
- expect(response).to redirect_to(:settings_theme)
+ expect(response).to render_template(:edit)
end
end
end
diff --git a/spec/controllers/ajax/subscription_controller_spec.rb b/spec/controllers/subscriptions_controller_spec.rb
similarity index 66%
rename from spec/controllers/ajax/subscription_controller_spec.rb
rename to spec/controllers/subscriptions_controller_spec.rb
index 23499211..6b9951d4 100644
--- a/spec/controllers/ajax/subscription_controller_spec.rb
+++ b/spec/controllers/subscriptions_controller_spec.rb
@@ -2,41 +2,33 @@
require "rails_helper"
-describe Ajax::SubscriptionController, :ajax_controller, type: :controller do
+describe SubscriptionsController, type: :controller do
# need to use a different user here, as after a create the user owning the
# answer is automatically subscribed to it
let(:answer_user) { FactoryBot.create(:user) }
let(:answer) { FactoryBot.create(:answer, user: answer_user) }
+ let(:user) { FactoryBot.create(:user) }
- describe "#subscribe" do
+ describe "#create" do
let(:params) do
{
- answer: answer_id
+ answer: answer_id,
}
end
- subject { post(:subscribe, params: params) }
+ subject { post(:create, params:, format: :turbo_stream) }
context "when user is signed in" do
before(:each) { sign_in(user) }
context "when answer exists" do
let(:answer_id) { answer.id }
- let(:expected_response) do
- {
- "success" => true,
- "status" => "okay",
- "message" => anything
- }
- end
context "when subscription does not exist" do
it "creates a subscription on the answer" do
expect { subject }.to(change { answer.subscriptions.count }.by(1))
expect(answer.subscriptions.map { |s| s.user.id }.sort).to eq([answer_user.id, user.id].sort)
end
-
- include_examples "returns the expected response"
end
context "when subscription already exists" do
@@ -46,26 +38,15 @@ describe Ajax::SubscriptionController, :ajax_controller, type: :controller do
expect { subject }.to(change { answer.subscriptions.count }.by(0))
expect(answer.subscriptions.map { |s| s.user.id }.sort).to eq([answer_user.id, user.id].sort)
end
-
- include_examples "returns the expected response"
end
end
context "when answer does not exist" do
let(:answer_id) { "Bielefeld" }
- let(:expected_response) do
- {
- "success" => false,
- "status" => "not_found",
- "message" => anything
- }
- end
it "does not create a new subscription" do
expect { subject }.not_to(change { Subscription.count })
end
-
- include_examples "returns the expected response"
end
end
@@ -79,27 +60,20 @@ describe Ajax::SubscriptionController, :ajax_controller, type: :controller do
end
end
- describe "#unsubscribe" do
+ describe "#destroy" do
let(:params) do
{
- answer: answer_id
+ answer: answer_id,
}
end
- subject { post(:unsubscribe, params: params) }
+ subject { delete(:destroy, params:, format: :turbo_stream) }
context "when user is signed in" do
before(:each) { sign_in(user) }
context "when answer exists" do
let(:answer_id) { answer.id }
- let(:expected_response) do
- {
- "success" => true,
- "status" => "okay",
- "message" => anything
- }
- end
context "when subscription exists" do
before(:each) { Subscription.subscribe(user, answer) }
@@ -108,43 +82,22 @@ describe Ajax::SubscriptionController, :ajax_controller, type: :controller do
expect { subject }.to(change { answer.subscriptions.count }.by(-1))
expect(answer.subscriptions.map { |s| s.user.id }.sort).to eq([answer_user.id].sort)
end
-
- include_examples "returns the expected response"
end
context "when subscription does not exist" do
- let(:expected_response) do
- {
- "success" => false,
- "status" => "okay",
- "message" => anything
- }
- end
-
it "does not modify the answer's subscriptions" do
expect { subject }.to(change { answer.subscriptions.count }.by(0))
expect(answer.subscriptions.map { |s| s.user.id }.sort).to eq([answer_user.id].sort)
end
-
- include_examples "returns the expected response"
end
end
context "when answer does not exist" do
let(:answer_id) { "Bielefeld" }
- let(:expected_response) do
- {
- "success" => false,
- "status" => "not_found",
- "message" => anything
- }
- end
it "does not create a new subscription" do
expect { subject }.not_to(change { Subscription.count })
end
-
- include_examples "returns the expected response"
end
end
diff --git a/spec/controllers/user/sessions_controller_spec.rb b/spec/controllers/user/sessions_controller_spec.rb
index 66d6bdf0..a4a616f6 100644
--- a/spec/controllers/user/sessions_controller_spec.rb
+++ b/spec/controllers/user/sessions_controller_spec.rb
@@ -82,7 +82,7 @@ describe User::SessionsController do
it "redirects to the sign in page" do
expect(subject).to redirect_to :new_user_session
- expect(flash[:notice]).to eq "#{I18n.t('user.sessions.create.banned', name: user.screen_name)}\n#{I18n.t('user.sessions.create.reason', reason: 'Do not feed the animals')}"
+ expect(flash[:notice]).to eq "#{I18n.t('user.sessions.create.banned', name: user.screen_name)}\n#{I18n.t('user.sessions.create.reason', reason: 'Do not feed the animals')}\n#{I18n.t('user.sessions.create.permanent')}"
end
end
diff --git a/spec/factories/comment_smile.rb b/spec/factories/comment_smile.rb
index 6923e121..09c65313 100644
--- a/spec/factories/comment_smile.rb
+++ b/spec/factories/comment_smile.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :comment_smile, class: Appendable::Reaction do
+ factory :comment_smile, class: Reaction do
user { FactoryBot.build(:user) }
parent { FactoryBot.build(:comment) }
content { "🙂" }
diff --git a/spec/factories/inbox.rb b/spec/factories/inbox_entry.rb
similarity index 82%
rename from spec/factories/inbox.rb
rename to spec/factories/inbox_entry.rb
index 6345fa72..182e69c2 100644
--- a/spec/factories/inbox.rb
+++ b/spec/factories/inbox_entry.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :inbox do
+ factory :inbox_entry do
question { FactoryBot.build(:question) }
new { true }
end
diff --git a/spec/factories/smile.rb b/spec/factories/smile.rb
index 43bd9bf4..1726e9fa 100644
--- a/spec/factories/smile.rb
+++ b/spec/factories/smile.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :smile, class: Appendable::Reaction do
+ factory :smile, class: Reaction do
user { FactoryBot.create(:user) }
parent { FactoryBot.create(:answer, user: FactoryBot.create(:user)) }
content { "🙂" }
diff --git a/spec/helpers/application_helper/graph_methods_spec.rb b/spec/helpers/application_helper/graph_methods_spec.rb
index 63baa848..1d79b78c 100644
--- a/spec/helpers/application_helper/graph_methods_spec.rb
+++ b/spec/helpers/application_helper/graph_methods_spec.rb
@@ -1,20 +1,22 @@
# frozen_string_literal: true
-require 'rails_helper'
+require "rails_helper"
-describe ApplicationHelper::GraphMethods, :type => :helper do
+describe ApplicationHelper::GraphMethods, type: :helper do
describe "#user_opengraph" do
context "sample user" do
- let(:user) { FactoryBot.create(:user,
- profile: { display_name: 'Cunes',
- description: 'A bunch of raccoons in a trenchcoat.' },
- screen_name: 'raccoons') }
+ let(:user) do
+ FactoryBot.create(:user,
+ profile: { display_name: "Cunes",
+ description: "A bunch of raccoons in a trenchcoat.", },
+ screen_name: "raccoons",)
+ end
subject { user_opengraph(user) }
- it 'should generate a matching OpenGraph structure for a user' do
- allow(APP_CONFIG).to receive(:[]).with('site_name').and_return('pineapplespring')
- expect(subject).to eq(<<~EOS.chomp)
+ it "should generate a matching OpenGraph structure for a user" do
+ allow(APP_CONFIG).to receive(:[]).with("site_name").and_return("pineapplespring")
+ expect(subject).to eq(<<~META.chomp)
@@ -22,55 +24,63 @@ describe ApplicationHelper::GraphMethods, :type => :helper do
- EOS
+ META
end
end
end
describe "#user_twitter_card" do
context "sample user" do
- let(:user) { FactoryBot.create(:user,
- profile: {
- display_name: '',
- description: 'A bunch of raccoons in a trenchcoat.'},
- screen_name: 'raccoons') }
+ let(:user) do
+ FactoryBot.create(:user,
+ profile: {
+ display_name: "",
+ description: "A bunch of raccoons in a trenchcoat.",
+ },
+ screen_name: "raccoons",)
+ end
subject { user_twitter_card(user) }
- it 'should generate a matching OpenGraph structure for a user' do
- expect(subject).to eq(<<~EOS.chomp)
+ it "should generate a matching OpenGraph structure for a user" do
+ expect(subject).to eq(<<~META.chomp)
- EOS
+ META
end
end
end
describe "#answer_opengraph" do
context "sample user and answer" do
- let!(:user) { FactoryBot.create(:user,
- profile: {
- display_name: '',
- description: 'A bunch of raccoons in a trenchcoat.'},
- screen_name: 'raccoons') }
- let(:answer) { FactoryBot.create(:answer,
- user_id: user.id,) }
+ let!(:user) do
+ FactoryBot.create(:user,
+ profile: {
+ display_name: "",
+ description: "A bunch of raccoons in a trenchcoat.",
+ },
+ screen_name: "raccoons",)
+ end
+ let(:answer) do
+ FactoryBot.create(:answer,
+ user_id: user.id,)
+ end
subject { answer_opengraph(answer) }
- it 'should generate a matching OpenGraph structure for a user' do
- allow(APP_CONFIG).to receive(:[]).with('site_name').and_return('pineapplespring')
- expect(subject).to eq(<<~EOS.chomp)
+ it "should generate a matching OpenGraph structure for a user" do
+ allow(APP_CONFIG).to receive(:[]).with("site_name").and_return("pineapplespring")
+ expect(subject).to eq(<<~META.chomp)
- EOS
+ META
end
end
end
-end
\ No newline at end of file
+end
diff --git a/spec/helpers/application_helper/title_methods_spec.rb b/spec/helpers/application_helper/title_methods_spec.rb
index 2b3c8bf0..92dc74a8 100644
--- a/spec/helpers/application_helper/title_methods_spec.rb
+++ b/spec/helpers/application_helper/title_methods_spec.rb
@@ -12,8 +12,8 @@ describe ApplicationHelper::TitleMethods, type: :helper do
"anonymous_name" => "Anonymous",
"https" => true,
"items_per_page" => 5,
- "sharing" => {}
- })
+ "sharing" => {},
+ },)
user.profile.display_name = "Cool Man"
user.profile.save!
@@ -42,7 +42,7 @@ describe ApplicationHelper::TitleMethods, type: :helper do
context "user has custom anonymous display name" do
before do
- FactoryBot.create(:answer, question: question, user: user)
+ FactoryBot.create(:answer, question:, user:)
user.profile.anon_display_name = "Amogus"
user.profile.save!
end
@@ -55,9 +55,9 @@ describe ApplicationHelper::TitleMethods, type: :helper do
describe "#answer_title" do
let(:answer) do
- FactoryBot.create(:answer, user: user,
+ FactoryBot.create(:answer, user:,
content: "a",
- question_content: "q")
+ question_content: "q",)
end
it "should generate a proper title" do
diff --git a/spec/helpers/bootstrap_helper_spec.rb b/spec/helpers/bootstrap_helper_spec.rb
index b996f162..02e98f65 100644
--- a/spec/helpers/bootstrap_helper_spec.rb
+++ b/spec/helpers/bootstrap_helper_spec.rb
@@ -1,107 +1,110 @@
+# frozen_string_literal: true
+
require "rails_helper"
-describe BootstrapHelper, :type => :helper do
+describe BootstrapHelper, type: :helper do
include ActiveSupport::Testing::TimeHelpers
- describe '#nav_entry' do
- it 'should return a HTML navigation item which links to a given address' do
+ describe "#nav_entry" do
+ it "should return a HTML navigation item which links to a given address" do
allow(self).to receive(:current_page?).and_return(false)
- expect(nav_entry('Example', '/example')).to(
- eq('Example')
+ expect(nav_entry("Example", "/example")).to(
+ eq('Example'),
)
end
- it 'should return with an active attribute if the link matches the current URL' do
+ it "should return with an active attribute if the link matches the current URL" do
allow(self).to receive(:current_page?).and_return(true)
- expect(nav_entry('Example', '/example')).to(
- eq('Example')
+ expect(nav_entry("Example", "/example")).to(
+ eq('Example'),
)
end
- it 'should include an icon if given' do
+ it "should include an icon if given" do
allow(self).to receive(:current_page?).and_return(false)
- expect(nav_entry('Example', '/example', icon: 'beaker')).to(
- eq(' Example')
+ expect(nav_entry("Example", "/example", icon: "beaker")).to(
+ eq(' Example'),
)
end
- it 'should only include an icon if wanted' do
+ it "should only include an icon if wanted" do
allow(self).to receive(:current_page?).and_return(false)
- expect(nav_entry('Example', '/example', icon: 'beaker', icon_only: true)).to(
- eq('')
+ expect(nav_entry("Example", "/example", icon: "beaker", icon_only: true)).to(
+ eq(''),
)
end
- it 'should include a badge if given' do
+ it "should include a badge if given" do
allow(self).to receive(:current_page?).and_return(false)
- expect(nav_entry('Example', '/example', badge: 3)).to(
- eq('Example 3')
+ expect(nav_entry("Example", "/example", badge: 3)).to(
+ eq('Example 3'),
)
- expect(nav_entry('Example', '/example', badge: 3, badge_color: 'primary', badge_pill: true)).to(
- eq('Example 3')
+ expect(nav_entry("Example", "/example", badge: 3, badge_color: "primary", badge_pill: true)).to(
+ eq('Example 3'),
+ )
+ end
+
+ it "should put an ID on the entry an id if given" do
+ allow(self).to receive(:current_page?).and_return(false)
+ expect(nav_entry("Example", "/example", id: "testing")).to(
+ eq("Example"),
)
end
end
describe "#list_group_item" do
- it 'should return a HTML navigation item which links to a given address' do
+ it "should return a HTML navigation item which links to a given address" do
allow(self).to receive(:current_page?).and_return(false)
- expect(list_group_item('Example', '/example')).to(
- eq('Example')
+ expect(list_group_item("Example", "/example")).to(
+ eq('Example'),
)
end
- it 'should return with an active attribute if the link matches the current URL' do
+ it "should return with an active attribute if the link matches the current URL" do
allow(self).to receive(:current_page?).and_return(true)
- expect(list_group_item('Example', '/example')).to(
- eq('Example')
+ expect(list_group_item("Example", "/example")).to(
+ eq('Example'),
)
end
- it 'should include a badge if given' do
+ it "should include a badge if given" do
allow(self).to receive(:current_page?).and_return(false)
- expect(list_group_item('Example', '/example', badge: 3)).to(
- eq('Example 3')
+ expect(list_group_item("Example", "/example", badge: 3)).to(
+ eq('Example 3'),
)
end
end
describe "#bootstrap_color" do
- it 'should map error and alert to danger' do
+ it "should map error and alert to danger" do
expect(bootstrap_color("error")).to eq("danger")
expect(bootstrap_color("alert")).to eq("danger")
end
- it 'should map notice to info' do
+ it "should map notice to info" do
expect(bootstrap_color("notice")).to eq("info")
end
- it 'should return any uncovered value' do
+ it "should return any uncovered value" do
expect(bootstrap_color("success")).to eq("success")
end
end
describe "#tooltip" do
- it 'should return the proper markup' do
- expect(tooltip("Example Text", "This is in a tooltip")).to eq("Example Text")
+ it "should return the proper markup" do
+ expect(tooltip("Example Text", "This is in a tooltip")).to eq("Example Text")
end
end
describe "#time_tooltip" do
- it 'should return a tooltip with proper time values' do
+ it "should return a tooltip with proper time values" do
travel_to(Time.utc(1984)) do
@user = FactoryBot.create(:user)
travel 10.minutes
- expect(time_tooltip(@user)).to eq("10 minutes")
+ expect(time_tooltip(@user)).to eq("10m")
end
end
end
-
- describe "#hidespan" do
- it 'should return the proper markup' do
- expect(hidespan("Hidden Text", "d-none")).to eq("Hidden Text")
- end
- end
end
diff --git a/spec/helpers/feedback_helper_spec.rb b/spec/helpers/feedback_helper_spec.rb
index 13d2245e..c5aa16df 100644
--- a/spec/helpers/feedback_helper_spec.rb
+++ b/spec/helpers/feedback_helper_spec.rb
@@ -11,9 +11,9 @@ describe FeedbackHelper, type: :helper do
"canny" => {
sso: "sso",
feature_board: "feature",
- bug_board: "bug"
- }
- })
+ bug_board: "bug",
+ },
+ },)
end
describe "#canny_token" do
diff --git a/spec/helpers/markdown_helper_spec.rb b/spec/helpers/markdown_helper_spec.rb
index b4c22b76..39270551 100644
--- a/spec/helpers/markdown_helper_spec.rb
+++ b/spec/helpers/markdown_helper_spec.rb
@@ -10,8 +10,8 @@ describe MarkdownHelper, type: :helper do
"items_per_page" => 5,
"allowed_hosts" => [
"twitter.com"
- ]
- })
+ ],
+ },)
end
describe "#markdown" do
diff --git a/spec/helpers/social_helper/bluesky_methods_spec.rb b/spec/helpers/social_helper/bluesky_methods_spec.rb
new file mode 100644
index 00000000..b2457089
--- /dev/null
+++ b/spec/helpers/social_helper/bluesky_methods_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe SocialHelper::BlueskyMethods, type: :helper do
+ include SocialHelper::TwitterMethods
+
+ let(:user) { FactoryBot.create(:user) }
+ let(:question_content) { "q" * 255 }
+ let(:answer_content) { "a" * 255 }
+ let(:answer) do
+ FactoryBot.create(:answer, user:,
+ content: answer_content,
+ question_content:,)
+ end
+
+ before do
+ stub_const("APP_CONFIG", {
+ "hostname" => "example.com",
+ "https" => true,
+ "items_per_page" => 5,
+ },)
+ end
+
+ describe "#bluesky_share_url" do
+ subject { bluesky_share_url(answer) }
+
+ it "should return a proper share link" do
+ expect(subject).to eq("https://bsky.app/intent/compose?text=#{CGI.escape(prepare_tweet(answer))}")
+ end
+ end
+end
diff --git a/spec/helpers/social_helper/telegram_methods_spec.rb b/spec/helpers/social_helper/telegram_methods_spec.rb
index 5e180373..3a72cff8 100644
--- a/spec/helpers/social_helper/telegram_methods_spec.rb
+++ b/spec/helpers/social_helper/telegram_methods_spec.rb
@@ -9,7 +9,7 @@ describe SocialHelper::TelegramMethods, type: :helper do
:answer,
user:,
content: "this is an answer\nwith multiple lines\nand **FORMATTING**",
- question_content: "this is a question .... or is it?"
+ question_content: "this is a question .... or is it?",
)
end
@@ -18,7 +18,7 @@ describe SocialHelper::TelegramMethods, type: :helper do
"hostname" => "example.com",
"https" => true,
"items_per_page" => 5,
- })
+ },)
end
describe "#telegram_text" do
diff --git a/spec/helpers/social_helper/tumblr_methods_spec.rb b/spec/helpers/social_helper/tumblr_methods_spec.rb
index 512a35e7..29ba43d0 100644
--- a/spec/helpers/social_helper/tumblr_methods_spec.rb
+++ b/spec/helpers/social_helper/tumblr_methods_spec.rb
@@ -1,33 +1,35 @@
# frozen_string_literal: true
-require 'rails_helper'
+require "rails_helper"
-describe SocialHelper::TumblrMethods, :type => :helper do
+describe SocialHelper::TumblrMethods, type: :helper do
let(:user) { FactoryBot.create(:user) }
- let(:answer) { FactoryBot.create(:answer, user: user,
- content: 'aaaa',
- question_content: 'q') }
+ let(:answer) do
+ FactoryBot.create(:answer, user:,
+ content: "aaaa",
+ question_content: "q",)
+ end
before do
stub_const("APP_CONFIG", {
- 'hostname' => 'example.com',
- 'anonymous_name' => 'Anonymous',
- 'https' => true,
- 'items_per_page' => 5,
- 'sharing' => {}
- })
+ "hostname" => "example.com",
+ "anonymous_name" => "Anonymous",
+ "https" => true,
+ "items_per_page" => 5,
+ "sharing" => {},
+ },)
end
- describe '#tumblr_title' do
- context 'Asker is anonymous' do
+ describe "#tumblr_title" do
+ context "Asker is anonymous" do
subject { tumblr_title(answer) }
- it 'should return a proper title' do
- expect(subject).to eq('Anonymous asked: q')
+ it "should return a proper title" do
+ expect(subject).to eq("Anonymous asked: q")
end
end
- context 'Asker is known' do
+ context "Asker is known" do
before do
@user = FactoryBot.create(:user)
answer.question.user = @user
@@ -36,24 +38,24 @@ describe SocialHelper::TumblrMethods, :type => :helper do
subject { tumblr_title(answer) }
- it 'should return a proper title' do
+ it "should return a proper title" do
expect(subject).to eq("#{answer.question.user.profile.display_name} asked: q")
end
end
end
- describe '#tumblr_body' do
+ describe "#tumblr_body" do
subject { tumblr_body(answer) }
- it 'should return a proper body' do
+ it "should return a proper body" do
expect(subject).to eq("aaaa\n\n[Smile or comment on the answer here](https://example.com/@#{answer.user.screen_name}/a/#{answer.id})")
end
end
- describe '#tumblr_share_url' do
+ describe "#tumblr_share_url" do
subject { tumblr_share_url(answer) }
- it 'should return a proper share link' do
+ it "should return a proper share link" do
expect(subject).to eq("https://www.tumblr.com/widgets/share/tool?shareSource=legacy&posttype=text&title=#{CGI.escape(tumblr_title(answer))}&url=#{CGI.escape("https://example.com/@#{answer.user.screen_name}/a/#{answer.id}")}&caption=&content=#{CGI.escape(tumblr_body(answer))}")
end
end
diff --git a/spec/helpers/social_helper/twitter_methods_spec.rb b/spec/helpers/social_helper/twitter_methods_spec.rb
index 7074f6e6..6157298f 100644
--- a/spec/helpers/social_helper/twitter_methods_spec.rb
+++ b/spec/helpers/social_helper/twitter_methods_spec.rb
@@ -1,72 +1,85 @@
# frozen_string_literal: true
-require 'rails_helper'
+require "rails_helper"
-describe SocialHelper::TwitterMethods, :type => :helper do
+describe SocialHelper::TwitterMethods, type: :helper do
let(:user) { FactoryBot.create(:user) }
- let(:question_content) { 'q' * 255 }
- let(:answer_content) { 'a' * 255 }
- let(:answer) { FactoryBot.create(:answer, user: user,
- content: answer_content,
- question_content: question_content) }
+ let(:question_content) { "q" * 255 }
+ let(:answer_content) { "a" * 255 }
+ let(:answer) do
+ FactoryBot.create(:answer, user:,
+ content: answer_content,
+ question_content:,)
+ end
before do
stub_const("APP_CONFIG", {
- 'hostname' => 'example.com',
- 'https' => true,
- 'items_per_page' => 5
- })
+ "hostname" => "example.com",
+ "https" => true,
+ "items_per_page" => 5,
+ },)
end
- describe '#prepare_tweet' do
- context 'when the question and answer need to be shortened' do
+ describe "#prepare_tweet" do
+ context "when the question and answer need to be shortened" do
subject { prepare_tweet(answer) }
- it 'should return a properly formatted tweet' do
+ it "should return a properly formatted tweet" do
expect(subject).to eq("#{'q' * 123}… — #{'a' * 124}… https://example.com/@#{user.screen_name}/a/#{answer.id}")
end
end
- context 'when a suffix has been passed' do
- let(:question_content) { 'question' }
- let(:answer_content) { 'answer' }
+ context "when a suffix has been passed" do
+ let(:question_content) { "question" }
+ let(:answer_content) { "answer" }
- subject { prepare_tweet(answer, '#askracc') }
+ subject { prepare_tweet(answer, "#askracc") }
- it 'should include the suffix after the link' do
+ it "should include the suffix after the link" do
expect(subject).to eq("question — answer #askracc https://example.com/@#{user.screen_name}/a/#{answer.id}")
end
end
- context 'when a suffix has been passed and the tweet needs to be shortened' do
- subject { prepare_tweet(answer, '#askracc') }
+ context "when the url should be omitted" do
+ let(:question_content) { "question" }
+ let(:answer_content) { "answer" }
- it 'should shorten the tweet while keeping the suffix intact' do
+ subject { prepare_tweet(answer, nil, true) }
+
+ it "should include the suffix after the link" do
+ expect(subject).to eq("question — answer")
+ end
+ end
+
+ context "when a suffix has been passed and the tweet needs to be shortened" do
+ subject { prepare_tweet(answer, "#askracc") }
+
+ it "should shorten the tweet while keeping the suffix intact" do
expect(subject).to eq("#{'q' * 120}… — #{'a' * 120}… #askracc https://example.com/@#{user.screen_name}/a/#{answer.id}")
end
end
- context 'when the question and answer are short' do
+ context "when the question and answer are short" do
before do
- answer.question.content = 'Why are raccoons so good?'
+ answer.question.content = "Why are raccoons so good?"
answer.question.save!
- answer.content = 'Because they are good cunes.'
+ answer.content = "Because they are good cunes."
answer.save!
end
subject { prepare_tweet(answer) }
- it 'should return a properly formatted tweet' do
+ it "should return a properly formatted tweet" do
expect(subject).to eq("#{answer.question.content} — #{answer.content} https://example.com/@#{user.screen_name}/a/#{answer.id}")
end
end
end
- describe '#twitter_share_url' do
+ describe "#twitter_share_url" do
subject { twitter_share_url(answer) }
- it 'should return a proper share link' do
+ it "should return a proper share link" do
expect(subject).to eq("https://twitter.com/intent/tweet?text=#{CGI.escape(prepare_tweet(answer))}")
end
end
-end
\ No newline at end of file
+end
diff --git a/spec/helpers/social_helper_spec.rb b/spec/helpers/social_helper_spec.rb
new file mode 100644
index 00000000..b56d6ab3
--- /dev/null
+++ b/spec/helpers/social_helper_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe SocialHelper, type: :helper do
+ let(:user) { FactoryBot.create(:user) }
+ let(:answer) do
+ FactoryBot.create(
+ :answer,
+ user:,
+ content: "this is an answer\nwith multiple lines\nand **FORMATTING**",
+ question_content: "this is a question .... or is it?",
+ )
+ end
+
+ before do
+ stub_const("APP_CONFIG", {
+ "hostname" => "example.com",
+ "https" => true,
+ "items_per_page" => 5,
+ },)
+ end
+
+ describe "#answer_share_url" do
+ subject { answer_share_url(answer) }
+
+ it "returns a proper share link" do
+ expect(subject).to eq(<<~URL.strip)
+ https://example.com/@#{answer.user.screen_name}/a/#{answer.id}
+ URL
+ end
+ end
+end
diff --git a/spec/helpers/theme_helper_spec.rb b/spec/helpers/theme_helper_spec.rb
index 3a72ed34..f7de9441 100644
--- a/spec/helpers/theme_helper_spec.rb
+++ b/spec/helpers/theme_helper_spec.rb
@@ -2,7 +2,7 @@
require "rails_helper"
-describe ThemeHelper, :type => :helper do
+describe ThemeHelper, type: :helper do
describe "#render_theme" do
context "when target page doesn't have a theme" do
it "returns no theme" do
@@ -18,7 +18,7 @@ describe ThemeHelper, :type => :helper do
end
it "returns a theme" do
- expect(helper.render_theme).to include('