mirror of
https://github.com/Retrospring/retrospring.git
synced 2025-03-24 03:37:50 +01:00
Initial D-letion
This commit is contained in:
parent
2c6f2d0dab
commit
d8e98eee98
469 changed files with 38 additions and 19685 deletions
10
Gemfile
10
Gemfile
|
@ -34,15 +34,10 @@ gem "haml", "~> 6.3"
|
|||
gem "hcaptcha", git: "https://github.com/retrospring/hcaptcha", ref: "fix/flash-in-turbo-streams"
|
||||
gem "mini_magick"
|
||||
gem "oj"
|
||||
gem "rpush"
|
||||
gem "rqrcode"
|
||||
gem "web-push"
|
||||
|
||||
gem "rolify", "~> 6.0"
|
||||
|
||||
gem "dry-initializer", "~> 3.1"
|
||||
gem "dry-types", "~> 1.7"
|
||||
|
||||
gem "pghero"
|
||||
gem "rails_admin"
|
||||
gem "sentry-rails"
|
||||
|
@ -52,14 +47,9 @@ gem "sentry-sidekiq"
|
|||
gem "sidekiq", "< 7" # remove version constraint once are ready to upgrade https://github.com/mperham/sidekiq/blob/main/docs/7.0-Upgrade.md
|
||||
gem "sidekiq-scheduler"
|
||||
|
||||
gem "questiongenerator", "~> 1.1"
|
||||
|
||||
gem "httparty"
|
||||
gem "redcarpet"
|
||||
gem "sanitize"
|
||||
|
||||
gem "twitter-text"
|
||||
|
||||
gem "connection_pool"
|
||||
gem "redis"
|
||||
|
||||
|
|
60
Gemfile.lock
60
Gemfile.lock
|
@ -120,7 +120,6 @@ GEM
|
|||
crass (1.0.6)
|
||||
cssbundling-rails (1.4.1)
|
||||
railties (>= 6.0.0)
|
||||
csv (3.3.0)
|
||||
database_cleaner (2.0.2)
|
||||
database_cleaner-active_record (>= 2, < 3)
|
||||
database_cleaner-active_record (2.1.0)
|
||||
|
@ -146,22 +145,6 @@ GEM
|
|||
dotenv-rails (3.1.2)
|
||||
dotenv (= 3.1.2)
|
||||
railties (>= 6.1)
|
||||
dry-core (1.0.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-inflector (1.0.0)
|
||||
dry-initializer (3.1.1)
|
||||
dry-logic (1.5.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-types (1.7.2)
|
||||
bigdecimal (~> 3.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0)
|
||||
dry-inflector (~> 1.0)
|
||||
dry-logic (~> 1.4)
|
||||
zeitwerk (~> 2.6)
|
||||
erubi (1.13.0)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
|
@ -212,18 +195,11 @@ GEM
|
|||
rainbow
|
||||
rubocop (>= 1.0)
|
||||
sysexits (~> 1.1)
|
||||
hkdf (0.3.0)
|
||||
http-2 (0.11.0)
|
||||
httparty (0.22.0)
|
||||
csv
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (1.14.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (4.0.0)
|
||||
glob
|
||||
i18n
|
||||
idn-ruby (0.1.5)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
|
@ -276,13 +252,7 @@ GEM
|
|||
minitest (5.25.1)
|
||||
msgpack (1.7.2)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.7.1)
|
||||
bigdecimal (~> 3.1)
|
||||
nested_form (0.3.2)
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-http2 (0.18.5)
|
||||
http-2 (~> 0.11)
|
||||
net-imap (0.4.15)
|
||||
date
|
||||
net-protocol
|
||||
|
@ -316,7 +286,6 @@ GEM
|
|||
nio4r (~> 2.0)
|
||||
pundit (2.4.0)
|
||||
activesupport (>= 3.0.0)
|
||||
questiongenerator (1.1.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.9)
|
||||
|
@ -378,16 +347,6 @@ GEM
|
|||
rolify (6.0.1)
|
||||
rotp (6.3.0)
|
||||
rouge (4.1.3)
|
||||
rpush (7.0.1)
|
||||
activesupport (>= 5.2)
|
||||
jwt (>= 1.5.6)
|
||||
multi_json (~> 1.0)
|
||||
net-http-persistent
|
||||
net-http2 (~> 0.18, >= 0.18.3)
|
||||
railties
|
||||
rainbow
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
webpush (~> 1.0)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
|
@ -503,14 +462,8 @@ GEM
|
|||
actionpack (>= 6.0.0)
|
||||
activejob (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
twitter-text (3.1.0)
|
||||
idn-ruby
|
||||
unf (~> 0.1.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.5.0)
|
||||
uniform_notifier (1.16.0)
|
||||
view_component (3.14.0)
|
||||
|
@ -519,12 +472,6 @@ GEM
|
|||
method_source (~> 1.0)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-push (3.0.1)
|
||||
jwt (~> 2.0)
|
||||
openssl (~> 3.0)
|
||||
webpush (1.1.0)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
|
@ -551,8 +498,6 @@ DEPENDENCIES
|
|||
devise-async
|
||||
devise-i18n
|
||||
dotenv-rails (~> 3.1)
|
||||
dry-initializer (~> 3.1)
|
||||
dry-types (~> 1.7)
|
||||
factory_bot_rails
|
||||
fake_email_validator
|
||||
faker
|
||||
|
@ -562,7 +507,6 @@ DEPENDENCIES
|
|||
haml (~> 6.3)
|
||||
haml_lint
|
||||
hcaptcha!
|
||||
httparty
|
||||
i18n-js (= 4.0)
|
||||
jsbundling-rails (~> 1.3)
|
||||
json-schema
|
||||
|
@ -581,7 +525,6 @@ DEPENDENCIES
|
|||
prometheus-client (~> 4.2)
|
||||
puma
|
||||
pundit (~> 2.4)
|
||||
questiongenerator (~> 1.1)
|
||||
rails (~> 7.0.8)
|
||||
rails-controller-testing
|
||||
rails-i18n (~> 7.0)
|
||||
|
@ -590,7 +533,6 @@ DEPENDENCIES
|
|||
redcarpet
|
||||
redis
|
||||
rolify (~> 6.0)
|
||||
rpush
|
||||
rqrcode
|
||||
rspec-its (~> 1.3)
|
||||
rspec-mocks
|
||||
|
@ -614,9 +556,7 @@ DEPENDENCIES
|
|||
sprockets-rails
|
||||
tldv (~> 0.1.0)
|
||||
turbo-rails
|
||||
twitter-text
|
||||
view_component
|
||||
web-push
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.5
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
%li.comment{ data: { comment_id: @comment.id } }
|
||||
.d-flex
|
||||
.flex-shrink-0
|
||||
%a{ href: user_path(@comment.user), target: :_top }
|
||||
= render AvatarComponent.new(user: @comment.user, size: "sm", classes: ["comment__user-avatar"])
|
||||
.flex-grow-1
|
||||
%h6.comment__user
|
||||
= user_screen_name @comment.user
|
||||
%span.text-muted
|
||||
·
|
||||
= time_tooltip @comment
|
||||
.comment__content
|
||||
= markdown @comment.content
|
||||
.flex-shrink-0.ms-auto
|
||||
- if current_user&.smiled?(@comment)
|
||||
= render "reactions/destroy", type: "Comment", target: @comment
|
||||
- else
|
||||
= render "reactions/create", type: "Comment", target: @comment
|
||||
.dropdown.d-inline
|
||||
%button.btn.btn-link.answerbox__action{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
|
||||
%i.fa.fa-fw.fa-ellipsis
|
||||
= render "actions/comment", comment: @comment, answer: @answer
|
|
@ -1,12 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CommentComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
include BootstrapHelper
|
||||
include UserHelper
|
||||
|
||||
def initialize(comment:, answer:)
|
||||
@comment = comment
|
||||
@answer = answer
|
||||
end
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QuestionComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
include BootstrapHelper
|
||||
include UserHelper
|
||||
|
||||
def initialize(question:, context_user: nil, collapse: true, hide_avatar: false, profile_question: false)
|
||||
@question = question
|
||||
@context_user = context_user
|
||||
@collapse = collapse
|
||||
@hide_avatar = hide_avatar
|
||||
@profile_question = profile_question
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def author_identifier = @question.author_is_anonymous ? @question.author_identifier : nil
|
||||
|
||||
def follower_question? = !@question.author_is_anonymous && !@question.direct && @question.answer_count.positive?
|
||||
|
||||
def hide_avatar? = @hide_avatar || @question.author_is_anonymous
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
en:
|
||||
anon_hint: "This question was asked anonymously"
|
||||
answers:
|
||||
zero: "0 answers"
|
||||
one: "1 answer"
|
||||
other: "%{count} answers"
|
||||
asked_html: "%{user} asked %{time} ago"
|
||||
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"
|
|
@ -1,34 +0,0 @@
|
|||
.d-flex
|
||||
- unless hide_avatar?
|
||||
.flex-shrink-0
|
||||
%a{ href: user_path(@question.user) }
|
||||
= render AvatarComponent.new(user: @question.user, size: "md", classes: ["question__avatar"])
|
||||
.flex-grow-1
|
||||
%h6.text-muted.question__user
|
||||
- if @question.author_is_anonymous
|
||||
%span{ title: t(".anon_hint"), data: { controller: :tooltip, bs_placement: :bottom } }
|
||||
%i.fas.fa-user-secret
|
||||
- if @profile_question && @question.direct
|
||||
- if user_signed_in? && @question.user == current_user
|
||||
%span.d-inline-block{ title: t(".visible_to_you"), data: { controller: :tooltip, bs_placement: :bottom } }
|
||||
%i.fa.fa-eye-slash
|
||||
- elsif moderation_view?
|
||||
%span{ title: t(".visible_mod_mode"), data: { controller: :tooltip, bs_placement: :bottom } }
|
||||
%i.fa.fa-eye-slash
|
||||
= user_screen_name(@question.user, context_user: @context_user, author_identifier: author_identifier)
|
||||
- if follower_question?
|
||||
·
|
||||
%a{ href: question_path(@question.user.screen_name, @question.id), data: { selection_hotkey: "a" } }
|
||||
= t(".answers", count: @question.answer_count)
|
||||
·
|
||||
= time_tooltip(@question)
|
||||
- if user_signed_in?
|
||||
.dropdown.d-inline
|
||||
%button.btn.btn-link.btn-sm.p-0{ data: { bs_toggle: :dropdown }, aria: { expanded: false } }
|
||||
%i.fa.fa-fw.fa-ellipsis
|
||||
= render "actions/question", question: @question
|
||||
.question__body{ data: { controller: @question.long? ? "collapse" : nil } }
|
||||
.question__text{ class: @question.long? && @collapse ? "collapsed" : "", data: { collapse_target: "content" } }
|
||||
= question_markdown @question.content
|
||||
- if @question.long? && @collapse
|
||||
= render "shared/collapse", type: "question"
|
|
@ -7,28 +7,9 @@ class AboutController < ApplicationController
|
|||
render template: "about/index_advanced"
|
||||
end
|
||||
|
||||
def about
|
||||
@users = Rails.cache.fetch("about_count_users", expires_in: 1.hour) { user_count - current_ban_count }
|
||||
@questions = Rails.cache.fetch("about_count_questions", expires_in: 1.hour) { Question.count(:id) }
|
||||
@answers = Rails.cache.fetch("about_count_answers", expires_in: 1.hour) { Answer.count(:id) }
|
||||
@comments = Rails.cache.fetch("about_count_comments", expires_in: 1.hour) { Comment.count(:id) }
|
||||
end
|
||||
def about; end
|
||||
|
||||
def privacy_policy; end
|
||||
|
||||
def terms; end
|
||||
|
||||
private
|
||||
|
||||
def user_count = User
|
||||
.where.not(confirmed_at: nil)
|
||||
.where("answered_count > 0")
|
||||
.count
|
||||
|
||||
def current_ban_count = UserBan
|
||||
.current
|
||||
.joins(:user)
|
||||
.where.not("users.confirmed_at": nil)
|
||||
.where("users.answered_count > 0")
|
||||
.count
|
||||
end
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::AnnouncementController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@announcements = Announcement.all
|
||||
end
|
||||
|
||||
def new
|
||||
@announcement = Announcement.new
|
||||
end
|
||||
|
||||
def create
|
||||
@announcement = Announcement.new(announcement_params)
|
||||
@announcement.user = current_user
|
||||
if @announcement.save
|
||||
flash[:success] = t(".success")
|
||||
redirect_to action: :index
|
||||
else
|
||||
flash[:error] = t(".error")
|
||||
render "admin/announcement/new"
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@announcement = Announcement.find(params[:id])
|
||||
end
|
||||
|
||||
def update
|
||||
@announcement = Announcement.find(params[:id])
|
||||
@announcement.update(announcement_params)
|
||||
if @announcement.save
|
||||
flash[:success] = t(".success")
|
||||
redirect_to announcement_index_path
|
||||
else
|
||||
flash[:error] = t(".error")
|
||||
render "admin/announcement/edit"
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if Announcement.destroy(params[:id])
|
||||
flash[:success] = t(".success")
|
||||
else
|
||||
flash[:error] = t(".error")
|
||||
end
|
||||
redirect_to announcement_index_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def announcement_params
|
||||
params.require(:announcement).permit(:content, :link_text, :link_href, :starts_at, :ends_at)
|
||||
end
|
||||
end
|
|
@ -1,14 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "sidekiq/api"
|
||||
|
||||
class Admin::DashboardController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@sidekiq = {
|
||||
processes: Sidekiq::ProcessSet.new,
|
||||
stats: Sidekiq::Stats.new
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,81 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "cgi"
|
||||
|
||||
class Ajax::AnswerController < AjaxController
|
||||
include SocialHelper
|
||||
|
||||
def create
|
||||
params.require :id
|
||||
params.require :answer
|
||||
params.require :inbox
|
||||
|
||||
inbox = (params[:inbox] == "true")
|
||||
|
||||
if inbox
|
||||
inbox_entry = InboxEntry.find(params[:id])
|
||||
|
||||
unless current_user == inbox_entry.user
|
||||
@response[:status] = :fail
|
||||
@response[:message] = t(".error")
|
||||
return
|
||||
end
|
||||
else
|
||||
question = Question.find(params[:id])
|
||||
|
||||
unless question.user.privacy_allow_stranger_answers
|
||||
@response[:status] = :privacy_stronk
|
||||
@response[:message] = t(".privacy")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
answer = if inbox
|
||||
inbox_entry.answer params[:answer], current_user
|
||||
else
|
||||
current_user.answer question, params[:answer]
|
||||
end
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:message] = t(".success")
|
||||
@response[:success] = true
|
||||
|
||||
@response[:sharing] = sharing_hash(answer) if current_user.sharing_enabled
|
||||
|
||||
return if inbox
|
||||
|
||||
@question = 1
|
||||
@response[:render] = render_to_string(partial: "answerbox", locals: { a: answer, show_question: false })
|
||||
end
|
||||
|
||||
def destroy
|
||||
params.require :answer
|
||||
|
||||
answer = Answer.find(params[:answer])
|
||||
|
||||
unless (current_user == answer.user) || (privileged? answer.user)
|
||||
@response[:status] = :nopriv
|
||||
@response[:message] = t(".nopriv")
|
||||
return
|
||||
end
|
||||
|
||||
InboxEntry.create!(user: answer.user, question: answer.question, new: true, returning: true) if answer.user == current_user
|
||||
answer.destroy
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:message] = t(".success")
|
||||
@response[:success] = true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sharing_hash(answer) = {
|
||||
url: answer_share_url(answer),
|
||||
text: prepare_tweet(answer, nil, true),
|
||||
twitter: twitter_share_url(answer),
|
||||
bluesky: bluesky_share_url(answer),
|
||||
tumblr: tumblr_share_url(answer),
|
||||
telegram: telegram_share_url(answer),
|
||||
custom: CGI.escape(prepare_tweet(answer)),
|
||||
}
|
||||
end
|
|
@ -1,45 +0,0 @@
|
|||
class Ajax::CommentController < AjaxController
|
||||
def create
|
||||
params.require :answer
|
||||
params.require :comment
|
||||
|
||||
answer = Answer.find(params[:answer])
|
||||
|
||||
begin
|
||||
current_user.comment(answer, params[:comment])
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Sentry.capture_exception(e)
|
||||
@response[:status] = :rec_inv
|
||||
@response[:message] = t(".invalid")
|
||||
return
|
||||
end
|
||||
|
||||
comments = Comment.where(answer:).includes([{ user: :profile }, :smiles])
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:message] = t(".success")
|
||||
@response[:success] = true
|
||||
@response[:render] = render_to_string(partial: "answerbox/comments", locals: { a: answer, comments: })
|
||||
@response[:count] = answer.comment_count
|
||||
end
|
||||
|
||||
def destroy
|
||||
params.require :comment
|
||||
|
||||
@response[:status] = :err
|
||||
comment = Comment.find(params[:comment])
|
||||
|
||||
unless (current_user == comment.user) or (current_user == comment.answer.user) or (privileged? comment.user)
|
||||
@response[:status] = :nopriv
|
||||
@response[:message] = t(".nopriv")
|
||||
return
|
||||
end
|
||||
|
||||
@response[:count] = comment.answer.comment_count - 1
|
||||
comment.destroy
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:message] = t(".success")
|
||||
@response[:success] = true
|
||||
end
|
||||
end
|
|
@ -1,63 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Ajax::InboxController < AjaxController
|
||||
def remove
|
||||
params.require :id
|
||||
|
||||
inbox = InboxEntry.find(params[:id])
|
||||
|
||||
unless current_user == inbox.user
|
||||
@response[:status] = :fail
|
||||
@response[:message] = t(".error")
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
inbox.remove
|
||||
rescue => e
|
||||
Sentry.capture_exception(e)
|
||||
@response[:status] = :err
|
||||
@response[:message] = t("errors.base")
|
||||
return
|
||||
end
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:message] = t(".success")
|
||||
@response[:success] = true
|
||||
end
|
||||
|
||||
def remove_all
|
||||
raise unless user_signed_in?
|
||||
|
||||
begin
|
||||
InboxEntry.where(user: current_user).find_each(&:remove)
|
||||
rescue => e
|
||||
Sentry.capture_exception(e)
|
||||
@response[:status] = :err
|
||||
@response[:message] = t("errors.base")
|
||||
return
|
||||
end
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:message] = t(".success")
|
||||
@response[:success] = true
|
||||
end
|
||||
|
||||
def remove_all_author
|
||||
begin
|
||||
@target_user = User.where("LOWER(screen_name) = ?", params[:author].downcase).first!
|
||||
@inbox = current_user.inbox_entries.joins(:question)
|
||||
.where(questions: { user_id: @target_user.id, author_is_anonymous: false })
|
||||
@inbox.each(&:remove)
|
||||
rescue => e
|
||||
Sentry.capture_exception(e)
|
||||
@response[:status] = :err
|
||||
@response[:message] = t("errors.base")
|
||||
return
|
||||
end
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:message] = t(".success")
|
||||
@response[:success] = true
|
||||
end
|
||||
end
|
|
@ -1,61 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Ajax::ListController < AjaxController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def create
|
||||
params.require :name
|
||||
params.require :user
|
||||
|
||||
@response[:status] = :err
|
||||
|
||||
target_user = User.find_by!(screen_name: params[:user])
|
||||
list = List.create! user: current_user, display_name: params[:name]
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:success] = true
|
||||
@response[:message] = t(".success")
|
||||
@response[:render] = render_to_string(partial: "modal/list/item", locals: { list: list, user: target_user })
|
||||
end
|
||||
|
||||
def destroy
|
||||
params.require :list
|
||||
|
||||
@response[:status] = :err
|
||||
|
||||
List.where(user: current_user, name: params[:list]).first.destroy!
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:success] = true
|
||||
@response[:message] = t(".success")
|
||||
end
|
||||
|
||||
def membership
|
||||
params.require :user
|
||||
params.require :list
|
||||
params.require :add
|
||||
|
||||
@response[:status] = :err
|
||||
|
||||
add = params[:add] == "true"
|
||||
|
||||
target_user = User.find_by!(screen_name: params[:user])
|
||||
list = current_user.lists.find_by!(name: params[:list])
|
||||
|
||||
raise Errors::ListingSelfBlockedOther if current_user.blocking?(target_user)
|
||||
raise Errors::ListingOtherBlockedSelf if target_user.blocking?(current_user)
|
||||
|
||||
if add
|
||||
list.add_member target_user if list.members.find_by(user_id: target_user.id).nil?
|
||||
@response[:checked] = true
|
||||
@response[:message] = t(".success.add")
|
||||
else
|
||||
list.remove_member target_user unless list.members.find_by(user_id: target_user.id).nil?
|
||||
@response[:checked] = false
|
||||
@response[:message] = t(".success.remove")
|
||||
end
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:success] = true
|
||||
end
|
||||
end
|
|
@ -1,111 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Ajax::ModerationController < AjaxController
|
||||
def destroy_report
|
||||
params.require :id
|
||||
|
||||
report = Report.find(params[:id])
|
||||
|
||||
begin
|
||||
report.deleted = true
|
||||
report.save
|
||||
rescue => e
|
||||
Sentry.capture_exception(e)
|
||||
@response[:status] = :fail
|
||||
@response[:message] = t(".error")
|
||||
return
|
||||
end
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:message] = t(".success")
|
||||
@response[:success] = true
|
||||
end
|
||||
|
||||
def ban
|
||||
@response[:status] = :err
|
||||
@response[:message] = t(".error")
|
||||
|
||||
params.require :user
|
||||
params.require :ban
|
||||
|
||||
duration = params[:duration].to_i
|
||||
duration_unit = params[:duration_unit].to_s
|
||||
reason = params[:reason].to_s
|
||||
target_user = User.find_by_screen_name!(params[:user])
|
||||
unban = params[:ban] == '0'
|
||||
perma = params[:duration].blank?
|
||||
|
||||
if !unban && target_user.has_cached_role?(:administrator)
|
||||
@response[:status] = :nopriv
|
||||
@response[:message] = t(".nopriv")
|
||||
return
|
||||
end
|
||||
|
||||
if unban
|
||||
UseCase::User::Unban.call(target_user.id)
|
||||
@response[:message] = t(".success.unban")
|
||||
@response[:success] = true
|
||||
@response[:status] = :okay
|
||||
return
|
||||
elsif perma
|
||||
@response[:message] = t(".success.permanent")
|
||||
expiry = nil
|
||||
else
|
||||
params.require :duration
|
||||
params.require :duration_unit
|
||||
|
||||
raise Errors::InvalidBanDuration unless %w[hours days weeks months].include? duration_unit
|
||||
|
||||
expiry = DateTime.now + duration.public_send(duration_unit)
|
||||
@response[:message] = t(".success.temporary", date: expiry.to_s)
|
||||
end
|
||||
|
||||
UseCase::User::Ban.call(
|
||||
target_user_id: target_user.id,
|
||||
expiry: expiry,
|
||||
reason: reason,
|
||||
source_user_id: current_user.id)
|
||||
|
||||
target_user.save!
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:success] = true
|
||||
end
|
||||
|
||||
def privilege
|
||||
@response[:status] = :err
|
||||
|
||||
params.require :user
|
||||
params.require :type
|
||||
params.require :status
|
||||
|
||||
status = params[:status] == 'true'
|
||||
|
||||
target_user = User.find_by_screen_name!(params[:user])
|
||||
|
||||
@response[:message] = t(".error")
|
||||
return unless %w[moderator administrator].include? params[:type].downcase
|
||||
|
||||
unless current_user.has_cached_role?(:administrator)
|
||||
@response[:status] = :nopriv
|
||||
@response[:message] = t(".nopriv")
|
||||
return
|
||||
end
|
||||
|
||||
@response[:checked] = status
|
||||
type = params[:type].downcase
|
||||
target_role = type.to_sym
|
||||
|
||||
if status
|
||||
target_user.add_role target_role
|
||||
else
|
||||
target_user.remove_role target_role
|
||||
end
|
||||
target_user.save!
|
||||
|
||||
@response[:message] = t(".success", privilege: params[:type])
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:success] = true
|
||||
end
|
||||
end
|
|
@ -1,48 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Ajax::QuestionController < AjaxController
|
||||
def create
|
||||
params.require :question
|
||||
params.require :anonymousQuestion
|
||||
params.require :rcpt
|
||||
|
||||
# set up fake success response -- the use cases raise errors on exceptions
|
||||
# which get rescued by the base class
|
||||
@response = {
|
||||
success: true,
|
||||
message: t(".success"),
|
||||
status: :okay,
|
||||
}
|
||||
|
||||
if user_signed_in? && params[:rcpt] == "followers"
|
||||
UseCase::Question::CreateFollowers.call(
|
||||
source_user_id: current_user.id,
|
||||
content: params[:question],
|
||||
author_identifier: AnonymousBlock.get_identifier(request.remote_ip),
|
||||
send_to_own_inbox: params[:sendToOwnInbox],
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
UseCase::Question::Create.call(
|
||||
source_user_id: user_signed_in? ? current_user.id : nil,
|
||||
target_user_id: params[:rcpt],
|
||||
content: params[:question],
|
||||
anonymous: params[:anonymousQuestion],
|
||||
author_identifier: AnonymousBlock.get_identifier(request.remote_ip),
|
||||
)
|
||||
end
|
||||
|
||||
def destroy
|
||||
params.require :question
|
||||
|
||||
UseCase::Question::Destroy.call(
|
||||
question_id: params[:question],
|
||||
current_user:,
|
||||
)
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:message] = t(".success")
|
||||
@response[:success] = true
|
||||
end
|
||||
end
|
|
@ -1,45 +0,0 @@
|
|||
class Ajax::ReportController < AjaxController
|
||||
def create
|
||||
params.require :id
|
||||
params.require :type
|
||||
|
||||
@response[:status] = :err
|
||||
|
||||
unless user_signed_in?
|
||||
@response[:status] = :noauth
|
||||
@response[:message] = t(".noauth")
|
||||
return
|
||||
end
|
||||
|
||||
unless %w(answer comment question user).include? params[:type]
|
||||
@response[:message] = t(".unknown")
|
||||
return
|
||||
end
|
||||
|
||||
obj = params[:type].strip.capitalize
|
||||
|
||||
object = case obj
|
||||
when 'User'
|
||||
User.find_by_screen_name! params[:id]
|
||||
when 'Question'
|
||||
Question.find params[:id]
|
||||
when 'Answer'
|
||||
Answer.find params[:id]
|
||||
when 'Comment'
|
||||
Comment.find params[:id]
|
||||
else
|
||||
Answer.find params[:id]
|
||||
end
|
||||
|
||||
if object.nil?
|
||||
@response[:message] = t(".notfound", parameter: params[:type])
|
||||
return
|
||||
end
|
||||
|
||||
current_user.report object, params[:reason]
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:message] = t(".success", parameter: params[:type].titleize)
|
||||
@response[:success] = true
|
||||
end
|
||||
end
|
|
@ -1,58 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Ajax::WebPushController < AjaxController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def key
|
||||
certificate = Rpush::Webpush::App.find_by(name: "webpush").certificate
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:success] = true
|
||||
@response[:key] = JSON.parse(certificate)["public_key"]
|
||||
end
|
||||
|
||||
def check
|
||||
params.require(:endpoint)
|
||||
|
||||
found = current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).first
|
||||
|
||||
@response[:status] = if found
|
||||
if found.failures >= 3
|
||||
:failed
|
||||
else
|
||||
:subscribed
|
||||
end
|
||||
else
|
||||
:unsubscribed
|
||||
end
|
||||
@response[:success] = true
|
||||
end
|
||||
|
||||
def subscribe
|
||||
params.require(:subscription)
|
||||
|
||||
WebPushSubscription.create!(
|
||||
user: current_user,
|
||||
subscription: params[:subscription]
|
||||
)
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:success] = true
|
||||
@response[:message] = t(".subscription_count", count: current_user.web_push_subscriptions.count)
|
||||
end
|
||||
|
||||
def unsubscribe
|
||||
removed = if params.key?(:endpoint)
|
||||
current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).destroy_all
|
||||
else
|
||||
current_user.web_push_subscriptions.destroy_all
|
||||
end
|
||||
|
||||
count = current_user.web_push_subscriptions.count
|
||||
|
||||
@response[:status] = removed.any? ? :okay : :err
|
||||
@response[:success] = removed.any?
|
||||
@response[:message] = t(".subscription_count", count:)
|
||||
@response[:count] = count
|
||||
end
|
||||
end
|
|
@ -1,116 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AjaxController < ApplicationController
|
||||
skip_before_action :find_active_announcements
|
||||
before_action :build_response
|
||||
after_action :return_response
|
||||
|
||||
respond_to :json
|
||||
|
||||
unless Rails.env.development?
|
||||
rescue_from(StandardError) do |e|
|
||||
Sentry.capture_exception(e)
|
||||
|
||||
@response = {
|
||||
success: false,
|
||||
message: t("errors.base"),
|
||||
status: :err
|
||||
}
|
||||
|
||||
return_response
|
||||
end
|
||||
end
|
||||
|
||||
rescue_from(Errors::Base) do |e|
|
||||
Sentry.capture_exception(e)
|
||||
|
||||
@response = {
|
||||
success: false,
|
||||
message: e.message,
|
||||
status: e.code
|
||||
}
|
||||
|
||||
return_response
|
||||
end
|
||||
|
||||
rescue_from(KeyError) do |e|
|
||||
Sentry.capture_exception(e)
|
||||
|
||||
@response = {
|
||||
success: false,
|
||||
message: t("errors.parameter_error", parameter: e.key),
|
||||
status: :err
|
||||
}
|
||||
|
||||
return_response
|
||||
end
|
||||
|
||||
rescue_from(Dry::Types::CoercionError, Dry::Types::ConstraintError) do |e|
|
||||
Sentry.capture_exception(e)
|
||||
|
||||
@response = {
|
||||
success: false,
|
||||
message: t("errors.invalid_parameter"),
|
||||
status: :err
|
||||
}
|
||||
|
||||
return_response
|
||||
end
|
||||
|
||||
rescue_from(ActiveRecord::RecordNotFound) do |e|
|
||||
Sentry.capture_exception(e)
|
||||
|
||||
@response = {
|
||||
success: false,
|
||||
message: t("errors.record_not_found"),
|
||||
status: :not_found
|
||||
}
|
||||
|
||||
return_response
|
||||
end
|
||||
|
||||
rescue_from(ActionController::ParameterMissing) do |e|
|
||||
Sentry.capture_exception(e)
|
||||
|
||||
@response = {
|
||||
success: false,
|
||||
message: t("errors.parameter_error", parameter: e.param.capitalize),
|
||||
status: :parameter_error
|
||||
}
|
||||
|
||||
return_response
|
||||
end
|
||||
|
||||
rescue_from(Errors::Base) do |e|
|
||||
Sentry.capture_exception(e)
|
||||
|
||||
@response = {
|
||||
success: false,
|
||||
message: I18n.t(e.locale_tag),
|
||||
status: e.code
|
||||
}
|
||||
|
||||
return_response
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_response
|
||||
@response = {
|
||||
success: false,
|
||||
message: "",
|
||||
status: "unknown"
|
||||
}
|
||||
end
|
||||
|
||||
def return_response
|
||||
# Q: Why don't we just use render(json:) here?
|
||||
# A: Because otherwise Rails wants us to use views, which do not make much sense here.
|
||||
#
|
||||
# Q: Why do we always return 200?
|
||||
# A: Because JQuery might not do things we want it to if we don't.
|
||||
response.status = @status || 200
|
||||
response.headers["Content-Type"] = "application/json"
|
||||
response.body = @response.to_json
|
||||
end
|
||||
end
|
|
@ -1,57 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AnonymousBlockController < ApplicationController
|
||||
include TurboStreamable
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
turbo_stream_actions :create, :destroy
|
||||
|
||||
def create
|
||||
params.require :question
|
||||
|
||||
question = Question.find(params[:question])
|
||||
authorize AnonymousBlock, :create_global? if params[:global]
|
||||
|
||||
AnonymousBlock.create!(
|
||||
user: params[:global] ? nil : current_user,
|
||||
identifier: question.author_identifier,
|
||||
question:,
|
||||
target_user: question.user
|
||||
)
|
||||
|
||||
inbox_id = question.inbox_entries.first&.id
|
||||
question.inbox_entries.first&.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
inbox_id ? turbo_stream.remove("inbox_#{inbox_id}") : nil,
|
||||
render_toast(t(".success"))
|
||||
].compact
|
||||
end
|
||||
|
||||
format.html { redirect_back(fallback_location: inbox_path) }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
params.require :id
|
||||
|
||||
block = AnonymousBlock.find(params[:id])
|
||||
authorize block
|
||||
|
||||
block.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.remove("block_#{params[:id]}"),
|
||||
render_toast(t(".success"))
|
||||
]
|
||||
end
|
||||
|
||||
format.html { redirect_back(fallback_location: settings_blocks_path) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,60 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AnswerController < ApplicationController
|
||||
before_action :authenticate_user!, only: %i[pin unpin]
|
||||
|
||||
include TurboStreamable
|
||||
|
||||
turbo_stream_actions :pin, :unpin
|
||||
|
||||
def show
|
||||
@answer = Answer.for_user(current_user).includes(question: [:user], smiles: [:user]).find(params[:id])
|
||||
@display_all = true
|
||||
|
||||
return unless user_signed_in?
|
||||
|
||||
mark_notifications_as_read
|
||||
end
|
||||
|
||||
def pin
|
||||
answer = Answer.includes(:user).find(params[:id])
|
||||
UseCase::Answer::Pin.call(user: current_user, answer:)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(user_path(username: current_user.screen_name)) }
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.update("ab-pin-#{answer.id}", partial: "actions/pin", locals: { answer: }),
|
||||
render_toast(t(".success"))
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def unpin
|
||||
answer = Answer.includes(:user).find(params[:id])
|
||||
UseCase::Answer::Unpin.call(user: current_user, answer:)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(user_path(username: current_user.screen_name)) }
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.update("ab-pin-#{answer.id}", partial: "actions/pin", locals: { answer: }),
|
||||
render_toast(t(".success"))
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mark_notifications_as_read
|
||||
updated = Notification.where(recipient_id: current_user.id, new: true)
|
||||
.and(Notification.where(type: "Notification::QuestionAnswered", target_id: @answer.id)
|
||||
.or(Notification.where(type: "Notification::Commented", target_id: @answer.comments.pluck(:id)))
|
||||
.or(Notification.where(type: "Notification::Smiled", target_id: @answer.smiles.pluck(:id)))
|
||||
.or(Notification.where(type: "Notification::CommentSmiled", target_id: @answer.comment_smiles.pluck(:id))))
|
||||
.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
|
||||
current_user.touch(:notifications_updated_at) if updated.positive?
|
||||
end
|
||||
end
|
|
@ -7,9 +7,6 @@ class ApplicationController < ActionController::Base
|
|||
before_action :sentry_user_context
|
||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||
around_action :switch_locale
|
||||
before_action :banned?
|
||||
before_action :find_active_announcements
|
||||
before_action :set_has_new_reports
|
||||
|
||||
# check if user wants to read
|
||||
def switch_locale(&)
|
||||
|
@ -24,43 +21,6 @@ class ApplicationController < ActionController::Base
|
|||
I18n.with_locale(locale, &)
|
||||
end
|
||||
|
||||
# check if user got hit by the banhammer of doom
|
||||
def banned?
|
||||
if current_user.present? && current_user.banned?
|
||||
name = current_user.screen_name
|
||||
# obligatory '2001: A Space Odyssey' reference
|
||||
flash[:notice] = t("user.sessions.create.banned", name:)
|
||||
current_ban = current_user.bans.current.first
|
||||
flash[:notice] += "\n#{t('user.sessions.create.reason', reason: current_ban.reason)}" unless current_ban&.reason&.empty?
|
||||
|
||||
flash[:notice] += if current_ban&.permanent?
|
||||
"\n#{t('user.sessions.create.permanent')}"
|
||||
else
|
||||
# TODO: format banned_until
|
||||
"\n#{t('user.sessions.create.until', time: current_ban.expires_at)}"
|
||||
end
|
||||
|
||||
sign_out current_user
|
||||
redirect_to new_user_session_path
|
||||
end
|
||||
end
|
||||
|
||||
def find_active_announcements
|
||||
@active_announcements ||= Announcement.find_active
|
||||
end
|
||||
|
||||
def set_has_new_reports
|
||||
return unless current_user&.mod?
|
||||
|
||||
@has_new_reports = if current_user.last_reports_visit.nil?
|
||||
true
|
||||
else
|
||||
Report.where(deleted: false)
|
||||
.where("created_at > ?", current_user.last_reports_visit)
|
||||
.count.positive?
|
||||
end
|
||||
end
|
||||
|
||||
include ApplicationHelper
|
||||
|
||||
protected
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Comments::ReactionsController < ApplicationController
|
||||
def index
|
||||
comment = Comment.find(params[:id])
|
||||
@reactions = Reaction.where(parent_type: "Comment", parent: comment.id).includes([{ user: :profile }])
|
||||
|
||||
redirect_to answer_path(username: comment.answer.user.screen_name, id: comment.answer.id) unless turbo_frame_request?
|
||||
end
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CommentsController < ApplicationController
|
||||
def index
|
||||
answer = Answer.find(params[:id])
|
||||
@comments = Comment.where(answer:).includes([{ user: :profile }, :smiles])
|
||||
|
||||
render "index", locals: { a: answer }
|
||||
end
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PaginatesAnswers
|
||||
def paginate_answers
|
||||
@answers = yield(last_id: params[:last_id])
|
||||
answer_ids = @answers.map(&:id)
|
||||
@answers_last_id = answer_ids.min
|
||||
@more_data_available = !yield(last_id: @answers_last_id, size: 1).select("answers.id").count.zero?
|
||||
end
|
||||
end
|
|
@ -1,39 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module TurboStreamable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def turbo_stream_actions(*actions)
|
||||
around_action :handle_error, only: actions
|
||||
end
|
||||
end
|
||||
|
||||
def render_toast(message, success = true)
|
||||
turbo_stream.append("toasts", partial: "shared/toast", locals: { message:, success: })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_error
|
||||
yield
|
||||
rescue Errors::Base => e
|
||||
render_error I18n.t(e.locale_tag)
|
||||
rescue KeyError, ActionController::ParameterMissing => e
|
||||
render_error t("errors.parameter_error", parameter: e.instance_of?(KeyError) ? e.key : e.param.capitalize)
|
||||
rescue Dry::Types::CoercionError, Dry::Types::ConstraintError
|
||||
render_error t("errors.invalid_parameter")
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render_error e.record.errors.full_messages.flatten.join(" ")
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_error t("errors.record_not_found")
|
||||
end
|
||||
|
||||
def render_error(message)
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: render_toast(message, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,34 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DiscoverController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
return redirect_to root_path unless APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod?
|
||||
|
||||
top_x = 10 # only display the top X items
|
||||
week_ago = Time.now.utc.ago(1.week)
|
||||
|
||||
@popular_answers = Answer.for_user(current_user).where("created_at > ?", week_ago).order(:smile_count).reverse_order.limit(top_x).includes(:question, :user, :comments)
|
||||
@most_discussed = Answer.for_user(current_user).where("created_at > ?", week_ago).order(:comment_count).reverse_order.limit(top_x).includes(:question, :user, :comments)
|
||||
@popular_questions = Question.where("created_at > ?", week_ago).order(:answer_count).reverse_order.limit(top_x).includes(:user)
|
||||
@new_users = User.where("asked_count > 0").order(:id).reverse_order.limit(top_x).includes(:profile)
|
||||
|
||||
# .user = the user
|
||||
# .question_count = how many questions did the user ask
|
||||
@users_with_most_questions = Question.select("user_id, COUNT(*) AS question_count")
|
||||
.where("created_at > ?", week_ago)
|
||||
.where(author_is_anonymous: false)
|
||||
.group(:user_id)
|
||||
.order("question_count")
|
||||
.reverse_order.limit(top_x)
|
||||
|
||||
# .user = the user
|
||||
# .answer_count = how many questions did the user answer
|
||||
@users_with_most_answers = Answer.select("user_id, COUNT(*) AS answer_count")
|
||||
.where("created_at > ?", week_ago)
|
||||
.group(:user_id)
|
||||
.order("answer_count")
|
||||
.reverse_order.limit(top_x)
|
||||
end
|
||||
end
|
|
@ -1,32 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FeedbackController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :feature_enabled?
|
||||
before_action :canny_consent_given?, only: %w[features bugs]
|
||||
|
||||
def consent
|
||||
redirect_to feedback_bugs_path if current_user.has_cached_role? :canny_consent
|
||||
end
|
||||
|
||||
def update
|
||||
return unless params[:consent] == "true"
|
||||
|
||||
current_user.add_role :canny_consent
|
||||
redirect_to feedback_bugs_path
|
||||
end
|
||||
|
||||
def features; end
|
||||
|
||||
def bugs; end
|
||||
|
||||
private
|
||||
|
||||
def feature_enabled?
|
||||
redirect_to root_path if APP_CONFIG["canny"].nil?
|
||||
end
|
||||
|
||||
def canny_consent_given?
|
||||
redirect_to feedback_consent_path unless current_user.has_cached_role? :canny_consent
|
||||
end
|
||||
end
|
|
@ -1,77 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class InboxController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def show
|
||||
find_inbox_entries
|
||||
|
||||
@delete_id = find_delete_id
|
||||
@disabled = true if @inbox.empty?
|
||||
|
||||
mark_inbox_entries_as_read
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.turbo_stream
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
question = Question.create!(content: QuestionGenerator.generate,
|
||||
author_is_anonymous: true,
|
||||
author_identifier: "justask",
|
||||
user: current_user)
|
||||
|
||||
inbox = InboxEntry.create!(user: current_user, question_id: question.id, new: true)
|
||||
increment_metric
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: turbo_stream.prepend("entries", partial: "inbox/entry", locals: { i: inbox })
|
||||
|
||||
inbox.update(new: false)
|
||||
end
|
||||
|
||||
format.html { redirect_to inbox_path }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_params
|
||||
params.slice(*InboxFilter::KEYS).permit(*InboxFilter::KEYS)
|
||||
end
|
||||
|
||||
def find_inbox_entries
|
||||
filter = InboxFilter.new(current_user, filter_params)
|
||||
@inbox = filter.cursored_results(last_id: params[:last_id])
|
||||
@inbox_last_id = @inbox.map(&:id).min
|
||||
@more_data_available = filter.cursored_results(last_id: @inbox_last_id, size: 1).count.positive?
|
||||
@inbox_count = filter.results.count
|
||||
end
|
||||
|
||||
def find_delete_id
|
||||
return "ib-delete-all-author" if params[:author].present? && @inbox_count.positive?
|
||||
|
||||
"ib-delete-all"
|
||||
end
|
||||
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
def mark_inbox_entries_as_read
|
||||
# using .dup to not modify @inbox -- useful in tests
|
||||
updated = @inbox&.dup&.update_all(new: false)
|
||||
current_user.touch(:inbox_updated_at) if updated.positive?
|
||||
end
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
|
||||
def increment_metric
|
||||
Retrospring::Metrics::QUESTIONS_ASKED.increment(
|
||||
labels: {
|
||||
anonymous: true,
|
||||
followers: false,
|
||||
generated: true,
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class LinkFilterController < ApplicationController
|
||||
def index
|
||||
redirect_to root_path unless params[:url]
|
||||
|
||||
@link = params[:url]
|
||||
end
|
||||
end
|
|
@ -3,9 +3,6 @@
|
|||
class ManifestsController < ApplicationController
|
||||
include ThemeHelper
|
||||
|
||||
skip_before_action :banned?
|
||||
skip_before_action :find_active_announcements
|
||||
|
||||
def show
|
||||
expires_in 1.day
|
||||
return if fresh_when current_user&.theme
|
||||
|
@ -28,7 +25,6 @@ class ManifestsController < ApplicationController
|
|||
private
|
||||
|
||||
def shortcuts = [
|
||||
webapp_shortcut(inbox_url, t("navigation.inbox"), "inbox")
|
||||
]
|
||||
|
||||
def webapp_shortcut(url, name, icon_name)
|
||||
|
|
|
@ -4,8 +4,6 @@ class ModalController < ApplicationController
|
|||
include ActionView::Helpers::TagHelper
|
||||
include Turbo::FramesHelper
|
||||
|
||||
skip_before_action :find_active_announcements, :banned?
|
||||
|
||||
def close
|
||||
return redirect_to root_path unless turbo_frame_request?
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Moderation::AnonymousBlockController < ApplicationController
|
||||
def index
|
||||
@anonymous_blocks = AnonymousBlock.where(user: nil)
|
||||
end
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Moderation::InboxController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@user = User.find_by(screen_name: params[:user])
|
||||
filter = InboxFilter.new(@user, filter_params)
|
||||
|
||||
@inboxes = filter.cursored_results(last_id: params[:last_id])
|
||||
@inbox_last_id = @inboxes.map(&:id).min
|
||||
@more_data_available = !filter.cursored_results(last_id: @inbox_last_id, size: 1).count.zero?
|
||||
@inbox_count = @user.inbox_entries.count
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.turbo_stream { render "index", layout: false, status: :see_other }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_params
|
||||
params.slice(*InboxFilter::KEYS).permit(*InboxFilter::KEYS)
|
||||
end
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Moderation::QuestionsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def show
|
||||
@questions = Question.where(author_identifier: params[:author_identifier])
|
||||
end
|
||||
end
|
|
@ -1,48 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Moderation::ReportsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_filter_enabled
|
||||
before_action :set_type_options
|
||||
before_action :set_last_reports_visit
|
||||
|
||||
def index
|
||||
filter = ReportFilter.new(filter_params)
|
||||
@reports = filter.cursored_results(last_id: params[:last_id])
|
||||
@reports_last_id = @reports.map(&:id).min
|
||||
@more_data_available = filter.cursored_results(last_id: @reports_last_id, size: 1).count.positive?
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.turbo_stream { render "index", layout: false, status: :see_other }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_params
|
||||
params.slice(*ReportFilter::KEYS).permit(*ReportFilter::KEYS)
|
||||
end
|
||||
|
||||
def set_filter_enabled
|
||||
@filter_enabled = params.slice(*ReportFilter::KEYS)
|
||||
.reject! { |_, value| value.empty? || value.nil? }
|
||||
.values
|
||||
.any?
|
||||
end
|
||||
|
||||
def set_type_options
|
||||
@type_options = [
|
||||
[t("voc.all"), ""],
|
||||
[t("activerecord.models.answer.one"), :answer],
|
||||
[t("activerecord.models.comment.one"), :comment],
|
||||
[t("activerecord.models.question.one"), :question],
|
||||
[t("activerecord.models.user.one"), :user]
|
||||
]
|
||||
end
|
||||
|
||||
def set_last_reports_visit
|
||||
current_user.last_reports_visit = DateTime.now
|
||||
current_user.save
|
||||
end
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
class ModerationController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def toggle_unmask
|
||||
session[:moderation_view] = !session[:moderation_view]
|
||||
redirect_back fallback_location: root_path
|
||||
end
|
||||
end
|
|
@ -1,71 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NotificationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
TYPE_MAPPINGS = {
|
||||
"answer" => Notification::QuestionAnswered.name,
|
||||
"comment" => Notification::Commented.name,
|
||||
"commentsmile" => Notification::CommentSmiled.name,
|
||||
"relationship" => Notification::StartedFollowing.name,
|
||||
"smile" => Notification::Smiled.name
|
||||
}.freeze
|
||||
|
||||
def index
|
||||
@type = TYPE_MAPPINGS[params[:type]] || params[:type]
|
||||
@notifications = cursored_notifications_for(type: @type, last_id: params[:last_id])
|
||||
paginate_notifications
|
||||
@counters = count_unread_by_type
|
||||
mark_notifications_as_read
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.turbo_stream { render layout: false, status: :see_other }
|
||||
end
|
||||
end
|
||||
|
||||
def read
|
||||
current_user.notifications.where(new: true).update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
|
||||
current_user.touch(:notifications_updated_at)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render "navigation/notifications", locals: { notifications: [], notification_count: nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def paginate_notifications
|
||||
@notifications_last_id = @notifications.map(&:id).min
|
||||
@more_data_available = !cursored_notifications_for(type: @type, last_id: @notifications_last_id, size: 1).count.zero?
|
||||
end
|
||||
|
||||
def count_unread_by_type
|
||||
Notification.where(recipient: current_user, new: true)
|
||||
.group(:target_type)
|
||||
.count(:target_type)
|
||||
end
|
||||
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
def mark_notifications_as_read
|
||||
# using .dup to not modify @notifications -- useful in tests
|
||||
updated = @notifications&.dup&.update_all(new: false)
|
||||
current_user.touch(:notifications_updated_at) if updated.positive?
|
||||
end
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
|
||||
def cursored_notifications_for(type:, last_id:, size: nil)
|
||||
cursor_params = { last_id: last_id, size: size }.compact
|
||||
|
||||
case type
|
||||
when "all"
|
||||
Notification.cursored_for(current_user, **cursor_params)
|
||||
when "new"
|
||||
Notification.cursored_for(current_user, new: true, **cursor_params)
|
||||
else
|
||||
Notification.cursored_for_type(current_user, type, **cursor_params)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,18 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QuestionController < ApplicationController
|
||||
include PaginatesAnswers
|
||||
|
||||
def show
|
||||
@question = Question.find(params[:id])
|
||||
@answers = @question.cursored_answers(last_id: params[:last_id], current_user:)
|
||||
answer_ids = @answers.map(&:id)
|
||||
@answers_last_id = answer_ids.min
|
||||
@more_data_available = !@question.cursored_answers(last_id: @answers_last_id, size: 1, current_user:).select("answers.id").count.zero?
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.turbo_stream { render layout: false, status: :see_other }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,75 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ReactionsController < ApplicationController
|
||||
include TurboStreamable
|
||||
|
||||
before_action :authenticate_user!, only: %w[create destroy]
|
||||
|
||||
turbo_stream_actions :create, :destroy
|
||||
|
||||
def index
|
||||
answer = Answer.includes([smiles: { user: :profile }]).find(params[:id])
|
||||
|
||||
render "index", locals: { a: answer }
|
||||
end
|
||||
|
||||
def create
|
||||
params.require :id
|
||||
|
||||
target = target_class.find(params[:id])
|
||||
|
||||
UseCase::Reaction::Create.call(
|
||||
source_user_id: current_user.id,
|
||||
target:,
|
||||
)
|
||||
|
||||
target.reload
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace("reaction-#{params[:type]}-#{params[:id]}", partial: "reactions/destroy", locals: { type: params[:type], target: }),
|
||||
render_toast(t(".#{params[:type].downcase}.success"))
|
||||
]
|
||||
end
|
||||
|
||||
format.html { redirect_back(fallback_location: root_path) }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
params.require :id
|
||||
|
||||
target = target_class.find(params[:id])
|
||||
|
||||
UseCase::Reaction::Destroy.call(
|
||||
source_user_id: current_user.id,
|
||||
target:,
|
||||
)
|
||||
|
||||
target.reload
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace("reaction-#{params[:type]}-#{params[:id]}", partial: "reactions/create", locals: { type: params[:type], target: }),
|
||||
render_toast(t(".#{params[:type].downcase}.success"))
|
||||
]
|
||||
end
|
||||
|
||||
format.html { redirect_back(fallback_location: root_path) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
ALLOWED_TYPES = %w[Answer Comment].freeze
|
||||
private_constant :ALLOWED_TYPES
|
||||
|
||||
def target_class
|
||||
params.require :type
|
||||
raise NameError unless ALLOWED_TYPES.include?(params[:type])
|
||||
|
||||
params[:type].constantize
|
||||
end
|
||||
end
|
|
@ -1,49 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RelationshipsController < ApplicationController
|
||||
include TurboStreamable
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
turbo_stream_actions :create, :destroy
|
||||
|
||||
def create
|
||||
params.require :screen_name
|
||||
|
||||
UseCase::Relationship::Create.call(
|
||||
source_user: current_user,
|
||||
target_user: ::User.find_by!(screen_name: params[:screen_name]),
|
||||
type: params[:type],
|
||||
)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace("#{params[:type]}-#{params[:screen_name]}", partial: "relationships/destroy", locals: { type: params[:type], screen_name: params[:screen_name] }),
|
||||
render_toast(t(".#{params[:type]}.success"))
|
||||
]
|
||||
end
|
||||
|
||||
format.html { redirect_back(fallback_location: user_path(username: params[:screen_name])) }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
UseCase::Relationship::Destroy.call(
|
||||
source_user: current_user,
|
||||
target_user: ::User.find_by!(screen_name: params[:screen_name]),
|
||||
type: params[:type],
|
||||
)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace("#{params[:type]}-#{params[:screen_name]}", partial: "relationships/create", locals: { type: params[:type], screen_name: params[:screen_name] }),
|
||||
render_toast(t(".#{params[:type]}.success"))
|
||||
]
|
||||
end
|
||||
|
||||
format.html { redirect_back(fallback_location: user_path(username: params[:screen_name])) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::BlocksController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@blocks = Relationships::Block.where(source: current_user)
|
||||
@anonymous_blocks = AnonymousBlock.where(user: current_user)
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::DataController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index; end
|
||||
end
|
|
@ -2,31 +2,12 @@
|
|||
|
||||
class Settings::ExportController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :mark_notifications_as_read, only: %i[index]
|
||||
|
||||
def index
|
||||
flash[:info] = t(".info") if current_user.export_processing
|
||||
end
|
||||
|
||||
def create
|
||||
if current_user.can_export?
|
||||
ExportWorker.perform_async(current_user.id)
|
||||
flash[:success] = t(".success")
|
||||
else
|
||||
flash[:error] = t(".error")
|
||||
end
|
||||
|
||||
redirect_to settings_export_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
def mark_notifications_as_read
|
||||
updated = Notification::DataExported
|
||||
.where(recipient: current_user, new: true)
|
||||
.update_all(new: false)
|
||||
current_user.touch(:notifications_updated_at) if updated.positive?
|
||||
end
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::MutesController < ApplicationController
|
||||
include TurboStreamable
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
turbo_stream_actions :create, :destroy
|
||||
|
||||
def index
|
||||
@users = current_user.muted_users
|
||||
@rules = MuteRule.where(user: current_user)
|
||||
end
|
||||
|
||||
def create
|
||||
result = UseCase::MuteRule::Create.call(user: current_user, phrase: params[:muted_phrase])
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace("form", partial: "settings/mutes/form"),
|
||||
turbo_stream.append("rules", partial: "settings/mutes/rule", locals: { rule: result[:resource] }),
|
||||
render_toast(t(".success"))
|
||||
]
|
||||
end
|
||||
|
||||
format.html { redirect_to settings_muted_path }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
rule = MuteRule.find(params[:id])
|
||||
|
||||
authorize rule
|
||||
|
||||
UseCase::MuteRule::Destroy.call(rule:)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.remove("rule_#{params[:id]}"),
|
||||
render_toast(t(".success"))
|
||||
]
|
||||
end
|
||||
|
||||
format.html { redirect_to settings_muted_path }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,24 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::PrivacyController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
user_attributes = params.require(:user).permit(:privacy_lock_inbox,
|
||||
:privacy_allow_anonymous_questions,
|
||||
:privacy_allow_public_timeline,
|
||||
:privacy_allow_stranger_answers,
|
||||
:privacy_show_in_search,
|
||||
:privacy_require_user,
|
||||
:privacy_noindex,
|
||||
:privacy_hide_social_graph)
|
||||
if current_user.update(user_attributes)
|
||||
flash[:success] = t(".success")
|
||||
else
|
||||
flash[:error] = t(".error")
|
||||
end
|
||||
render :edit
|
||||
end
|
||||
end
|
|
@ -1,19 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ProfileController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
profile_attributes = params.require(:profile).permit(:display_name, :motivation_header, :website, :location, :description, :anon_display_name, :allow_long_questions)
|
||||
|
||||
if current_user.profile.update(profile_attributes)
|
||||
flash[:success] = t(".success")
|
||||
else
|
||||
flash[:error] = t(".error")
|
||||
end
|
||||
|
||||
render :edit
|
||||
end
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ProfilePictureController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def update
|
||||
user_attributes = params.require(:user).permit(:show_foreign_themes, :profile_picture_x, :profile_picture_y, :profile_picture_w, :profile_picture_h,
|
||||
:profile_header_x, :profile_header_y, :profile_header_w, :profile_header_h, :profile_picture, :profile_header)
|
||||
if current_user.update(user_attributes)
|
||||
text = t(".success")
|
||||
text += t(".notice.profile_picture") if user_attributes[:profile_picture]
|
||||
text += t(".notice.profile_header") if user_attributes[:profile_header]
|
||||
flash[:success] = text
|
||||
else
|
||||
# CarrierWave resets the image to the default upon an error
|
||||
current_user.reload
|
||||
end
|
||||
|
||||
render "settings/profile/edit"
|
||||
end
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::PushNotificationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@subscriptions = current_user.web_push_subscriptions.active
|
||||
end
|
||||
end
|
|
@ -1,20 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::SharingController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
user_attributes = params.require(:user).permit(:sharing_enabled,
|
||||
:sharing_autoclose,
|
||||
:sharing_custom_url)
|
||||
if current_user.update(user_attributes)
|
||||
flash.now[:success] = t(".success")
|
||||
else
|
||||
flash.now[:error] = t(".error")
|
||||
end
|
||||
|
||||
render :edit
|
||||
end
|
||||
end
|
|
@ -1,51 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ThemeController < ApplicationController
|
||||
include ThemeHelper
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
if current_user.theme.nil?
|
||||
current_user.theme = Theme.new theme_attributes
|
||||
current_user.theme.user_id = current_user.id
|
||||
|
||||
if current_user.theme.save
|
||||
flash[:success] = t(".success")
|
||||
else
|
||||
flash[:error] = t(".error", errors: current_user.theme.errors.messages.flatten.join(" "))
|
||||
end
|
||||
elsif current_user.theme.update(theme_attributes)
|
||||
flash[:success] = t(".success")
|
||||
else
|
||||
flash[:error] = t(".error", errors: current_user.theme.errors.messages.flatten.join(" "))
|
||||
end
|
||||
render :edit
|
||||
end
|
||||
|
||||
def destroy
|
||||
current_user.theme.destroy!
|
||||
redirect_to edit_settings_theme_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def theme_attributes
|
||||
params.require(:theme).permit(%i[
|
||||
primary_color primary_text
|
||||
danger_color danger_text
|
||||
success_color success_text
|
||||
warning_color warning_text
|
||||
info_color info_text
|
||||
dark_color dark_text
|
||||
light_color light_text
|
||||
raised_background raised_accent
|
||||
raised_text raised_accent_text
|
||||
background_color body_text
|
||||
muted_text input_color
|
||||
input_text input_placeholder
|
||||
])
|
||||
end
|
||||
end
|
|
@ -1,47 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::TwoFactorAuthentication::OtpAuthenticationController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
if current_user.otp_module_disabled?
|
||||
current_user.otp_secret_key = User.otp_random_secret(25)
|
||||
current_user.save
|
||||
|
||||
qr_code = RQRCode::QRCode.new(current_user.provisioning_uri("Retrospring:#{current_user.screen_name}", issuer: "Retrospring"))
|
||||
|
||||
@qr_svg = qr_code.as_svg({ offset: 4, module_size: 4, color: "000;fill:var(--primary)" }).html_safe
|
||||
else
|
||||
@recovery_code_count = current_user.totp_recovery_codes.count
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
req_params = params.require(:user).permit(:otp_validation)
|
||||
current_user.otp_module = :enabled
|
||||
|
||||
if current_user.authenticate_otp(req_params[:otp_validation], drift: APP_CONFIG.fetch(:otp_drift_period, 30).to_i)
|
||||
@recovery_keys = TotpRecoveryCode.generate_for(current_user)
|
||||
current_user.save!
|
||||
|
||||
render "settings/two_factor_authentication/otp_authentication/recovery_keys"
|
||||
else
|
||||
flash[:error] = t(".error")
|
||||
redirect_to settings_two_factor_authentication_otp_authentication_path
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
current_user.otp_module = :disabled
|
||||
current_user.save!
|
||||
current_user.totp_recovery_codes.delete_all
|
||||
flash[:success] = t(".success")
|
||||
redirect_to settings_two_factor_authentication_otp_authentication_path, status: :see_other
|
||||
end
|
||||
|
||||
def reset
|
||||
current_user.totp_recovery_codes.delete_all
|
||||
@recovery_keys = TotpRecoveryCode.generate_for(current_user)
|
||||
render "settings/two_factor_authentication/otp_authentication/recovery_keys", status: :see_other
|
||||
end
|
||||
end
|
|
@ -1,41 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SubscriptionsController < ApplicationController
|
||||
include TurboStreamable
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
turbo_stream_actions :create, :destroy
|
||||
|
||||
def create
|
||||
answer = Answer.find(params[:answer])
|
||||
result = Subscription.subscribe(current_user, answer)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace("subscription-#{answer.id}", partial: "subscriptions/destroy", locals: { answer: }),
|
||||
render_toast(t(result.present? ? ".success" : ".error"), result.present?)
|
||||
]
|
||||
end
|
||||
|
||||
format.html { redirect_to answer_path(username: answer.user.screen_name, id: answer.id) }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
answer = Answer.find(params[:answer])
|
||||
result = Subscription.unsubscribe(current_user, answer)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace("subscription-#{answer.id}", partial: "subscriptions/create", locals: { answer: }),
|
||||
render_toast(t(result.present? ? ".success" : ".error"), result.present?)
|
||||
]
|
||||
end
|
||||
|
||||
format.html { redirect_to answer_path(username: answer.user.screen_name, id: answer.id) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,44 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TimelineController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_list, only: %i[list]
|
||||
before_action :set_lists
|
||||
|
||||
def index
|
||||
paginate_timeline { |args| current_user.cursored_timeline(**args) }
|
||||
end
|
||||
|
||||
def list
|
||||
@title = list_title(@list)
|
||||
paginate_timeline { |args| @list.cursored_timeline(**args, current_user:) }
|
||||
end
|
||||
|
||||
def public
|
||||
@title = generate_title(t(".title"))
|
||||
paginate_timeline { |args| Answer.cursored_public_timeline(**args, current_user:) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_list
|
||||
@list = current_user.lists.find_by!(name: params[:list_name]) if params[:list_name].present?
|
||||
end
|
||||
|
||||
def set_lists
|
||||
@lists = current_user.lists
|
||||
@lists = @lists.where.not(id: @list.id) if @list.present?
|
||||
end
|
||||
|
||||
def paginate_timeline
|
||||
@timeline = yield(last_id: params[:last_id])
|
||||
timeline_ids = @timeline.select("answers.id").map(&:id)
|
||||
@timeline_last_id = timeline_ids.min
|
||||
@more_data_available = !yield(last_id: @timeline_last_id, size: 1).select("answers.id").count.zero?
|
||||
|
||||
respond_to do |format|
|
||||
format.html { render "timeline/timeline" }
|
||||
format.turbo_stream { render "timeline/timeline", layout: false, status: :see_other }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,21 +3,8 @@
|
|||
class User::RegistrationsController < Devise::RegistrationsController
|
||||
before_action :redirect_if_registrations_disabled!, only: %w[create new] # rubocop:disable Rails/LexicallyScopedActionFilter
|
||||
|
||||
def create
|
||||
if captcha_valid?
|
||||
super
|
||||
else
|
||||
respond_with_navigational(resource) { redirect_to new_user_registration_path }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if resource.export_processing
|
||||
flash[:error] = t(".export_pending")
|
||||
redirect_to edit_user_registration_path
|
||||
return
|
||||
end
|
||||
|
||||
# TODO: destroy export async
|
||||
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
|
||||
resource.destroy
|
||||
set_flash_message :notice, :destroyed if is_flashing_format?
|
||||
|
@ -35,6 +22,6 @@ class User::RegistrationsController < Devise::RegistrationsController
|
|||
end
|
||||
|
||||
def redirect_if_registrations_disabled!
|
||||
redirect_to root_path unless Retrospring::Config.registrations_enabled?
|
||||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserController < ApplicationController
|
||||
include PaginatesAnswers
|
||||
|
||||
before_action :set_user
|
||||
before_action :hidden_social_graph_redirect, only: %i[followers followings]
|
||||
after_action :mark_notification_as_read, only: %i[show]
|
||||
|
||||
def show
|
||||
@pinned_answers = @user.answers.for_user(current_user).pinned.includes([{ user: :profile }, :question]).order(pinned_at: :desc).limit(10).load_async
|
||||
paginate_answers { |args| @user.cursored_answers(current_user_id: current_user, **args) }
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.turbo_stream { render layout: false }
|
||||
end
|
||||
end
|
||||
|
||||
def followers
|
||||
paginate_relationships(:cursored_follower_relationships)
|
||||
@users = @relationships.map(&:source)
|
||||
own_relationships = find_own_relationships
|
||||
locals = {
|
||||
type: :follower,
|
||||
own_followings: own_relationships[Relationships::Follow],
|
||||
own_blocks: own_relationships[Relationships::Block],
|
||||
own_mutes: own_relationships[Relationships::Mute]
|
||||
}
|
||||
|
||||
respond_to do |format|
|
||||
format.html { render "show_follow", locals: }
|
||||
format.turbo_stream { render "show_follow", locals: }
|
||||
end
|
||||
end
|
||||
|
||||
def followings
|
||||
paginate_relationships(:cursored_following_relationships)
|
||||
@users = @relationships.map(&:target)
|
||||
own_relationships = find_own_relationships
|
||||
locals = {
|
||||
type: :friend,
|
||||
own_followings: own_relationships[Relationships::Follow],
|
||||
own_blocks: own_relationships[Relationships::Block],
|
||||
own_mutes: own_relationships[Relationships::Mute]
|
||||
}
|
||||
|
||||
respond_to do |format|
|
||||
format.html { render "show_follow", locals: }
|
||||
format.turbo_stream { render "show_follow", locals: }
|
||||
end
|
||||
end
|
||||
|
||||
def questions
|
||||
@questions = @user.cursored_questions(author_is_anonymous: false, direct: direct_param, last_id: params[:last_id])
|
||||
@questions_last_id = @questions.map(&:id).min
|
||||
@more_data_available = !@user.cursored_questions(author_is_anonymous: false, direct: direct_param, last_id: @questions_last_id, size: 1).count.zero?
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.turbo_stream
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mark_notification_as_read
|
||||
return unless user_signed_in?
|
||||
|
||||
Notification
|
||||
.where(
|
||||
target_type: "Relationship",
|
||||
target_id: @user.active_follow_relationships.where(target_id: current_user.id).pluck(:id),
|
||||
recipient_id: current_user.id,
|
||||
new: true
|
||||
).update(new: false)
|
||||
end
|
||||
|
||||
def set_user
|
||||
@user = User.where("LOWER(screen_name) = ?", params[:username].downcase).includes(:profile).first!
|
||||
end
|
||||
|
||||
def find_own_relationships
|
||||
return {} unless user_signed_in?
|
||||
|
||||
Relationship.where(source: current_user, target_id: @users.map(&:id))
|
||||
&.select(:target_id, :type)
|
||||
&.group_by(&:type)
|
||||
end
|
||||
|
||||
def paginate_relationships(method)
|
||||
@relationships = @user.public_send(method, last_id: params[:last_id])
|
||||
@relationships_last_id = @relationships.map(&:id).min
|
||||
@more_data_available = !@user.public_send(method, last_id: @relationships_last_id, size: 1).count.zero?
|
||||
end
|
||||
|
||||
def hidden_social_graph_redirect
|
||||
return if belongs_to_current_user? || !@user.privacy_hide_social_graph
|
||||
|
||||
redirect_to user_path(@user)
|
||||
end
|
||||
|
||||
def direct_param
|
||||
# return `nil` instead of `false` so we retrieve all questions for the user, direct or not.
|
||||
# `cursored_questions` will then remove the `direct` field from the WHERE query. otherwise the query filters
|
||||
# for `WHERE direct = false` ...
|
||||
return if belongs_to_current_user? || moderation_view?
|
||||
|
||||
# page is not being viewed by the current user, and we're not in the moderation view -> only show public questions
|
||||
false
|
||||
end
|
||||
|
||||
def belongs_to_current_user? = @user == current_user
|
||||
end
|
|
@ -1,4 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AjaxHelper
|
||||
end
|
|
@ -1,14 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationHelper
|
||||
include ApplicationHelper::GraphMethods
|
||||
include ApplicationHelper::TitleMethods
|
||||
|
||||
def privileged?(user)
|
||||
!current_user.nil? && ((current_user == user) || current_user.mod?)
|
||||
end
|
||||
|
||||
def rails_admin_path_for_resource(resource)
|
||||
[rails_admin_path, resource.model_name.param_key, resource.id].join("/")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationHelper::GraphMethods
|
||||
# Creates <meta> tags for OpenGraph properties from a hash
|
||||
# @param values [Hash]
|
||||
def opengraph_meta_tags(values)
|
||||
safe_join(values.map { |name, content| tag.meta(property: name, content:) }, "\n")
|
||||
end
|
||||
|
||||
# Creates <meta> tags from a hash
|
||||
# @param values [Hash]
|
||||
def meta_tags(values)
|
||||
safe_join(values.map { |name, content| tag.meta(name:, content:) }, "\n")
|
||||
end
|
||||
|
||||
# @param user [User]
|
||||
def user_opengraph(user)
|
||||
opengraph_meta_tags({
|
||||
"og:title": user.profile.safe_name,
|
||||
"og:type": "profile",
|
||||
"og:image": full_profile_picture_url(user),
|
||||
"og:url": user_url(user),
|
||||
"og:description": user.profile.description,
|
||||
"og:site_name": APP_CONFIG["site_name"],
|
||||
"profile:username": user.screen_name,
|
||||
})
|
||||
end
|
||||
|
||||
# @param user [User]
|
||||
def user_twitter_card(user)
|
||||
meta_tags({
|
||||
"twitter:card": "summary",
|
||||
"twitter:site": "@retrospring",
|
||||
"twitter:title": user.profile.motivation_header.presence || "Ask me anything!",
|
||||
"twitter:description": "Ask #{user.profile.safe_name} anything on Retrospring",
|
||||
"twitter:image": full_profile_picture_url(user),
|
||||
})
|
||||
end
|
||||
|
||||
# @param answer [Answer]
|
||||
def answer_opengraph(answer)
|
||||
opengraph_meta_tags({
|
||||
"og:title": "#{answer.user.profile.safe_name} answered: #{answer.question.content}",
|
||||
"og:type": "article",
|
||||
"og:image": full_profile_picture_url(answer.user),
|
||||
"og:url": answer_url(answer.user.screen_name, answer.id),
|
||||
"og:description": answer.content,
|
||||
"og:site_name": APP_CONFIG["site_name"],
|
||||
})
|
||||
end
|
||||
|
||||
def full_profile_picture_url(user)
|
||||
# @type [String]
|
||||
profile_picture_url = user.profile_picture.url(:large)
|
||||
if profile_picture_url.start_with? "/"
|
||||
"#{root_url}#{profile_picture_url[1..]}"
|
||||
else
|
||||
profile_picture_url
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
module ApplicationHelper::TitleMethods
|
||||
include MarkdownHelper
|
||||
include UserHelper
|
||||
|
||||
def generate_title(name, junction = nil, content = nil, possessive = false)
|
||||
if possessive
|
||||
|
@ -24,43 +23,4 @@ module ApplicationHelper::TitleMethods
|
|||
|
||||
list.join " "
|
||||
end
|
||||
|
||||
def question_title(question)
|
||||
context_user = question.answers&.first&.user if question.direct
|
||||
name = user_screen_name question.user,
|
||||
context_user:,
|
||||
author_identifier: question.author_is_anonymous ? question.author_identifier : nil,
|
||||
url: false
|
||||
generate_title name, "asked", question.content
|
||||
end
|
||||
|
||||
def answer_title(answer)
|
||||
name = user_screen_name answer.user, url: false
|
||||
generate_title name, "answered", answer.question.content
|
||||
end
|
||||
|
||||
def user_title(user, junction = nil)
|
||||
name = user_screen_name user, url: false
|
||||
generate_title name, junction, nil, !junction.nil?
|
||||
end
|
||||
|
||||
def questions_title(user)
|
||||
user_title user, "questions"
|
||||
end
|
||||
|
||||
def answers_title(user)
|
||||
user_title user, "answers"
|
||||
end
|
||||
|
||||
def smiles_title(user)
|
||||
user_title user, "smiles"
|
||||
end
|
||||
|
||||
def comments_title(user)
|
||||
user_title user, "comments"
|
||||
end
|
||||
|
||||
def list_title(list)
|
||||
generate_title list.display_name
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module FeedbackHelper
|
||||
def canny_token
|
||||
return if current_user.nil?
|
||||
|
||||
user_data = {
|
||||
avatarURL: current_user.profile_picture.url(:large),
|
||||
name: current_user.screen_name,
|
||||
id: current_user.id,
|
||||
email: current_user.email,
|
||||
}
|
||||
|
||||
JWT.encode(user_data, APP_CONFIG.dig("canny", "sso"))
|
||||
end
|
||||
end
|
|
@ -1,20 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ModerationHelper
|
||||
# @param report [Report]
|
||||
def content_url(report)
|
||||
target = report.target
|
||||
case report.type
|
||||
when "Reports::Answer"
|
||||
answer_path target.user.screen_name, target.id
|
||||
when "Reports::Comment"
|
||||
answer_path target.answer.user.screen_name, target.answer.id
|
||||
when "Reports::Question"
|
||||
question_path "user", target.id
|
||||
when "Reports::User"
|
||||
user_path target
|
||||
else
|
||||
"#"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,17 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SocialHelper
|
||||
include SocialHelper::BlueskyMethods
|
||||
include SocialHelper::TwitterMethods
|
||||
include SocialHelper::TumblrMethods
|
||||
include SocialHelper::TelegramMethods
|
||||
|
||||
def answer_share_url(answer)
|
||||
answer_url(
|
||||
id: answer.id,
|
||||
username: answer.user.screen_name,
|
||||
host: APP_CONFIG["hostname"],
|
||||
protocol: (APP_CONFIG["https"] ? :https : :http),
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "cgi"
|
||||
|
||||
module SocialHelper::BlueskyMethods
|
||||
def bluesky_share_url(answer)
|
||||
"https://bsky.app/intent/compose?text=#{CGI.escape(prepare_tweet(answer))}"
|
||||
end
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "cgi"
|
||||
|
||||
module SocialHelper::TelegramMethods
|
||||
include MarkdownHelper
|
||||
|
||||
def telegram_text(answer)
|
||||
# using twitter_markdown here as it removes all formatting
|
||||
"#{twitter_markdown answer.question.content}\n———\n#{twitter_markdown answer.content}"
|
||||
end
|
||||
|
||||
def telegram_share_url(answer)
|
||||
url = answer_url(
|
||||
id: answer.id,
|
||||
username: answer.user.screen_name,
|
||||
host: APP_CONFIG["hostname"],
|
||||
protocol: (APP_CONFIG["https"] ? :https : :http),
|
||||
)
|
||||
|
||||
%(https://t.me/share/url?url=#{CGI.escape(url)}&text=#{CGI.escape(telegram_text(answer))})
|
||||
end
|
||||
end
|
|
@ -1,37 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "cgi"
|
||||
|
||||
module SocialHelper::TumblrMethods
|
||||
def tumblr_title(answer)
|
||||
asker = if answer.question.author_is_anonymous?
|
||||
answer.user.profile.anon_display_name.presence || APP_CONFIG["anonymous_name"]
|
||||
else
|
||||
answer.question.user.profile.safe_name
|
||||
end
|
||||
|
||||
"#{asker} asked: #{answer.question.content}"
|
||||
end
|
||||
|
||||
def tumblr_body(answer)
|
||||
answer_url = answer_url(
|
||||
id: answer.id,
|
||||
username: answer.user.screen_name,
|
||||
host: APP_CONFIG["hostname"],
|
||||
protocol: (APP_CONFIG["https"] ? :https : :http),
|
||||
)
|
||||
|
||||
"#{answer.content}\n\n[Smile or comment on the answer here](#{answer_url})"
|
||||
end
|
||||
|
||||
def tumblr_share_url(answer)
|
||||
answer_url = answer_url(
|
||||
id: answer.id,
|
||||
username: answer.user.screen_name,
|
||||
host: APP_CONFIG["hostname"],
|
||||
protocol: (APP_CONFIG["https"] ? :https : :http),
|
||||
)
|
||||
|
||||
"https://www.tumblr.com/widgets/share/tool?shareSource=legacy&posttype=text&title=#{CGI.escape(tumblr_title(answer))}&url=#{CGI.escape(answer_url)}&caption=&content=#{CGI.escape(tumblr_body(answer))}"
|
||||
end
|
||||
end
|
|
@ -1,50 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "cgi"
|
||||
|
||||
module SocialHelper::TwitterMethods
|
||||
include MarkdownHelper
|
||||
|
||||
def prepare_tweet(answer, post_tag = nil, omit_url = false)
|
||||
question_content = twitter_markdown answer.question.content.gsub(/@(\w+)/, '\1')
|
||||
original_question_length = question_content.length
|
||||
answer_content = twitter_markdown answer.content
|
||||
original_answer_length = answer_content.length
|
||||
|
||||
unless omit_url
|
||||
answer_url = answer_url(
|
||||
id: answer.id,
|
||||
username: answer.user.screen_name,
|
||||
host: APP_CONFIG["hostname"],
|
||||
protocol: (APP_CONFIG["https"] ? :https : :http),
|
||||
)
|
||||
end
|
||||
|
||||
parsed_tweet = { valid: false }
|
||||
tweet_text = ""
|
||||
|
||||
until parsed_tweet[:valid]
|
||||
shortened_question = "#{question_content[0..122]}#{'…' if original_question_length > [123, question_content.length].min}"
|
||||
shortened_answer = "#{answer_content[0..123]}#{'…' if original_answer_length > [124, answer_content.length].min}"
|
||||
components = [
|
||||
shortened_question,
|
||||
"—",
|
||||
shortened_answer,
|
||||
post_tag,
|
||||
answer_url
|
||||
]
|
||||
tweet_text = components.compact.join(" ")
|
||||
|
||||
parsed_tweet = Twitter::TwitterText::Validation.parse_tweet(tweet_text)
|
||||
|
||||
question_content = question_content[0..-2]
|
||||
answer_content = answer_content[0..-2]
|
||||
end
|
||||
|
||||
tweet_text
|
||||
end
|
||||
|
||||
def twitter_share_url(answer)
|
||||
"https://twitter.com/intent/tweet?text=#{CGI.escape(prepare_tweet(answer))}"
|
||||
end
|
||||
end
|
|
@ -1,51 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ThemeHelper
|
||||
ATTRIBUTE_MAP = {
|
||||
"primary_color" => %w[primary primary-rgb],
|
||||
"primary_text" => "primary-text",
|
||||
"danger_color" => "danger",
|
||||
"danger_text" => "danger-text",
|
||||
"warning_color" => "warning",
|
||||
"warning_text" => "warning-text",
|
||||
"info_color" => "info",
|
||||
"info_text" => "info-text",
|
||||
"success_color" => "success",
|
||||
"success_text" => "success-text",
|
||||
"dark_color" => "dark",
|
||||
"dark_text" => "dark-text",
|
||||
"light_color" => "light",
|
||||
"light_text" => "light-text",
|
||||
"raised_background" => %w[raised-bg raised-bg-rgb],
|
||||
"raised_text" => "raised-text",
|
||||
"raised_accent" => %w[raised-accent raised-accent-rgb],
|
||||
"raised_accent_text" => "raised-accent-text",
|
||||
"background_color" => "background",
|
||||
"body_text" => "body-text",
|
||||
"input_color" => "input-bg",
|
||||
"input_text" => "input-text",
|
||||
"input_placeholder" => "input-placeholder",
|
||||
"muted_text" => "muted-text",
|
||||
}.freeze
|
||||
|
||||
def render_theme
|
||||
theme = get_active_theme
|
||||
|
||||
return unless theme
|
||||
|
||||
body = ":root {\n"
|
||||
|
||||
theme.attributes.each do |k, v|
|
||||
next unless ATTRIBUTE_MAP.key?(k)
|
||||
|
||||
Array(ATTRIBUTE_MAP[k]).each do |var|
|
||||
body += "\t--#{var}: #{get_color_for_key(var, v)};\n"
|
||||
end
|
||||
end
|
||||
body += "\t--turbolinks-progress-color: ##{lighten(theme.primary_color)}\n}"
|
||||
|
||||
content_tag(:style, body)
|
||||
end
|
||||
def render_theme = nil
|
||||
|
||||
def get_color_for_key(key, color)
|
||||
hex = get_hex_color_from_theme_value(color)
|
||||
|
@ -57,49 +13,9 @@ module ThemeHelper
|
|||
end
|
||||
end
|
||||
|
||||
def theme_color
|
||||
theme = get_active_theme
|
||||
if theme
|
||||
theme.theme_color
|
||||
else
|
||||
"#5e35b1"
|
||||
end
|
||||
end
|
||||
def theme_color = "#5e35b1"
|
||||
|
||||
def mobile_theme_color
|
||||
theme = get_active_theme
|
||||
if theme
|
||||
theme.mobile_theme_color
|
||||
else
|
||||
"#f0edf4"
|
||||
end
|
||||
end
|
||||
|
||||
def get_active_theme
|
||||
if @user&.theme
|
||||
if user_signed_in?
|
||||
if current_user&.show_foreign_themes?
|
||||
@user.theme
|
||||
else
|
||||
current_user&.theme
|
||||
end
|
||||
else
|
||||
@user.theme
|
||||
end
|
||||
elsif @answer&.user&.theme
|
||||
if user_signed_in?
|
||||
if current_user&.show_foreign_themes?
|
||||
@answer.user.theme
|
||||
else
|
||||
current_user&.theme
|
||||
end
|
||||
else
|
||||
@answer.user.theme
|
||||
end
|
||||
elsif current_user&.theme
|
||||
current_user.theme
|
||||
end
|
||||
end
|
||||
def mobile_theme_color = "#f0edf4"
|
||||
|
||||
def get_hex_color_from_theme_value(value)
|
||||
"0000000#{value.to_s(16)}"[-6, 6]
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module UserHelper
|
||||
# Decides what user name to show.
|
||||
# @param context_user [User] the user whose the profile preferences should be applied
|
||||
# @param author_identifier [nil, String] the author identifier of the question (questions only)
|
||||
# @return [String] The user name
|
||||
def user_screen_name(user, context_user: nil, author_identifier: nil, url: true, link_only: false)
|
||||
return unmask(user, context_user, author_identifier) if should_unmask?(author_identifier)
|
||||
return anonymous_name(context_user) if anonymous?(user, author_identifier.present?)
|
||||
|
||||
if url
|
||||
return user_path(user) if link_only
|
||||
|
||||
return profile_link(user, target: "_top")
|
||||
end
|
||||
user.profile.safe_name.strip
|
||||
end
|
||||
|
||||
def moderation_view?
|
||||
!!(current_user&.mod? && session[:moderation_view] == true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def profile_link(user, target: nil)
|
||||
link_to(user.profile.safe_name, user_path(user), class: ("user--banned" if user.banned?).to_s, target:)
|
||||
end
|
||||
|
||||
def should_unmask?(author_identifier)
|
||||
moderation_view? && author_identifier.present?
|
||||
end
|
||||
|
||||
def unmask(user, context_user, author_identifier)
|
||||
return profile_link(user) if user.present?
|
||||
|
||||
content_tag(:abbr, anonymous_name(context_user), title: author_identifier)
|
||||
end
|
||||
|
||||
def anonymous_name(context_user)
|
||||
sanitize(context_user&.profile&.anon_display_name.presence || APP_CONFIG["anonymous_name"], tags: [])
|
||||
end
|
||||
|
||||
def anonymous?(user, author_identifier)
|
||||
user.nil? || author_identifier.present?
|
||||
end
|
||||
end
|
|
@ -1,29 +1,11 @@
|
|||
import start from 'retrospring/common';
|
||||
import initAnswerbox from 'retrospring/features/answerbox/index';
|
||||
import initInbox from 'retrospring/features/inbox/index';
|
||||
import initUser from 'retrospring/features/user';
|
||||
import initSettings from 'retrospring/features/settings/index';
|
||||
import initLists from 'retrospring/features/lists';
|
||||
import initQuestionbox from 'retrospring/features/questionbox';
|
||||
import initQuestion from 'retrospring/features/question';
|
||||
import initModeration from 'retrospring/features/moderation';
|
||||
import initMemes from 'retrospring/features/memes';
|
||||
import initFront from 'retrospring/features/front';
|
||||
import initWebpush from 'retrospring/features/webpush';
|
||||
import initWebpushSettingsButtons from 'retrospring/features/webpush/settingsButtons';
|
||||
|
||||
start();
|
||||
document.addEventListener('DOMContentLoaded', initAnswerbox);
|
||||
document.addEventListener('DOMContentLoaded', initInbox);
|
||||
document.addEventListener('DOMContentLoaded', initUser);
|
||||
document.addEventListener('turbo:load', initSettings);
|
||||
document.addEventListener('DOMContentLoaded', initLists);
|
||||
document.addEventListener('turbo:load', initQuestionbox);
|
||||
document.addEventListener('DOMContentLoaded', initQuestion);
|
||||
document.addEventListener('DOMContentLoaded', initModeration);
|
||||
document.addEventListener('DOMContentLoaded', initMemes);
|
||||
document.addEventListener('turbo:load', initFront);
|
||||
document.addEventListener('DOMContentLoaded', initWebpush);
|
||||
document.addEventListener('turbo:load', initWebpushSettingsButtons);
|
||||
|
||||
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import '@hotwired/turbo-rails';
|
||||
import initializeBootstrap from './initializers/bootstrap';
|
||||
import initializeHotkey from './initializers/hotkey';
|
||||
import initializeServiceWorker from './initializers/serviceWorker';
|
||||
import initializeStimulus from './initializers/stimulus';
|
||||
|
||||
export default function start(): void {
|
||||
try {
|
||||
initializeBootstrap();
|
||||
initializeHotkey();
|
||||
initializeServiceWorker();
|
||||
initializeStimulus();
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { Alert } from 'bootstrap';
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
id: Number
|
||||
};
|
||||
|
||||
declare readonly idValue: number;
|
||||
|
||||
connect(): void {
|
||||
if (!window.localStorage.getItem(`announcement${this.idValue}`)) {
|
||||
this.element.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
window.localStorage.setItem(`announcement${this.idValue}`, 'true');
|
||||
|
||||
const alert = Alert.getOrCreateInstance(this.element);
|
||||
alert.close();
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
connect(): void {
|
||||
(this.element as HTMLInputElement).focus();
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['input', 'counter', 'action'];
|
||||
|
||||
declare readonly inputTarget: HTMLInputElement;
|
||||
declare readonly counterTarget: HTMLElement;
|
||||
declare readonly actionTarget: HTMLInputElement;
|
||||
|
||||
static values = {
|
||||
max: Number
|
||||
};
|
||||
|
||||
declare readonly maxValue: number;
|
||||
|
||||
connect(): void {
|
||||
this.inputTarget.addEventListener('input', this.update.bind(this));
|
||||
}
|
||||
|
||||
update(): void {
|
||||
this.counterTarget.innerHTML = String(`${this.maxValue - this.inputTarget.value.length}`);
|
||||
|
||||
if (this.inputTarget.value.length > this.maxValue) {
|
||||
if (!this.inputTarget.classList.contains('is-invalid') && !this.actionTarget.disabled) {
|
||||
this.inputTarget.classList.add('is-invalid');
|
||||
this.actionTarget.disabled = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (this.inputTarget.classList.contains('is-invalid') && this.actionTarget.disabled) {
|
||||
this.inputTarget.classList.remove('is-invalid');
|
||||
this.actionTarget.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['input', 'warning'];
|
||||
|
||||
declare readonly inputTarget: HTMLInputElement;
|
||||
declare readonly warningTarget: HTMLElement;
|
||||
|
||||
static values = {
|
||||
warn: Number
|
||||
};
|
||||
|
||||
declare readonly warnValue: number;
|
||||
|
||||
connect(): void {
|
||||
this.inputTarget.addEventListener('input', this.update.bind(this));
|
||||
}
|
||||
|
||||
update(): void {
|
||||
if (this.inputTarget.value.length > this.warnValue) {
|
||||
if (this.warningTarget.classList.contains('d-none')) {
|
||||
this.warningTarget.classList.remove('d-none');
|
||||
}
|
||||
} else {
|
||||
if (!this.warningTarget.classList.contains('d-none')) {
|
||||
this.warningTarget.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
import I18n from 'retrospring/i18n';
|
||||
import { showErrorNotification, showNotification } from "retrospring/utilities/notifications";
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
static values = {
|
||||
copy: String
|
||||
};
|
||||
|
||||
declare readonly copyValue: string;
|
||||
|
||||
async copy(){
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.copyValue);
|
||||
showNotification(I18n.translate("frontend.clipboard_copy.success"));
|
||||
this.element.dispatchEvent(new CustomEvent('retrospring:copied'));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
showErrorNotification(I18n.translate("frontend.clipboard_copy.error"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['action', 'content'];
|
||||
|
||||
declare readonly contentTarget: HTMLElement;
|
||||
declare readonly actionTarget: HTMLElement;
|
||||
|
||||
connect(): void {
|
||||
this.actionTarget.addEventListener('click', this.update.bind(this));
|
||||
}
|
||||
|
||||
update(): void {
|
||||
this.contentTarget.classList.toggle('collapsed');
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import Croppr from 'croppr';
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['input', 'controls', 'cropper', 'x', 'y', 'w', 'h'];
|
||||
|
||||
declare readonly inputTarget: HTMLInputElement;
|
||||
declare readonly controlsTarget: HTMLElement;
|
||||
declare readonly cropperTarget: HTMLImageElement;
|
||||
declare readonly xTarget: HTMLInputElement;
|
||||
declare readonly yTarget: HTMLInputElement;
|
||||
declare readonly wTarget: HTMLInputElement;
|
||||
declare readonly hTarget: HTMLInputElement;
|
||||
|
||||
static values = {
|
||||
aspectRatio: String
|
||||
};
|
||||
|
||||
declare readonly aspectRatioValue: string;
|
||||
|
||||
readImage(file: File, callback: (string) => void): void {
|
||||
callback((window.URL || window.webkitURL).createObjectURL(file));
|
||||
}
|
||||
|
||||
updateValues(data: Record<string, string>): void {
|
||||
this.xTarget.value = data.x;
|
||||
this.yTarget.value = data.y;
|
||||
this.wTarget.value = data.width;
|
||||
this.hTarget.value = data.height;
|
||||
}
|
||||
|
||||
change(): void {
|
||||
this.controlsTarget.classList.toggle('d-none');
|
||||
|
||||
if (this.inputTarget.files && this.inputTarget.files[0]) {
|
||||
this.readImage(this.inputTarget.files[0], (src) => {
|
||||
this.cropperTarget.addEventListener('load', () => {
|
||||
new Croppr(this.cropperTarget, {
|
||||
aspectRatio: parseFloat(this.aspectRatioValue),
|
||||
startSize: [100, 100, '%'],
|
||||
onInitialize: (instance) => { this.updateValues(instance.getValue()) },
|
||||
onCropStart: this.updateValues.bind(this),
|
||||
onCropMove: this.updateValues.bind(this),
|
||||
onCropEnd: this.updateValues.bind(this)
|
||||
});
|
||||
}, {
|
||||
once: true
|
||||
});
|
||||
|
||||
this.cropperTarget.src = src;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { Popover } from 'bootstrap';
|
||||
|
||||
export default class extends Controller {
|
||||
connect(): void {
|
||||
const formatOptionsElement = document.getElementById('formatting-options');
|
||||
|
||||
this.element.addEventListener('click', e => e.preventDefault());
|
||||
|
||||
new Popover(this.element, {
|
||||
html: true,
|
||||
content: formatOptionsElement.innerHTML,
|
||||
placement: 'bottom',
|
||||
trigger: 'focus',
|
||||
customClass: 'rs-popover'
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
import { install, uninstall } from "@github/hotkey";
|
||||
|
||||
export default class extends Controller<HTMLElement> {
|
||||
connect(): void {
|
||||
install(this.element);
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
uninstall(this.element);
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['twitter', 'bluesky', 'tumblr', 'telegram', 'other', 'custom', 'clipboard'];
|
||||
|
||||
declare readonly twitterTarget: HTMLAnchorElement;
|
||||
declare readonly blueskyTarget: HTMLAnchorElement;
|
||||
declare readonly tumblrTarget: HTMLAnchorElement;
|
||||
declare readonly telegramTarget: HTMLAnchorElement;
|
||||
declare readonly customTarget: HTMLAnchorElement;
|
||||
declare readonly otherTarget: HTMLButtonElement;
|
||||
declare readonly clipboardTarget: HTMLButtonElement;
|
||||
declare readonly hasCustomTarget: boolean;
|
||||
|
||||
static values = {
|
||||
config: Object,
|
||||
autoClose: Boolean
|
||||
};
|
||||
|
||||
declare readonly configValue: Record<string, string>;
|
||||
declare readonly autoCloseValue: boolean;
|
||||
|
||||
connect(): void {
|
||||
if (this.autoCloseValue) {
|
||||
this.twitterTarget.addEventListener('click', () => this.close());
|
||||
this.blueskyTarget.addEventListener('click', () => this.close());
|
||||
this.tumblrTarget.addEventListener('click', () => this.close());
|
||||
this.telegramTarget.addEventListener('click', () => this.close());
|
||||
this.otherTarget.addEventListener('click', () => this.closeAfterShare());
|
||||
this.clipboardTarget.addEventListener('click', () => this.closeAfterCopyToClipboard());
|
||||
|
||||
if (this.hasCustomTarget) {
|
||||
this.customTarget.addEventListener('click', () => this.close());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configValueChanged(value: Record<string, string>): void {
|
||||
if (Object.keys(value).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.element.classList.remove('d-none');
|
||||
|
||||
this.twitterTarget.href = this.configValue['twitter'];
|
||||
this.blueskyTarget.href = this.configValue['bluesky'];
|
||||
this.tumblrTarget.href = this.configValue['tumblr'];
|
||||
this.telegramTarget.href = this.configValue['telegram'];
|
||||
|
||||
if (this.hasCustomTarget) {
|
||||
this.customTarget.href = `${this.customTarget.href}${this.configValue['custom']}`;
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
(this.element.closest(".inbox-entry")).remove();
|
||||
}
|
||||
|
||||
closeAfterShare(): void {
|
||||
this.otherTarget.addEventListener('retrospring:shared', () => this.close());
|
||||
}
|
||||
|
||||
closeAfterCopyToClipboard(): void {
|
||||
this.clipboardTarget.addEventListener('retrospring:copied', () => this.close());
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
import { install, uninstall } from "@github/hotkey";
|
||||
|
||||
export default class extends Controller {
|
||||
static classes = ["current"];
|
||||
static targets = ["current", "traversable"];
|
||||
|
||||
declare readonly hasCurrentTarget: boolean;
|
||||
declare readonly currentTarget: HTMLElement;
|
||||
declare readonly traversableTargets: HTMLElement[];
|
||||
|
||||
traversableTargetConnected(target: HTMLElement): void {
|
||||
if (!("navigationIndex" in target.dataset)) {
|
||||
target.dataset.navigationIndex = this.traversableTargets.indexOf(target).toString();
|
||||
}
|
||||
|
||||
if (!this.hasCurrentTarget) {
|
||||
const first = this.traversableTargets[0];
|
||||
first.dataset.navigationTarget += " current";
|
||||
}
|
||||
}
|
||||
|
||||
currentTargetConnected(target: HTMLElement): void {
|
||||
target.classList.add("js-hotkey-current-selection");
|
||||
|
||||
target.querySelectorAll<HTMLElement>("[data-selection-hotkey]")
|
||||
.forEach(el => install(el, el.dataset.selectionHotkey));
|
||||
}
|
||||
|
||||
currentTargetDisconnected(target: HTMLElement): void {
|
||||
target.classList.remove("js-hotkey-current-selection");
|
||||
|
||||
target.querySelectorAll<HTMLElement>("[data-selection-hotkey]")
|
||||
.forEach(el => uninstall(el));
|
||||
}
|
||||
|
||||
up(): void {
|
||||
const prevIndex = this.traversableTargets.indexOf(this.currentTarget) - 1;
|
||||
if (prevIndex == -1) return;
|
||||
|
||||
this.navigate(this.traversableTargets[prevIndex]);
|
||||
}
|
||||
|
||||
down(): void {
|
||||
const nextIndex = this.traversableTargets.indexOf(this.currentTarget) + 1;
|
||||
if (nextIndex == this.traversableTargets.length) return;
|
||||
|
||||
this.navigate(this.traversableTargets[nextIndex]);
|
||||
}
|
||||
|
||||
navigate(target: HTMLElement): void {
|
||||
if (!document.body.classList.contains("js-hotkey-navigating")) {
|
||||
document.body.classList.add("js-hotkey-navigating");
|
||||
}
|
||||
|
||||
if (target.dataset.navigationTarget == "traversable") {
|
||||
this.currentTarget.dataset.navigationTarget = "traversable";
|
||||
target.dataset.navigationTarget = "traversable current";
|
||||
target.scrollIntoView({ block: "center", inline: "center" });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller<HTMLElement> {
|
||||
isPwa: boolean;
|
||||
badgeCapable: boolean;
|
||||
|
||||
initialize(): void {
|
||||
this.isPwa = window.matchMedia('(display-mode: standalone)').matches;
|
||||
this.badgeCapable = "setAppBadge" in navigator;
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
if (this.isPwa && this.badgeCapable) {
|
||||
const count = Number.parseInt(this.element.innerText) || 0;
|
||||
navigator.setAppBadge(count);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { Modal } from 'bootstrap';
|
||||
|
||||
export default class extends Controller {
|
||||
click(): void {
|
||||
const modal = Modal.getInstance(this.element.closest('.modal'));
|
||||
const questionbox = document.querySelector((this.element as HTMLAnchorElement).href);
|
||||
|
||||
modal.hide();
|
||||
questionbox.scrollIntoView();
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['button'];
|
||||
|
||||
declare readonly buttonTarget: HTMLButtonElement;
|
||||
|
||||
enable(): void {
|
||||
this.buttonTarget.disabled = false;
|
||||
}
|
||||
|
||||
disable(): void {
|
||||
this.buttonTarget.disabled = true;
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import noop from 'utilities/noop';
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
url: String,
|
||||
text: String,
|
||||
title: String
|
||||
};
|
||||
|
||||
declare readonly urlValue: string;
|
||||
declare readonly textValue: string;
|
||||
declare readonly titleValue: string;
|
||||
|
||||
share() {
|
||||
let shareConfiguration = {};
|
||||
|
||||
if (this.urlValue.length >= 1) {
|
||||
shareConfiguration = {
|
||||
...shareConfiguration,
|
||||
...{ url: this.urlValue }
|
||||
};
|
||||
}
|
||||
|
||||
if (this.textValue.length >= 1) {
|
||||
shareConfiguration = {
|
||||
...shareConfiguration,
|
||||
...{ text: this.textValue }
|
||||
};
|
||||
}
|
||||
|
||||
if (this.titleValue.length >= 1) {
|
||||
shareConfiguration = {
|
||||
...shareConfiguration,
|
||||
...{ title: this.titleValue }
|
||||
};
|
||||
}
|
||||
|
||||
navigator.share(shareConfiguration)
|
||||
.then(() => {
|
||||
this.element.dispatchEvent(new CustomEvent('retrospring:shared'));
|
||||
})
|
||||
.catch(noop);
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import Coloris from '@melloware/coloris';
|
||||
|
||||
import {
|
||||
THEME_MAPPING,
|
||||
getColorForKey,
|
||||
getHexColorFromThemeValue,
|
||||
getIntegerFromHexColor
|
||||
} from 'utilities/theme';
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['color'];
|
||||
|
||||
declare readonly colorTargets: HTMLInputElement[];
|
||||
|
||||
previewStyle = null;
|
||||
previewTimeout = null;
|
||||
|
||||
setupPreviewElement(): void {
|
||||
this.previewStyle = document.createElement('style');
|
||||
this.previewStyle.setAttribute('data-preview-style', '');
|
||||
document.body.appendChild(this.previewStyle);
|
||||
}
|
||||
|
||||
convertColors(): void {
|
||||
this.colorTargets.forEach((color) => {
|
||||
color.value = `#${getHexColorFromThemeValue(color.value)}`;
|
||||
});
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
this.setupPreviewElement();
|
||||
this.convertColors();
|
||||
|
||||
Coloris.init();
|
||||
Coloris({
|
||||
el: '.color',
|
||||
wrap: false,
|
||||
formatToggle: false,
|
||||
alpha: false
|
||||
});
|
||||
}
|
||||
|
||||
updatePreview(): void {
|
||||
clearTimeout(this.previewTimeout);
|
||||
this.previewTimeout = setTimeout(this.previewTheme.bind(this), 1000);
|
||||
}
|
||||
|
||||
previewTheme(): void {
|
||||
const payload = {};
|
||||
|
||||
this.colorTargets.forEach((color) => {
|
||||
const name = color.name.substring(6, color.name.length - 1);
|
||||
payload[name] = parseInt(color.value.substr(1, 6), 16);
|
||||
});
|
||||
|
||||
this.generateTheme(payload);
|
||||
}
|
||||
|
||||
generateTheme(payload: Record<string, string>): void {
|
||||
let body = ":root {\n";
|
||||
|
||||
Object.entries(payload).forEach(([key, value]) => {
|
||||
if (THEME_MAPPING[key]) {
|
||||
body += `--${THEME_MAPPING[key]}: ${getColorForKey(THEME_MAPPING[key], value)};\n`;
|
||||
}
|
||||
});
|
||||
|
||||
body += "}";
|
||||
|
||||
this.previewStyle.innerHTML = body;
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
this.colorTargets.forEach((color) => {
|
||||
color.value = String(getIntegerFromHexColor(color.value));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { showNotification } from "utilities/notifications";
|
||||
|
||||
export default class extends Controller<HTMLElement> {
|
||||
static values = {
|
||||
message: String,
|
||||
success: Boolean
|
||||
};
|
||||
|
||||
declare readonly messageValue: string;
|
||||
declare readonly successValue: boolean;
|
||||
|
||||
connect(): void {
|
||||
showNotification(this.messageValue, this.successValue);
|
||||
this.element.remove();
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { Tooltip } from 'bootstrap';
|
||||
|
||||
export default class extends Controller {
|
||||
connect(): void {
|
||||
new Tooltip(this.element);
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import { post } from '@rails/request.js';
|
||||
import swal from 'sweetalert';
|
||||
|
||||
import I18n from 'retrospring/i18n';
|
||||
import { showNotification, showErrorNotification } from 'utilities/notifications';
|
||||
|
||||
export function commentDestroyHandler(event: Event): void {
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const id = button.dataset.cId;
|
||||
event.preventDefault();
|
||||
button.disabled = true;
|
||||
|
||||
swal({
|
||||
title: I18n.translate('frontend.destroy_comment.confirm.title'),
|
||||
text: I18n.translate('frontend.destroy_comment.confirm.text'),
|
||||
type: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#DD6B55',
|
||||
confirmButtonText: I18n.translate('voc.delete'),
|
||||
cancelButtonText: I18n.translate('voc.cancel'),
|
||||
closeOnConfirm: true
|
||||
}, (returnValue) => {
|
||||
if (returnValue === false) {
|
||||
button.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
post('/ajax/destroy_comment', {
|
||||
body: {
|
||||
comment: id
|
||||
},
|
||||
contentType: 'application/json'
|
||||
})
|
||||
.then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
showNotification(data.message);
|
||||
|
||||
document.querySelector(`[data-comment-id="${id}"]`).remove();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
export function commentHotkeyHandler(event: Event): void {
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const id = button.dataset.aId;
|
||||
const answerbox = button.closest('.answerbox');
|
||||
|
||||
if (answerbox !== null) {
|
||||
answerbox.querySelector(`#ab-comments-section-${id}`).classList.toggle('d-none');
|
||||
answerbox.querySelector<HTMLElement>(`[name="ab-comment-new"][data-a-id="${id}"]`).focus();
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import registerEvents from "retrospring/utilities/registerEvents";
|
||||
import { commentDestroyHandler } from "./destroy";
|
||||
import { commentComposeEnd, commentComposeStart, commentCreateClickHandler, commentCreateKeyboardHandler } from "./new";
|
||||
import { commentReportHandler } from "./report";
|
||||
import { commentToggleHandler } from "./toggle";
|
||||
import { commentHotkeyHandler } from "retrospring/features/answerbox/comment/hotkey";
|
||||
|
||||
export default (): void => {
|
||||
registerEvents([
|
||||
{ type: 'click', target: '[name=ab-comments]', handler: commentToggleHandler, global: true },
|
||||
{ type: 'click', target: '[name=ab-open-and-comment]', handler: commentHotkeyHandler, global: true },
|
||||
{ type: 'click', target: '[data-action=ab-comment-report]', handler: commentReportHandler, global: true },
|
||||
{ type: 'click', target: '[data-action=ab-comment-destroy]', handler: commentDestroyHandler, global: true },
|
||||
{ type: 'compositionstart', target: '[name=ab-comment-new]', handler: commentComposeStart, global: true },
|
||||
{ type: 'compositionend', target: '[name=ab-comment-new]', handler: commentComposeEnd, global: true },
|
||||
{ type: 'keydown', target: '[name=ab-comment-new]', handler: commentCreateKeyboardHandler, global: true },
|
||||
{ type: 'click', target: '[name=ab-comment-new-submit]', handler: commentCreateClickHandler, global: true }
|
||||
]);
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
import { post } from '@rails/request.js';
|
||||
|
||||
import I18n from 'retrospring/i18n';
|
||||
import { showNotification, showErrorNotification } from 'utilities/notifications';
|
||||
|
||||
let compositionJustEnded = false;
|
||||
|
||||
function createComment(input: HTMLInputElement, id: string, counter: Element, group: Element) {
|
||||
if (input.value.length > 512) {
|
||||
group.classList.add('has-error');
|
||||
return true;
|
||||
}
|
||||
|
||||
input.disabled = true;
|
||||
|
||||
post('/ajax/create_comment', {
|
||||
body: {
|
||||
answer: id,
|
||||
comment: input.value
|
||||
},
|
||||
contentType: 'application/json'
|
||||
})
|
||||
.then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
if (data.success) {
|
||||
document.querySelector(`#ab-comments-${id}`).innerHTML = data.render;
|
||||
const commentCount = document.getElementById(`#ab-comment-count-${id}`);
|
||||
if (commentCount) {
|
||||
commentCount.innerHTML = data.count;
|
||||
}
|
||||
input.value = '';
|
||||
counter.innerHTML = String(512);
|
||||
}
|
||||
|
||||
showNotification(data.message, data.success);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||
})
|
||||
.finally(() => {
|
||||
input.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
export function commentCreateKeyboardHandler(event: KeyboardEvent): boolean {
|
||||
if (compositionJustEnded && event.which == 13) {
|
||||
compositionJustEnded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const input = event.target as HTMLInputElement;
|
||||
const id = input.dataset.aId;
|
||||
const counter = document.querySelector(`#ab-comment-charcount-${id}`);
|
||||
const group = document.querySelector(`[name=ab-comment-new-group][data-a-id="${id}"]`);
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && event.which === 13) {
|
||||
event.preventDefault();
|
||||
|
||||
createComment(input, id, counter, group);
|
||||
}
|
||||
}
|
||||
|
||||
export function commentCreateClickHandler(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const id = button.dataset.aId;
|
||||
const input = document.querySelector<HTMLInputElement>(`[name="ab-comment-new"][data-a-id="${id}"]`);
|
||||
const counter = document.querySelector(`#ab-comment-charcount-${id}`);
|
||||
const group = document.querySelector(`[name=ab-comment-new-group][data-a-id="${id}"]`);
|
||||
|
||||
createComment(input, id, counter, group);
|
||||
}
|
||||
|
||||
export function commentComposeStart(): boolean {
|
||||
compositionJustEnded = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function commentComposeEnd(): boolean {
|
||||
compositionJustEnded = true;
|
||||
return true;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { reportDialog } from 'utilities/reportDialog';
|
||||
|
||||
export function commentReportHandler(event: Event): void {
|
||||
event.preventDefault();
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const commentId = button.dataset.cId;
|
||||
|
||||
reportDialog('comment', commentId);
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
export function commentToggleHandler(event: Event): void {
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const id = button.dataset.aId;
|
||||
const answerbox = button.closest('.answerbox');
|
||||
|
||||
if (answerbox !== null) {
|
||||
answerbox.querySelector(`#ab-comments-section-${id}`).classList.toggle('d-none');
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import { post } from '@rails/request.js';
|
||||
import { showErrorNotification, showNotification } from 'utilities/notifications';
|
||||
import swal from 'sweetalert';
|
||||
|
||||
import I18n from 'retrospring/i18n';
|
||||
|
||||
export function answerboxDestroyHandler(event: Event): void {
|
||||
event.preventDefault();
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const answerId = button.dataset.aId;
|
||||
|
||||
swal({
|
||||
title: I18n.translate('frontend.destroy_own.confirm.title'),
|
||||
text: I18n.translate('frontend.destroy_own.confirm.text'),
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: I18n.translate('voc.y'),
|
||||
cancelButtonText: I18n.translate('voc.n'),
|
||||
closeOnConfirm: true
|
||||
}, (returnValue) => {
|
||||
if (returnValue === false) {
|
||||
button.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
post('/ajax/destroy_answer', {
|
||||
body: {
|
||||
answer: answerId
|
||||
},
|
||||
contentType: 'application/json'
|
||||
})
|
||||
.then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
if (data.success) {
|
||||
document.querySelector(`.answerbox[data-id="${answerId}"]`).remove();
|
||||
}
|
||||
|
||||
showNotification(data.message, data.success);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
import registerEvents from 'utilities/registerEvents';
|
||||
import registerAnswerboxCommentEvents from './comment';
|
||||
import { answerboxDestroyHandler } from './destroy';
|
||||
import { answerboxReportHandler } from './report';
|
||||
|
||||
export default (): void => {
|
||||
registerEvents([
|
||||
{ type: 'click', target: '[data-action=ab-report]', handler: answerboxReportHandler, global: true },
|
||||
{ type: 'click', target: '[data-action=ab-destroy]', handler: answerboxDestroyHandler, global: true },
|
||||
]);
|
||||
|
||||
registerAnswerboxCommentEvents();
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { reportDialog } from 'utilities/reportDialog';
|
||||
|
||||
export function answerboxReportHandler(event: Event): void {
|
||||
event.preventDefault();
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const answerId = button.dataset.aId;
|
||||
|
||||
reportDialog('answer', answerId);
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
import { post } from '@rails/request.js';
|
||||
import swal from 'sweetalert';
|
||||
|
||||
import I18n from 'retrospring/i18n';
|
||||
import { showErrorNotification } from 'utilities/notifications';
|
||||
|
||||
export function updateDeleteButton(increment = true): void {
|
||||
const deleteButton: HTMLElement = document.querySelector('[id^=ib-delete-all]');
|
||||
const inboxCount: number = parseInt(deleteButton.getAttribute('data-ib-count'));
|
||||
let targetInboxCount = 0;
|
||||
|
||||
if (increment) {
|
||||
targetInboxCount = inboxCount + 1;
|
||||
}
|
||||
else {
|
||||
targetInboxCount = inboxCount - 1;
|
||||
}
|
||||
|
||||
deleteButton.setAttribute('data-ib-count', targetInboxCount.toString());
|
||||
|
||||
if (targetInboxCount > 0) {
|
||||
deleteButton.removeAttribute('disabled');
|
||||
} else {
|
||||
deleteButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteAllQuestionsHandler(event: Event): void {
|
||||
const button = event.target as Element;
|
||||
const count = button.getAttribute('data-ib-count');
|
||||
|
||||
swal({
|
||||
title: I18n.translate('frontend.inbox.confirm_all.title', { count: count }),
|
||||
text: I18n.translate('frontend.inbox.confirm_all.text'),
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: I18n.translate('voc.delete'),
|
||||
cancelButtonText: I18n.translate('voc.cancel'),
|
||||
closeOnConfirm: true
|
||||
}, (returnValue) => {
|
||||
if (returnValue === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
post('/ajax/delete_all_inbox')
|
||||
.then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
if (!data.success) return false;
|
||||
|
||||
updateDeleteButton(false);
|
||||
document.querySelector('#entries').innerHTML = 'Nothing to see here!';
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteAllAuthorQuestionsHandler(event: Event): void {
|
||||
const button = event.target as Element;
|
||||
const count = button.getAttribute('data-ib-count');
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
swal({
|
||||
title: I18n.translate('frontend.inbox.confirm_all.title', { count: count }),
|
||||
text: I18n.translate('frontend.inbox.confirm_all.text'),
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: I18n.translate('voc.delete'),
|
||||
cancelButtonText: I18n.translate('voc.cancel'),
|
||||
closeOnConfirm: true
|
||||
}, (returnValue) => {
|
||||
if (returnValue === null) return false;
|
||||
|
||||
post(`/ajax/delete_all_inbox/${urlSearchParams.get('author')}`)
|
||||
.then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
if (!data.success) return false;
|
||||
|
||||
updateDeleteButton(false);
|
||||
document.querySelector('#entries').innerHTML = 'Nothing to see here!';
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import { post } from '@rails/request.js';
|
||||
|
||||
import { updateDeleteButton } from '../delete';
|
||||
import { showNotification, showErrorNotification } from 'utilities/notifications';
|
||||
|
||||
export function answerEntryHandler(event: Event): void {
|
||||
const element: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||
const inboxEntry: HTMLElement = element.closest<HTMLElement>('.inbox-entry');
|
||||
|
||||
element.disabled = true;
|
||||
|
||||
const data = {
|
||||
id: element.getAttribute('data-ib-id'),
|
||||
answer: inboxEntry.querySelector<HTMLInputElement>('textarea[name=ib-answer]')?.value,
|
||||
inbox: 'true'
|
||||
};
|
||||
|
||||
post('/ajax/answer', {
|
||||
body: data,
|
||||
contentType: 'application/json'
|
||||
})
|
||||
.then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
if (!data.success) {
|
||||
showErrorNotification(data.message);
|
||||
element.disabled = false;
|
||||
return false;
|
||||
}
|
||||
updateDeleteButton(false);
|
||||
showNotification(data.message);
|
||||
|
||||
const shareButton = inboxEntry.querySelector<HTMLButtonElement>('[data-controller="share"]');
|
||||
const clipboardCopyButton = inboxEntry.querySelector<HTMLButtonElement>('[data-action="clipboard#copy"]')
|
||||
if (shareButton != null) {
|
||||
shareButton.dataset.shareUrlValue = data.sharing.url;
|
||||
shareButton.dataset.shareTextValue = data.sharing.text;
|
||||
}
|
||||
|
||||
if (clipboardCopyButton != null){
|
||||
clipboardCopyButton.dataset.clipboardCopyValue = `${data.sharing.text} ${data.sharing.url}`;
|
||||
}
|
||||
|
||||
const sharing = inboxEntry.querySelector<HTMLElement>('.inbox-entry__sharing');
|
||||
if (sharing != null) {
|
||||
sharing.dataset.inboxSharingConfigValue = JSON.stringify(data.sharing);
|
||||
}
|
||||
else {
|
||||
(inboxEntry as HTMLElement).remove();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
element.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
export function answerEntryInputHandler(event: KeyboardEvent): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const inboxId = input.dataset.id;
|
||||
|
||||
if (event.keyCode == 13 && (event.ctrlKey || event.metaKey)) {
|
||||
document.querySelector<HTMLButtonElement>(`button[name="ib-answer"][data-ib-id="${inboxId}"]`).click();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue