Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
1aca0560f6
|
@ -1,14 +1,20 @@
|
|||
FROM ruby:3.1
|
||||
FROM ruby:3.2
|
||||
|
||||
USER root
|
||||
|
||||
ARG UID=1000
|
||||
ARG GID=1000
|
||||
ARG NODE_MAJOR=16
|
||||
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -y ca-certificates curl gnupg \
|
||||
&& mkdir -p /etc/apt/keyrings \
|
||||
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -y --no-install-recommends build-essential \
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
71894d6c4987547533606258447b576ecb604c2b
|
||||
1532741485af266f7ff04a1d6529abe9807a6815
|
|
@ -24,7 +24,8 @@ using. We find screenshots (for front-end issues) very helpful.
|
|||
We love pull requests! We are very happy to work with you to get your changes
|
||||
merged in, however please keep the following in mind.
|
||||
|
||||
* Please use the core team standard of `feature/*` or `fix/*` branch naming.
|
||||
* Please use the core team standard of `feature/*` or `bugfix/*` branch naming.
|
||||
* Using these branch prefixes tags the Pull Requests with the appropriate labels for release categorization.
|
||||
* Adhere to the coding conventions you see in the surrounding code.
|
||||
* If you include a new feature also include tests, and make sure they'll pass.
|
||||
* Before submitting a pull-request, clean up the history by going over your
|
||||
|
|
|
@ -18,6 +18,8 @@ updates:
|
|||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
ignore:
|
||||
- dependency-name: 'carrierwave_backgrounder'
|
||||
allow:
|
||||
- dependency-type: direct
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
feature:
|
||||
- head-branch: ['^feature', 'feature']
|
||||
|
||||
bugfix:
|
||||
- head-branch: ['^bugfix', 'bugfix']
|
|
@ -0,0 +1,17 @@
|
|||
changelog:
|
||||
categories:
|
||||
- title: Added
|
||||
labels:
|
||||
- feature
|
||||
- title: Fixed
|
||||
labels:
|
||||
- bugfix
|
||||
- title: Changed
|
||||
labels:
|
||||
- '*'
|
||||
- title: Developer experience
|
||||
labels:
|
||||
- developer experience
|
||||
- title: Dependency updates
|
||||
labels:
|
||||
- dependencies
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.0.0
|
||||
- uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Discover build-time variables
|
||||
run: |
|
||||
|
@ -38,7 +38,7 @@ jobs:
|
|||
esac
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
@ -46,7 +46,7 @@ jobs:
|
|||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
build-args: |
|
||||
BUNDLER_VERSION=${{ env.BUNDLER_VERSION }}
|
||||
|
|
|
@ -33,11 +33,11 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.0.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
|
@ -48,7 +48,7 @@ jobs:
|
|||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
@ -62,4 +62,4 @@ jobs:
|
|||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
name: "Pull Request Labeler"
|
||||
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
|
@ -11,10 +11,10 @@ jobs:
|
|||
name: Rubocop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.0.0
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v39
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files: "**/*.rb"
|
||||
- name: Install dependencies
|
||||
|
@ -37,16 +37,16 @@ jobs:
|
|||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.0.0
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v39
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files: "**/*.ts"
|
||||
- name: Set up Node 14
|
||||
uses: actions/setup-node@v3
|
||||
- name: Set up Node 16
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
cache: 'yarn'
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
- name: Install node modules
|
||||
|
@ -63,10 +63,10 @@ jobs:
|
|||
haml-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.0.0
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v39
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files: "**/*.haml"
|
||||
- name: Install dependencies
|
||||
|
@ -86,16 +86,16 @@ jobs:
|
|||
stylelint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.0.0
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v39
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files: "**/*.scss"
|
||||
- name: Set up Node 14
|
||||
uses: actions/setup-node@v3
|
||||
- name: Set up Node 16
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
cache: 'yarn'
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
- name: Install node modules
|
||||
|
@ -104,7 +104,7 @@ jobs:
|
|||
yarn install --frozen-lockfile
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
- name: stylelint
|
||||
uses: reviewdog/action-stylelint@v1.18.1
|
||||
uses: reviewdog/action-stylelint@v1.26.0
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
reporter: github-pr-check
|
||||
|
|
|
@ -41,17 +41,17 @@ jobs:
|
|||
BUNDLE_WITHOUT: 'production'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.0.0
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- name: Install dependencies
|
||||
run: sudo apt update && sudo apt-get install -y libpq-dev libxml2-dev libxslt1-dev libmagickwand-dev imagemagick libidn11-dev
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
- name: Set up Node 14
|
||||
uses: actions/setup-node@v3
|
||||
- name: Set up Node 16
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
cache: 'yarn'
|
||||
- name: Copy default configuration
|
||||
run: |
|
||||
|
@ -75,7 +75,7 @@ jobs:
|
|||
env:
|
||||
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
|
||||
REDIS_URL: "redis://localhost:${{ job.services.redis.ports[6379] }}"
|
||||
- uses: codecov/codecov-action@v3
|
||||
- uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage/coverage.xml
|
||||
|
|
37
.rubocop.yml
37
.rubocop.yml
|
@ -27,43 +27,36 @@ Lint/NestedMethodDefinition:
|
|||
Exclude:
|
||||
- api/sinatra/**/*
|
||||
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- app/components/**/*
|
||||
|
||||
|
||||
### Metrics
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 20
|
||||
Exclude:
|
||||
- 'db/**/*'
|
||||
Enabled: false
|
||||
|
||||
Layout/LineLength:
|
||||
Enabled: false
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 15
|
||||
Exclude:
|
||||
- 'db/migrate/*.rb'
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- '*.gemspec'
|
||||
- '**/*.rake'
|
||||
- 'api/**/*'
|
||||
- 'app/api/routes.rb'
|
||||
- 'config/initialize/**/*'
|
||||
- 'config/initializers/**/*'
|
||||
- 'spec/**/*'
|
||||
Enabled: false
|
||||
|
||||
Metrics/ClassLength:
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
Enabled: false
|
||||
|
||||
Metrics/CyclomaticComplexity:
|
||||
Severity: refactor
|
||||
Enabled: false
|
||||
|
||||
Metrics/PerceivedComplexity:
|
||||
Enabled: false
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Exclude:
|
||||
- 'app/api/routes.rb'
|
||||
- 'spec/requests/**/*'
|
||||
Enabled: false
|
||||
|
||||
|
||||
### Style / Layout
|
||||
|
@ -137,3 +130,7 @@ Style/TrailingCommaInHashLiteral:
|
|||
|
||||
Style/TrailingCommaInArguments:
|
||||
EnforcedStyleForMultiline: consistent_comma
|
||||
|
||||
Style/RedundantSelf:
|
||||
Exclude:
|
||||
- app/models/**/*
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.1.2
|
||||
3.2.3
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Container image for a production Retrospring setup
|
||||
|
||||
FROM registry.opensuse.org/opensuse/leap:15.4
|
||||
FROM registry.opensuse.org/opensuse/leap:15.5
|
||||
|
||||
LABEL org.opencontainers.image.title="Retrospring (production)"
|
||||
LABEL org.opencontainers.image.description="Image containing everything to run Retrospring in production mode. Do not use this for development."
|
||||
|
@ -8,14 +8,15 @@ LABEL org.opencontainers.image.vendor="The Retrospring team"
|
|||
LABEL org.opencontainers.image.url="https://github.com/Retrospring/retrospring"
|
||||
|
||||
ARG RETROSPRING_VERSION=2023.0131.1
|
||||
ARG RUBY_VERSION=3.1.2
|
||||
ARG RUBY_INSTALL_VERSION=0.9.0
|
||||
ARG BUNDLER_VERSION=2.3.18
|
||||
ARG RUBY_VERSION=3.2.3
|
||||
ARG RUBY_INSTALL_VERSION=0.9.3
|
||||
ARG BUNDLER_VERSION=2.5.5
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
|
||||
# update and install dependencies
|
||||
RUN zypper up -y \
|
||||
RUN zypper addrepo https://download.opensuse.org/repositories/devel:languages:nodejs/15.5/devel:languages:nodejs.repo \
|
||||
&& zypper --gpg-auto-import-keys up -y \
|
||||
&& zypper in -y \
|
||||
# build dependencies (ruby-install)
|
||||
automake \
|
||||
|
@ -25,18 +26,20 @@ RUN zypper up -y \
|
|||
libffi-devel \
|
||||
libopenssl-devel \
|
||||
libyaml-devel \
|
||||
jemalloc-devel \
|
||||
make \
|
||||
ncurses-devel \
|
||||
readline-devel \
|
||||
tar \
|
||||
xz \
|
||||
zlib-devel \
|
||||
curl \
|
||||
# build dependencies (app)
|
||||
gcc-c++ \
|
||||
git \
|
||||
libidn-devel \
|
||||
nodejs14 \
|
||||
npm14 \
|
||||
nodejs16 \
|
||||
npm16 \
|
||||
postgresql-devel \
|
||||
# runtime dependencies
|
||||
ImageMagick \
|
||||
|
@ -50,7 +53,7 @@ RUN curl -Lo ruby-install-${RUBY_INSTALL_VERSION}.tar.gz https://github.com/post
|
|||
&& tar xvf ruby-install-${RUBY_INSTALL_VERSION}.tar.gz \
|
||||
&& (cd ruby-install-${RUBY_INSTALL_VERSION} && make install) \
|
||||
&& rm -rf ruby-install-${RUBY_INSTALL_VERSION} ruby-install-${RUBY_INSTALL_VERSION}.tar.gz \
|
||||
&& ruby-install --no-install-deps --cleanup --system --jobs=$(nproc) ruby ${RUBY_VERSION} -- --disable-install-rdoc \
|
||||
&& ruby-install --no-install-deps --cleanup --system --jobs=$(nproc) ruby ${RUBY_VERSION} -- --disable-install-rdoc --with-jemalloc \
|
||||
&& gem install bundler:${BUNDLER_VERSION}
|
||||
|
||||
# create user and dirs to run retrospring in
|
||||
|
|
37
Gemfile
37
Gemfile
|
@ -3,11 +3,11 @@
|
|||
source "https://rubygems.org"
|
||||
|
||||
gem "i18n-js", "4.0"
|
||||
gem "rails", "~> 6.1"
|
||||
gem "rails", "~> 7.0.8"
|
||||
gem "rails-i18n", "~> 7.0"
|
||||
|
||||
gem "cssbundling-rails", "~> 1.2"
|
||||
gem "jsbundling-rails", "~> 1.1"
|
||||
gem "cssbundling-rails", "~> 1.4"
|
||||
gem "jsbundling-rails", "~> 1.3"
|
||||
gem "sassc-rails"
|
||||
gem "sprockets", "~> 4.2"
|
||||
gem "sprockets-rails", require: "sprockets/railtie"
|
||||
|
@ -16,13 +16,13 @@ gem "pg"
|
|||
|
||||
gem "turbo-rails"
|
||||
|
||||
gem "bcrypt", "~> 3.1.19"
|
||||
gem "bcrypt", "~> 3.1.20"
|
||||
|
||||
gem "active_model_otp"
|
||||
gem "bootsnap", require: false
|
||||
gem "bootstrap_form", "~> 5.0"
|
||||
gem "carrierwave", "~> 2.0"
|
||||
gem "carrierwave_backgrounder", git: "https://github.com/raccube/carrierwave_backgrounder.git"
|
||||
gem "carrierwave", "~> 2.1"
|
||||
gem "carrierwave_backgrounder", "~> 0.4.2"
|
||||
gem "colorize"
|
||||
gem "devise", "~> 4.9"
|
||||
gem "devise-async"
|
||||
|
@ -30,12 +30,13 @@ gem "devise-i18n"
|
|||
gem "fog-aws"
|
||||
gem "fog-core"
|
||||
gem "fog-local"
|
||||
gem "haml", "~> 6.1"
|
||||
gem "hcaptcha", "~> 7.0"
|
||||
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"
|
||||
|
||||
|
@ -49,6 +50,7 @@ gem "sentry-ruby"
|
|||
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.2", git: 'https://lab.freak.university/FreakU/questiongenerator'
|
||||
|
||||
|
@ -66,11 +68,12 @@ gem "fake_email_validator"
|
|||
# TLD validation
|
||||
gem "tldv", "~> 0.1.0"
|
||||
|
||||
gem "jwt", "~> 2.7"
|
||||
gem "view_component"
|
||||
|
||||
gem "jwt", "~> 2.8"
|
||||
|
||||
group :development do
|
||||
gem "binding_of_caller"
|
||||
gem "spring", "~> 4.1"
|
||||
end
|
||||
|
||||
gem "puma"
|
||||
|
@ -79,7 +82,7 @@ group :development, :test do
|
|||
gem "better_errors"
|
||||
gem "bullet"
|
||||
gem "database_cleaner"
|
||||
gem "dotenv-rails", "~> 2.8"
|
||||
gem "dotenv-rails", "~> 3.1"
|
||||
gem "factory_bot_rails", require: false
|
||||
gem "faker"
|
||||
gem "haml_lint", require: false
|
||||
|
@ -89,11 +92,11 @@ group :development, :test do
|
|||
gem "rake"
|
||||
gem "rspec-its", "~> 1.3"
|
||||
gem "rspec-mocks"
|
||||
gem "rspec-rails", "~> 6.0"
|
||||
gem "rspec-sidekiq", "~> 4.0", require: false
|
||||
gem "rubocop", "~> 1.56"
|
||||
gem "rubocop-rails", "~> 2.21"
|
||||
gem "shoulda-matchers", "~> 5.3"
|
||||
gem "rspec-rails", "~> 6.1"
|
||||
gem "rspec-sidekiq", "~> 5.0", require: false
|
||||
gem "rubocop", "~> 1.64"
|
||||
gem "rubocop-rails", "~> 2.25"
|
||||
gem "shoulda-matchers", "~> 6.2"
|
||||
gem "simplecov", require: false
|
||||
gem "simplecov-cobertura", require: false
|
||||
gem "simplecov-json", require: false
|
||||
|
@ -112,7 +115,7 @@ gem "pundit", "~> 2.3"
|
|||
gem "rubyzip", "~> 2.3"
|
||||
|
||||
# to solve https://github.com/jwt/ruby-jwt/issues/526
|
||||
gem "openssl", "~> 3.1"
|
||||
gem "openssl", "~> 3.2"
|
||||
|
||||
# mail 2.8.0 breaks sendmail usage: https://github.com/mikel/mail/issues/1538
|
||||
gem "mail", "~> 2.7.1"
|
||||
|
|
471
Gemfile.lock
471
Gemfile.lock
|
@ -1,10 +1,10 @@
|
|||
GIT
|
||||
remote: https://github.com/raccube/carrierwave_backgrounder.git
|
||||
revision: 41b756f7514c0e410c561bc8b5ee321cd8cce1ee
|
||||
remote: https://github.com/retrospring/hcaptcha
|
||||
revision: f8de70ee2d629ac34395902dbee724c21297960c
|
||||
ref: fix/flash-in-turbo-streams
|
||||
specs:
|
||||
carrierwave_backgrounder (0.4.2)
|
||||
carrierwave (>= 0.5, <= 2.1)
|
||||
mime-types (>= 3.0.0)
|
||||
hcaptcha (7.1.0)
|
||||
json
|
||||
|
||||
GIT
|
||||
remote: https://lab.freak.university/FreakU/questiongenerator
|
||||
|
@ -15,115 +15,127 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.7.6)
|
||||
actionpack (= 6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
actioncable (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.7.6)
|
||||
actionpack (= 6.1.7.6)
|
||||
activejob (= 6.1.7.6)
|
||||
activerecord (= 6.1.7.6)
|
||||
activestorage (= 6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
actionmailbox (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activestorage (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.7.6)
|
||||
actionpack (= 6.1.7.6)
|
||||
actionview (= 6.1.7.6)
|
||||
activejob (= 6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
actionview (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.7.6)
|
||||
actionview (= 6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
actionpack (7.0.8.4)
|
||||
actionview (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.7.6)
|
||||
actionpack (= 6.1.7.6)
|
||||
activerecord (= 6.1.7.6)
|
||||
activestorage (= 6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
actiontext (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activestorage (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
actionview (7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_model_otp (2.3.2)
|
||||
active_model_otp (2.3.4)
|
||||
activemodel
|
||||
rotp (~> 6.2.0)
|
||||
activejob (6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
rotp (~> 6.3.0)
|
||||
activejob (7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
activemodel (7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
activemodel-serializers-xml (1.0.2)
|
||||
activemodel (> 5.x)
|
||||
activesupport (> 5.x)
|
||||
builder (~> 3.1)
|
||||
activerecord (6.1.7.6)
|
||||
activemodel (= 6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
activestorage (6.1.7.6)
|
||||
actionpack (= 6.1.7.6)
|
||||
activejob (= 6.1.7.6)
|
||||
activerecord (= 6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
activerecord (7.0.8.4)
|
||||
activemodel (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
activestorage (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.7.6)
|
||||
activesupport (7.0.8.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.4)
|
||||
addressable (2.8.6)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
ast (2.4.2)
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.19)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
better_errors (2.10.1)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
rouge (>= 1.0.0)
|
||||
binding_of_caller (1.0.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.16.0)
|
||||
bigdecimal (3.1.8)
|
||||
binding_of_caller (1.0.1)
|
||||
debug_inspector (>= 1.2.0)
|
||||
bootsnap (1.18.3)
|
||||
msgpack (~> 1.2)
|
||||
bootstrap_form (5.1.0)
|
||||
actionpack (>= 5.2)
|
||||
activemodel (>= 5.2)
|
||||
builder (3.2.4)
|
||||
bullet (7.0.7)
|
||||
bootstrap_form (5.3.2)
|
||||
actionpack (>= 6.1)
|
||||
activemodel (>= 6.1)
|
||||
builder (3.3.0)
|
||||
bullet (7.1.6)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
carrierwave (2.1.0)
|
||||
carrierwave (2.1.1)
|
||||
activemodel (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
addressable (~> 2.6)
|
||||
image_processing (~> 1.1)
|
||||
mimemagic (>= 0.3.0)
|
||||
mini_mime (>= 0.1.3)
|
||||
ssrf_filter (~> 1.0)
|
||||
carrierwave_backgrounder (0.4.3)
|
||||
carrierwave (>= 0.5, < 2.2)
|
||||
childprocess (5.0.0)
|
||||
chunky_png (1.4.0)
|
||||
colorize (1.1.0)
|
||||
concurrent-ruby (1.2.2)
|
||||
concurrent-ruby (1.3.3)
|
||||
connection_pool (2.4.1)
|
||||
crass (1.0.6)
|
||||
cssbundling-rails (1.2.0)
|
||||
cssbundling-rails (1.4.0)
|
||||
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)
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.3.3)
|
||||
debug_inspector (1.1.0)
|
||||
devise (4.9.2)
|
||||
date (3.3.4)
|
||||
debug_inspector (1.2.0)
|
||||
devise (4.9.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
|
@ -132,15 +144,15 @@ GEM
|
|||
devise-async (1.0.0)
|
||||
activejob (>= 5.0)
|
||||
devise (>= 4.0)
|
||||
devise-i18n (1.10.3)
|
||||
devise (>= 4.8.0)
|
||||
diff-lcs (1.5.0)
|
||||
devise-i18n (1.12.0)
|
||||
devise (>= 4.9.0)
|
||||
diff-lcs (1.5.1)
|
||||
docile (1.4.0)
|
||||
dotenv (2.8.1)
|
||||
dotenv-rails (2.8.1)
|
||||
dotenv (= 2.8.1)
|
||||
railties (>= 3.2)
|
||||
dry-core (1.0.0)
|
||||
dotenv (3.1.2)
|
||||
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)
|
||||
|
@ -149,30 +161,33 @@ GEM
|
|||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-types (1.7.1)
|
||||
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.12.0)
|
||||
excon (0.99.0)
|
||||
factory_bot (6.2.0)
|
||||
erubi (1.13.0)
|
||||
et-orbi (1.2.7)
|
||||
tzinfo
|
||||
excon (0.110.0)
|
||||
factory_bot (6.4.5)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
factory_bot_rails (6.4.3)
|
||||
factory_bot (~> 6.4)
|
||||
railties (>= 5.0.0)
|
||||
fake_email_validator (1.0.11)
|
||||
activemodel
|
||||
mail
|
||||
faker (3.1.1)
|
||||
faker (3.4.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
ffi (1.15.5)
|
||||
fog-aws (3.19.0)
|
||||
ffi (1.16.3)
|
||||
fog-aws (3.23.0)
|
||||
fog-core (~> 2.1)
|
||||
fog-json (~> 1.1)
|
||||
fog-xml (~> 0.1)
|
||||
fog-core (2.3.0)
|
||||
fog-core (2.4.0)
|
||||
builder
|
||||
excon (~> 0.71)
|
||||
formatador (>= 0.2, < 2.0)
|
||||
|
@ -186,41 +201,44 @@ GEM
|
|||
fog-core
|
||||
nokogiri (>= 1.5.11, < 2.0.0)
|
||||
formatador (1.1.0)
|
||||
glob (0.3.1)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
haml (6.1.2)
|
||||
fugit (1.9.0)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
glob (0.4.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
haml (6.3.0)
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
haml_lint (0.50.0)
|
||||
haml (>= 4.0, < 6.2)
|
||||
haml_lint (0.58.0)
|
||||
haml (>= 5.0)
|
||||
parallel (~> 1.10)
|
||||
rainbow
|
||||
rubocop (>= 1.0)
|
||||
sysexits (~> 1.1)
|
||||
hcaptcha (7.1.0)
|
||||
json
|
||||
hkdf (0.3.0)
|
||||
http-2 (0.11.0)
|
||||
httparty (0.21.0)
|
||||
httparty (0.22.0)
|
||||
csv
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (1.14.1)
|
||||
i18n (1.14.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (4.0.0)
|
||||
glob
|
||||
i18n
|
||||
idn-ruby (0.1.4)
|
||||
idn-ruby (0.1.5)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
jsbundling-rails (1.1.2)
|
||||
jsbundling-rails (1.3.0)
|
||||
railties (>= 6.0.0)
|
||||
json (2.6.3)
|
||||
json-schema (4.0.0)
|
||||
json (2.7.2)
|
||||
json-schema (4.3.0)
|
||||
addressable (>= 2.8)
|
||||
jwt (2.7.1)
|
||||
jwt (2.8.2)
|
||||
base64
|
||||
kaminari (1.2.2)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.2)
|
||||
|
@ -234,88 +252,91 @@ GEM
|
|||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
language_server-protocol (3.17.0.3)
|
||||
launchy (2.5.0)
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.8.1)
|
||||
launchy (>= 2.2, < 3)
|
||||
lograge (0.13.0)
|
||||
launchy (3.0.0)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.21.3)
|
||||
loofah (2.22.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
marcel (1.0.2)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.4.1)
|
||||
marcel (1.0.4)
|
||||
method_source (1.1.0)
|
||||
mime-types (3.5.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0218.1)
|
||||
mime-types-data (3.2024.0604)
|
||||
mimemagic (0.4.3)
|
||||
nokogiri (~> 1)
|
||||
rake
|
||||
mini_magick (4.12.0)
|
||||
mini_magick (4.13.1)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.4)
|
||||
minitest (5.20.0)
|
||||
msgpack (1.6.0)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.24.0)
|
||||
msgpack (1.7.2)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multi_xml (0.7.1)
|
||||
bigdecimal (~> 3.1)
|
||||
nested_form (0.3.2)
|
||||
net-http-persistent (4.0.1)
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-http2 (0.18.4)
|
||||
net-http2 (0.18.5)
|
||||
http-2 (~> 0.11)
|
||||
net-imap (0.3.7)
|
||||
net-imap (0.4.14)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.1)
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.3.3)
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.4)
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.6)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.1)
|
||||
openssl (3.1.0)
|
||||
oj (3.16.4)
|
||||
bigdecimal (>= 3.0)
|
||||
openssl (3.2.0)
|
||||
orm_adapter (0.5.0)
|
||||
parallel (1.23.0)
|
||||
parser (3.2.2.3)
|
||||
parallel (1.24.0)
|
||||
parser (3.3.2.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.4)
|
||||
pghero (3.3.4)
|
||||
pg (1.5.6)
|
||||
pghero (3.5.0)
|
||||
activerecord (>= 6)
|
||||
prometheus-client (4.2.1)
|
||||
public_suffix (5.0.1)
|
||||
puma (6.3.1)
|
||||
prometheus-client (4.2.2)
|
||||
public_suffix (5.0.4)
|
||||
puma (6.4.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.1)
|
||||
pundit (2.3.2)
|
||||
activesupport (>= 3.0.0)
|
||||
racc (1.7.1)
|
||||
rack (2.2.8)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.0)
|
||||
rack (2.2.9)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rails (6.1.7.6)
|
||||
actioncable (= 6.1.7.6)
|
||||
actionmailbox (= 6.1.7.6)
|
||||
actionmailer (= 6.1.7.6)
|
||||
actionpack (= 6.1.7.6)
|
||||
actiontext (= 6.1.7.6)
|
||||
actionview (= 6.1.7.6)
|
||||
activejob (= 6.1.7.6)
|
||||
activemodel (= 6.1.7.6)
|
||||
activerecord (= 6.1.7.6)
|
||||
activestorage (= 6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
rails (7.0.8.4)
|
||||
actioncable (= 7.0.8.4)
|
||||
actionmailbox (= 7.0.8.4)
|
||||
actionmailer (= 7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
actiontext (= 7.0.8.4)
|
||||
actionview (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activemodel (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activestorage (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.7.6)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
railties (= 7.0.8.4)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
|
@ -327,7 +348,7 @@ GEM
|
|||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
rails-i18n (7.0.8)
|
||||
rails-i18n (7.0.9)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
rails_admin (3.1.2)
|
||||
|
@ -336,26 +357,28 @@ GEM
|
|||
nested_form (~> 0.3)
|
||||
rails (>= 6.0, < 8)
|
||||
turbo-rails (~> 1.0)
|
||||
railties (6.1.7.6)
|
||||
actionpack (= 6.1.7.6)
|
||||
activesupport (= 6.1.7.6)
|
||||
railties (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rake (13.2.1)
|
||||
redcarpet (3.6.0)
|
||||
redis (4.8.0)
|
||||
regexp_parser (2.8.1)
|
||||
redis (4.8.1)
|
||||
regexp_parser (2.9.2)
|
||||
request_store (1.5.1)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.0)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.2.6)
|
||||
rexml (3.2.8)
|
||||
strscan (>= 3.0.9)
|
||||
rolify (6.0.1)
|
||||
rotp (6.2.2)
|
||||
rouge (4.1.2)
|
||||
rotp (6.3.0)
|
||||
rouge (4.1.3)
|
||||
rpush (7.0.1)
|
||||
activesupport (>= 5.2)
|
||||
jwt (>= 1.5.6)
|
||||
|
@ -370,54 +393,56 @@ GEM
|
|||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
rspec-core (3.12.2)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-expectations (3.12.3)
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-its (1.3.0)
|
||||
rspec-core (>= 3.0.0)
|
||||
rspec-expectations (>= 3.0.0)
|
||||
rspec-mocks (3.12.6)
|
||||
rspec-mocks (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-rails (6.0.3)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.3)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
rspec-core (~> 3.12)
|
||||
rspec-expectations (~> 3.12)
|
||||
rspec-mocks (~> 3.12)
|
||||
rspec-support (~> 3.12)
|
||||
rspec-sidekiq (4.0.2)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-sidekiq (5.0.0)
|
||||
rspec-core (~> 3.0)
|
||||
rspec-expectations (~> 3.0)
|
||||
rspec-mocks (~> 3.0)
|
||||
sidekiq (>= 5, < 8)
|
||||
rspec-support (3.12.1)
|
||||
rubocop (1.56.3)
|
||||
base64 (~> 0.1.1)
|
||||
rspec-support (3.13.1)
|
||||
rubocop (1.64.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.2.2.3)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.28.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.29.0)
|
||||
parser (>= 3.2.1.0)
|
||||
rubocop-rails (2.21.0)
|
||||
rubocop-ast (1.31.3)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-rails (2.25.0)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.1.4)
|
||||
ruby-vips (2.2.1)
|
||||
ffi (~> 1.12)
|
||||
rubyzip (2.3.2)
|
||||
sanitize (6.0.2)
|
||||
rufus-scheduler (3.9.1)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
sanitize (6.1.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
sassc (2.4.0)
|
||||
|
@ -428,20 +453,25 @@ GEM
|
|||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
sentry-rails (5.10.0)
|
||||
sentry-rails (5.17.3)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.10.0)
|
||||
sentry-ruby (5.10.0)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-ruby (5.17.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.10.0)
|
||||
sentry-ruby (~> 5.10.0)
|
||||
sentry-sidekiq (5.17.3)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sidekiq (>= 3.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
shoulda-matchers (6.2.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (6.5.8)
|
||||
sidekiq (6.5.12)
|
||||
connection_pool (>= 2.2.5, < 3)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.5.0, < 5)
|
||||
sidekiq-scheduler (5.0.3)
|
||||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 6, < 8)
|
||||
tilt (>= 1.4.0)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
|
@ -454,23 +484,24 @@ GEM
|
|||
json
|
||||
simplecov
|
||||
simplecov_json_formatter (0.1.4)
|
||||
spring (4.1.1)
|
||||
sprockets (4.2.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (>= 2.2.4, < 4)
|
||||
sprockets-rails (3.4.2)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
sprockets-rails (3.5.1)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
sprockets (>= 3.0.0)
|
||||
ssrf_filter (1.1.2)
|
||||
strscan (3.1.0)
|
||||
sysexits (1.2.0)
|
||||
temple (0.10.2)
|
||||
thor (1.2.2)
|
||||
tilt (2.2.0)
|
||||
timeout (0.4.0)
|
||||
temple (0.10.3)
|
||||
thor (1.3.1)
|
||||
tilt (2.3.0)
|
||||
timeout (0.4.1)
|
||||
tldv (0.1.0)
|
||||
tldv-data (~> 1.0)
|
||||
tldv-data (1.0.2023031000)
|
||||
turbo-rails (1.4.0)
|
||||
tldv-data (1.0.2023080900)
|
||||
turbo-rails (1.5.0)
|
||||
actionpack (>= 6.0.0)
|
||||
activejob (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
|
@ -481,40 +512,47 @@ GEM
|
|||
concurrent-ruby (~> 1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8)
|
||||
unicode-display_width (2.4.2)
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.5.0)
|
||||
uniform_notifier (1.16.0)
|
||||
view_component (3.12.1)
|
||||
activesupport (>= 5.2.0, < 8.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
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)
|
||||
zeitwerk (2.6.11)
|
||||
zeitwerk (2.6.16)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
active_model_otp
|
||||
bcrypt (~> 3.1.19)
|
||||
bcrypt (~> 3.1.20)
|
||||
better_errors
|
||||
binding_of_caller
|
||||
bootsnap
|
||||
bootstrap_form (~> 5.0)
|
||||
bullet
|
||||
carrierwave (~> 2.0)
|
||||
carrierwave_backgrounder!
|
||||
carrierwave (~> 2.1)
|
||||
carrierwave_backgrounder (~> 0.4.2)
|
||||
colorize
|
||||
connection_pool
|
||||
cssbundling-rails (~> 1.2)
|
||||
cssbundling-rails (~> 1.4)
|
||||
database_cleaner
|
||||
devise (~> 4.9)
|
||||
devise-async
|
||||
devise-i18n
|
||||
dotenv-rails (~> 2.8)
|
||||
dotenv-rails (~> 3.1)
|
||||
dry-initializer (~> 3.1)
|
||||
dry-types (~> 1.7)
|
||||
factory_bot_rails
|
||||
|
@ -523,14 +561,14 @@ DEPENDENCIES
|
|||
fog-aws
|
||||
fog-core
|
||||
fog-local
|
||||
haml (~> 6.1)
|
||||
haml (~> 6.3)
|
||||
haml_lint
|
||||
hcaptcha (~> 7.0)
|
||||
hcaptcha!
|
||||
httparty
|
||||
i18n-js (= 4.0)
|
||||
jsbundling-rails (~> 1.1)
|
||||
jsbundling-rails (~> 1.3)
|
||||
json-schema
|
||||
jwt (~> 2.7)
|
||||
jwt (~> 2.8)
|
||||
letter_opener
|
||||
lograge
|
||||
mail (~> 2.7.1)
|
||||
|
@ -539,14 +577,13 @@ DEPENDENCIES
|
|||
net-pop
|
||||
net-smtp
|
||||
oj
|
||||
openssl (~> 3.1)
|
||||
openssl (~> 3.2)
|
||||
pg
|
||||
pghero
|
||||
prometheus-client (~> 4.2)
|
||||
puma
|
||||
pundit (~> 2.3)
|
||||
questiongenerator (~> 1.2)!
|
||||
rails (~> 6.1)
|
||||
rails (~> 7.0.8)
|
||||
rails-controller-testing
|
||||
rails-i18n (~> 7.0)
|
||||
rails_admin
|
||||
|
@ -558,27 +595,29 @@ DEPENDENCIES
|
|||
rqrcode
|
||||
rspec-its (~> 1.3)
|
||||
rspec-mocks
|
||||
rspec-rails (~> 6.0)
|
||||
rspec-sidekiq (~> 4.0)
|
||||
rubocop (~> 1.56)
|
||||
rubocop-rails (~> 2.21)
|
||||
rspec-rails (~> 6.1)
|
||||
rspec-sidekiq (~> 5.0)
|
||||
rubocop (~> 1.64)
|
||||
rubocop-rails (~> 2.25)
|
||||
rubyzip (~> 2.3)
|
||||
sanitize
|
||||
sassc-rails
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
sentry-sidekiq
|
||||
shoulda-matchers (~> 5.3)
|
||||
shoulda-matchers (~> 6.2)
|
||||
sidekiq (< 7)
|
||||
sidekiq-scheduler
|
||||
simplecov
|
||||
simplecov-cobertura
|
||||
simplecov-json
|
||||
spring (~> 4.1)
|
||||
sprockets (~> 4.2)
|
||||
sprockets-rails
|
||||
tldv (~> 0.1.0)
|
||||
turbo-rails
|
||||
twitter-text
|
||||
view_component
|
||||
web-push
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.18
|
||||
2.5.5
|
||||
|
|
|
@ -4,7 +4,7 @@ Currently the only change is to enable long questions by default.
|
|||
|
||||
## Licence
|
||||
|
||||
Copyright (C) 2014-2022 The Retrospring team and contributors
|
||||
Copyright (C) 2014-2024 The Retrospring team and contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
|
|
2
Rakefile
2
Rakefile
|
@ -7,7 +7,7 @@ require File.expand_path("config/application", __dir__)
|
|||
|
||||
Rails.application.load_tasks
|
||||
|
||||
namespace :justask do # rubocop:disable Metrics/BlockLength
|
||||
namespace :justask do
|
||||
desc "Gives admin status to a user."
|
||||
task :admin, [:screen_name] => :environment do |_t, args|
|
||||
abort "screen name required" if args[:screen_name].nil?
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
// This is a stub so that we don't have to install actiontext
|
|
@ -0,0 +1 @@
|
|||
// This is a stub so that we don't have to install Trix
|
|
@ -25,3 +25,12 @@
|
|||
.pull-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
// FIXME: Backport from Bootstrap 5.3, remove once updated
|
||||
.z-n1 {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.grid-row-1 {
|
||||
grid-row: 1;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
@use "sass:map";
|
||||
|
||||
.answerbox {
|
||||
&__question-text,
|
||||
&__question-user,
|
||||
&__answer-user,
|
||||
&__answer-date {
|
||||
margin-bottom: 0;
|
||||
|
@ -25,7 +23,6 @@
|
|||
margin-bottom: map.get($spacers, 3);
|
||||
}
|
||||
|
||||
&__question-user-avatar,
|
||||
&__answer-user-avatar {
|
||||
margin-right: map.get($spacers, 2);
|
||||
border-radius: $avatar-border-radius;
|
||||
|
@ -38,8 +35,9 @@
|
|||
}
|
||||
|
||||
&__action {
|
||||
padding-left: 0;
|
||||
padding-right: map.get($spacers, 1);
|
||||
color: RGBA(var(--raised-text), 0.75);
|
||||
padding: var(--btn-padding-y);
|
||||
margin-right: map.get($spacers, 1);
|
||||
text-decoration: none;
|
||||
|
||||
& i {
|
||||
|
@ -50,25 +48,29 @@
|
|||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: RGBA(var(--raised-text), 1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&[name="ab-smile"],
|
||||
&[name="ab-smile-comment"] {
|
||||
&.smile {
|
||||
color: var(--primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-action="unsmile"] {
|
||||
color: var(--success);
|
||||
&.unsmile {
|
||||
color: var(--success);
|
||||
|
||||
&:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
&.dropdown-toggle::after {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
|
@ -97,9 +99,3 @@
|
|||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
body:not(.cap-web-share) {
|
||||
[name="ab-share"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.answerbox__question-text {
|
||||
&.question__text {
|
||||
max-height: 15rem;
|
||||
|
||||
@include media-breakpoint-up('sm') {
|
||||
|
|
|
@ -23,6 +23,10 @@
|
|||
.text-muted {
|
||||
color: RGBA(var(--primary-text), 0.8) !important;
|
||||
}
|
||||
|
||||
.btn {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,3 +61,10 @@
|
|||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
body:not(.cap-web-share) {
|
||||
|
||||
[data-controller="share"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
@use "sass:map";
|
||||
|
||||
.question {
|
||||
&--fixed {
|
||||
position: absolute;
|
||||
|
||||
&__avatar {
|
||||
margin-right: map.get($spacers, 2);
|
||||
border-radius: $avatar-border-radius;
|
||||
}
|
||||
|
||||
&__text,
|
||||
&__user {
|
||||
margin-bottom: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__text {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&--sticky {
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
|
||||
@include media-breakpoint-up('sm') {
|
||||
position: fixed;
|
||||
position: sticky;
|
||||
top: $navbar-height;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
.btn {
|
||||
--btn-padding-x: 1rem;
|
||||
--btn-border-radius: 2rem;
|
||||
color: RGB(var(--body-text));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
|
|
|
@ -20,3 +20,18 @@
|
|||
background-color: var(--raised-accent);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.form-check-input:focus {
|
||||
box-shadow: rgba(var(--primary-rgb), 0.25) 0 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.input-group .button_to .btn {
|
||||
margin-left: -1px;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
// This is a stub so that we don't have to install Trix
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationComponent < ViewComponent::Base
|
||||
include ApplicationHelper
|
||||
delegate :current_user, to: :helpers
|
||||
delegate :user_signed_in?, to: :helpers
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
%img{ class: avatar_classes,
|
||||
alt: alt_text,
|
||||
src: avatar_image,
|
||||
loading: :lazy }
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AvatarComponent < ViewComponent::Base
|
||||
ALLOWED_SIZES = %w[xs sm md lg xl xxl].freeze
|
||||
|
||||
def initialize(user:, size:, classes: [])
|
||||
@user = user
|
||||
@size = size if ALLOWED_SIZES.include? size
|
||||
@classes = classes
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def size_to_version(size)
|
||||
case size
|
||||
when "xs", "sm"
|
||||
:small
|
||||
when "md", "lg"
|
||||
:medium
|
||||
when "xl", "xxl"
|
||||
:large
|
||||
end
|
||||
end
|
||||
|
||||
def alt_text = "@#{@user.screen_name}"
|
||||
|
||||
def avatar_classes = @classes.unshift("avatar-#{@size}")
|
||||
|
||||
def avatar_image = @user.profile_picture.url(size_to_version(@size))
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
%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
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CommentComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
include BootstrapHelper
|
||||
include UserHelper
|
||||
|
||||
def initialize(comment:, answer:)
|
||||
@comment = comment
|
||||
@answer = answer
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# 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
|
|
@ -0,0 +1,9 @@
|
|||
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"
|
|
@ -0,0 +1,34 @@
|
|||
.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"
|
|
@ -3,9 +3,7 @@
|
|||
require "cgi"
|
||||
|
||||
class Ajax::AnswerController < AjaxController
|
||||
include SocialHelper::TwitterMethods
|
||||
include SocialHelper::TumblrMethods
|
||||
include SocialHelper::TelegramMethods
|
||||
include SocialHelper
|
||||
|
||||
def create
|
||||
params.require :id
|
||||
|
@ -15,7 +13,7 @@ class Ajax::AnswerController < AjaxController
|
|||
inbox = (params[:inbox] == "true")
|
||||
|
||||
if inbox
|
||||
inbox_entry = Inbox.find(params[:id])
|
||||
inbox_entry = InboxEntry.find(params[:id])
|
||||
|
||||
unless current_user == inbox_entry.user
|
||||
@response[:status] = :fail
|
||||
|
@ -46,9 +44,8 @@ class Ajax::AnswerController < AjaxController
|
|||
|
||||
return if inbox
|
||||
|
||||
# this assign is needed because shared/_answerbox relies on it, I think
|
||||
@question = 1
|
||||
@response[:render] = render_to_string(partial: "answerbox", locals: { a: answer, show_question: false, subscribed_answer_ids: [answer.id] })
|
||||
@response[:render] = render_to_string(partial: "answerbox", locals: { a: answer, show_question: false })
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
@ -62,7 +59,7 @@ class Ajax::AnswerController < AjaxController
|
|||
return
|
||||
end
|
||||
|
||||
Inbox.create!(user: answer.user, question: answer.question, new: true, returning: true) if answer.user == current_user
|
||||
InboxEntry.create!(user: answer.user, question: answer.question, new: true, returning: true) if answer.user == current_user
|
||||
answer.destroy
|
||||
|
||||
@response[:status] = :okay
|
||||
|
@ -73,7 +70,10 @@ class Ajax::AnswerController < AjaxController
|
|||
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)),
|
||||
|
|
|
@ -14,10 +14,12 @@ class Ajax::CommentController < AjaxController
|
|||
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 })
|
||||
@response[:render] = render_to_string(partial: "answerbox/comments", locals: { a: answer, comments: })
|
||||
@response[:count] = answer.comment_count
|
||||
end
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Ajax::InboxController < AjaxController
|
||||
def remove
|
||||
params.require :id
|
||||
|
||||
inbox = Inbox.find(params[:id])
|
||||
inbox = InboxEntry.find(params[:id])
|
||||
|
||||
unless current_user == inbox.user
|
||||
@response[:status] = :fail
|
||||
|
@ -28,7 +30,7 @@ class Ajax::InboxController < AjaxController
|
|||
raise unless user_signed_in?
|
||||
|
||||
begin
|
||||
Inbox.where(user: current_user).each { |i| i.remove }
|
||||
InboxEntry.where(user: current_user).find_each(&:remove)
|
||||
rescue => e
|
||||
Sentry.capture_exception(e)
|
||||
@response[:status] = :err
|
||||
|
@ -43,10 +45,10 @@ class Ajax::InboxController < AjaxController
|
|||
|
||||
def remove_all_author
|
||||
begin
|
||||
@target_user = User.where('LOWER(screen_name) = ?', params[:author].downcase).first!
|
||||
@inbox = current_user.inboxes.joins(:question)
|
||||
.where(questions: { user_id: @target_user.id, author_is_anonymous: false })
|
||||
@inbox.each { |i| i.remove }
|
||||
@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
|
||||
|
|
|
@ -84,7 +84,7 @@ class Ajax::ModerationController < AjaxController
|
|||
target_user = User.find_by_screen_name!(params[:user])
|
||||
|
||||
@response[:message] = t(".error")
|
||||
return unless %w(moderator admin).include? params[:type].downcase
|
||||
return unless %w[moderator administrator].include? params[:type].downcase
|
||||
|
||||
unless current_user.has_cached_role?(:administrator)
|
||||
@response[:status] = :nopriv
|
||||
|
@ -94,7 +94,7 @@ class Ajax::ModerationController < AjaxController
|
|||
|
||||
@response[:checked] = status
|
||||
type = params[:type].downcase
|
||||
target_role = {'admin' => 'administrator'}.fetch(type, type).to_sym
|
||||
target_role = type.to_sym
|
||||
|
||||
if status
|
||||
target_user.add_role target_role
|
||||
|
|
|
@ -1,19 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Ajax::QuestionController < AjaxController
|
||||
def destroy
|
||||
params.require :question
|
||||
|
||||
UseCase::Question::Destroy.call(
|
||||
question_id: params[:question],
|
||||
current_user: current_user
|
||||
)
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:message] = t(".success")
|
||||
@response[:success] = true
|
||||
end
|
||||
|
||||
def create
|
||||
params.require :question
|
||||
params.require :anonymousQuestion
|
||||
|
@ -24,14 +11,15 @@ class Ajax::QuestionController < AjaxController
|
|||
@response = {
|
||||
success: true,
|
||||
message: t(".success"),
|
||||
status: :okay
|
||||
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)
|
||||
author_identifier: AnonymousBlock.get_identifier(request.remote_ip),
|
||||
send_to_own_inbox: params[:sendToOwnInbox],
|
||||
)
|
||||
return
|
||||
end
|
||||
|
@ -41,7 +29,20 @@ class Ajax::QuestionController < AjaxController
|
|||
target_user_id: params[:rcpt],
|
||||
content: params[:question],
|
||||
anonymous: params[:anonymousQuestion],
|
||||
author_identifier: AnonymousBlock.get_identifier(request.remote_ip)
|
||||
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,35 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Ajax::RelationshipController < AjaxController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def create
|
||||
params.require :screen_name
|
||||
|
||||
UseCase::Relationship::Create.call(
|
||||
source_user: current_user,
|
||||
target_user: ::User.find_by!(screen_name: params[:screen_name]),
|
||||
type: params[:type]
|
||||
)
|
||||
@response[:success] = true
|
||||
@response[:message] = t(".#{params[:type]}.success")
|
||||
rescue Errors::Base => e
|
||||
@response[:message] = t(e.locale_tag)
|
||||
ensure
|
||||
return_response
|
||||
end
|
||||
|
||||
def destroy
|
||||
UseCase::Relationship::Destroy.call(
|
||||
source_user: current_user,
|
||||
target_user: ::User.find_by!(screen_name: params[:screen_name]),
|
||||
type: params[:type]
|
||||
)
|
||||
@response[:success] = true
|
||||
@response[:message] = t(".#{params[:type]}.success")
|
||||
rescue Errors::Base => e
|
||||
@response[:message] = t(e.locale_tag)
|
||||
ensure
|
||||
return_response
|
||||
end
|
||||
end
|
|
@ -1,85 +0,0 @@
|
|||
class Ajax::SmileController < AjaxController
|
||||
def create
|
||||
params.require :id
|
||||
|
||||
answer = Answer.find(params[:id])
|
||||
|
||||
begin
|
||||
current_user.smile answer
|
||||
rescue Errors::Base => e
|
||||
@response[:status] = e.code
|
||||
@response[:message] = I18n.t(e.locale_tag)
|
||||
return
|
||||
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 destroy
|
||||
params.require :id
|
||||
|
||||
answer = Answer.find(params[:id])
|
||||
|
||||
begin
|
||||
current_user.unsmile answer
|
||||
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 create_comment
|
||||
params.require :id
|
||||
|
||||
comment = Comment.find(params[:id])
|
||||
|
||||
begin
|
||||
current_user.smile comment
|
||||
rescue Errors::Base => e
|
||||
@response[:status] = e.code
|
||||
@response[:message] = I18n.t(e.locale_tag)
|
||||
return
|
||||
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 destroy_comment
|
||||
params.require :id
|
||||
|
||||
comment = Comment.find(params[:id])
|
||||
|
||||
begin
|
||||
current_user.unsmile comment
|
||||
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
|
||||
end
|
|
@ -1,17 +0,0 @@
|
|||
class Ajax::SubscriptionController < AjaxController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def subscribe
|
||||
params.require :answer
|
||||
@response[:status] = :okay
|
||||
result = Subscription.subscribe(current_user, Answer.find(params[:answer]))
|
||||
@response[:success] = result.present?
|
||||
end
|
||||
|
||||
def unsubscribe
|
||||
params.require :answer
|
||||
@response[:status] = :okay
|
||||
result = Subscription.unsubscribe(current_user, Answer.find(params[:answer]))
|
||||
@response[:success] = result&.destroyed? || false
|
||||
end
|
||||
end
|
|
@ -41,7 +41,7 @@ class Ajax::WebPushController < AjaxController
|
|||
@response[:message] = t(".subscription_count", count: current_user.web_push_subscriptions.count)
|
||||
end
|
||||
|
||||
def unsubscribe # rubocop:disable Metrics/AbcSize
|
||||
def unsubscribe
|
||||
removed = if params.key?(:endpoint)
|
||||
current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).destroy_all
|
||||
else
|
||||
|
|
|
@ -20,8 +20,8 @@ class AnonymousBlockController < ApplicationController
|
|||
target_user: question.user
|
||||
)
|
||||
|
||||
inbox_id = question.inboxes.first&.id
|
||||
question.inboxes.first&.destroy
|
||||
inbox_id = question.inbox_entries.first&.id
|
||||
question.inbox_entries.first&.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
|
|
|
@ -8,13 +8,11 @@ class AnswerController < ApplicationController
|
|||
turbo_stream_actions :pin, :unpin
|
||||
|
||||
def show
|
||||
@answer = Answer.includes(comments: %i[user smiles], question: [:user], smiles: [:user]).find(params[:id])
|
||||
@answer = Answer.for_user(current_user).includes(question: [:user], smiles: [:user]).find(params[:id])
|
||||
@display_all = true
|
||||
@subscribed_answer_ids = []
|
||||
|
||||
return unless user_signed_in?
|
||||
|
||||
@subscribed_answer_ids = Subscription.where(user: current_user, answer: @answer).pluck(:answer_id)
|
||||
mark_notifications_as_read
|
||||
end
|
||||
|
||||
|
@ -51,11 +49,12 @@ class AnswerController < ApplicationController
|
|||
private
|
||||
|
||||
def mark_notifications_as_read
|
||||
Notification.where(recipient_id: current_user.id, new: true)
|
||||
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
|
||||
|
|
|
@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
|
|||
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(&)
|
||||
|
@ -30,13 +31,15 @@ class ApplicationController < ActionController::Base
|
|||
# obligatory '2001: A Space Odyssey' reference
|
||||
flash[:notice] = t("user.sessions.create.banned", name:)
|
||||
current_ban = current_user.bans.current.first
|
||||
unless current_ban&.reason.nil?
|
||||
flash[:notice] += "\n#{t('user.sessions.create.reason', reason: current_ban.reason)}"
|
||||
end
|
||||
unless current_ban&.permanent?
|
||||
# TODO format banned_until
|
||||
flash[:notice] += "\n#{t('user.sessions.create.until', time: current_ban.expires_at)}"
|
||||
end
|
||||
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
|
||||
|
@ -46,6 +49,18 @@ class ApplicationController < ActionController::Base
|
|||
@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
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# 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
|
|
@ -0,0 +1,10 @@
|
|||
# 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
|
|
@ -5,8 +5,6 @@ module PaginatesAnswers
|
|||
@answers = yield(last_id: params[:last_id])
|
||||
answer_ids = @answers.map(&:id)
|
||||
@answers_last_id = answer_ids.min
|
||||
answer_ids += @pinned_answers.pluck(:id) if @pinned_answers.present?
|
||||
@more_data_available = !yield(last_id: @answers_last_id, size: 1).count.zero?
|
||||
@subscribed_answer_ids = Subscription.where(user: current_user, answer_id: answer_ids).pluck(:answer_id) if user_signed_in?
|
||||
@more_data_available = !yield(last_id: @answers_last_id, size: 1).select("answers.id").count.zero?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,6 +23,8 @@ module TurboStreamable
|
|||
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
|
||||
|
|
|
@ -1,36 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DiscoverController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
unless APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod?
|
||||
return redirect_to root_path
|
||||
end
|
||||
return redirect_to root_path unless APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod?
|
||||
|
||||
top_x = 10 # only display the top X items
|
||||
top_x = 10 # only display the top X items
|
||||
week_ago = Time.now.utc.ago(1.week)
|
||||
|
||||
@popular_answers = Answer.where("created_at > ?", Time.now.ago(1.week)).order(:smile_count).reverse_order.limit(top_x).includes(:question, :user, :comments)
|
||||
@most_discussed = Answer.where("created_at > ?", Time.now.ago(1.week)).order(:comment_count).reverse_order.limit(top_x).includes(:question, :user, :comments)
|
||||
@popular_questions = Question.where("created_at > ?", Time.now.ago(1.week)).order(:answer_count).reverse_order.limit(top_x).includes(:user)
|
||||
@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)
|
||||
|
||||
answer_ids = @popular_answers.map(&:id) + @most_discussed.map(&:id)
|
||||
@subscribed_answer_ids = Subscription.where(user: current_user, answer_id: answer_ids).pluck(:answer_id)
|
||||
|
||||
# .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 > ?", Time.now.ago(1.week)).
|
||||
where(author_is_anonymous: false).
|
||||
group(:user_id).
|
||||
order('question_count').
|
||||
reverse_order.limit(top_x)
|
||||
@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 > ?", Time.now.ago(1.week)).
|
||||
group(:user_id).
|
||||
order('answer_count').
|
||||
reverse_order.limit(top_x)
|
||||
@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
|
||||
|
|
|
@ -3,30 +3,17 @@
|
|||
class InboxController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
after_action :mark_inbox_entries_as_read, only: %i[show]
|
||||
|
||||
def show # rubocop:disable Metrics/MethodLength
|
||||
find_author
|
||||
def show
|
||||
find_inbox_entries
|
||||
|
||||
if @author_user && @inbox_count.zero?
|
||||
# rubocop disabled because of a false positive
|
||||
flash[:info] = t(".author.info", author: @author) # rubocop:disable Rails/ActionControllerFlashBeforeRender
|
||||
redirect_to inbox_path(last_id: params[:last_id])
|
||||
return
|
||||
end
|
||||
|
||||
@delete_id = find_delete_id
|
||||
@disabled = true if @inbox.empty?
|
||||
|
||||
respond_to do |format|
|
||||
format.html { render "show" }
|
||||
format.turbo_stream do
|
||||
render "show", layout: false, status: :see_other
|
||||
mark_inbox_entries_as_read
|
||||
|
||||
# rubocop disabled as just flipping a flag doesn't need to have validations to be run
|
||||
@inbox.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.turbo_stream
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -36,7 +23,7 @@ class InboxController < ApplicationController
|
|||
author_identifier: "justask",
|
||||
user: current_user)
|
||||
|
||||
inbox = Inbox.create!(user: current_user, question_id: question.id, new: true)
|
||||
inbox = InboxEntry.create!(user: current_user, question_id: question.id, new: true)
|
||||
increment_metric
|
||||
|
||||
respond_to do |format|
|
||||
|
@ -52,41 +39,29 @@ class InboxController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def find_author
|
||||
return if params[:author].blank?
|
||||
|
||||
@author = params[:author]
|
||||
|
||||
@author_user = User.where("LOWER(screen_name) = ?", @author.downcase).first
|
||||
flash.now[:error] = t(".author.error", author: @author) unless @author_user
|
||||
def filter_params
|
||||
params.slice(*InboxFilter::KEYS).permit(*InboxFilter::KEYS)
|
||||
end
|
||||
|
||||
def find_inbox_entries
|
||||
@inbox = current_user.cursored_inbox(last_id: params[:last_id]).then(&method(:filter_author_chain))
|
||||
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 = current_user.cursored_inbox(last_id: @inbox_last_id, size: 1).then(&method(:filter_author_chain)).count.positive?
|
||||
@inbox_count = current_user.inboxes.then(&method(:filter_author_chain)).count
|
||||
@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 @author_user && @inbox_count.positive?
|
||||
return "ib-delete-all-author" if params[:author].present? && @inbox_count.positive?
|
||||
|
||||
"ib-delete-all"
|
||||
end
|
||||
|
||||
def filter_author_chain(query)
|
||||
return query unless @author_user
|
||||
|
||||
query
|
||||
.joins(:question)
|
||||
.where(questions: { user: @author_user, author_is_anonymous: false })
|
||||
end
|
||||
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
def mark_inbox_entries_as_read
|
||||
# using .dup to not modify @inbox -- useful in tests
|
||||
@inbox&.dup&.update_all(new: false)
|
||||
current_user.touch(:inbox_updated_at)
|
||||
updated = @inbox&.dup&.update_all(new: false)
|
||||
current_user.touch(:inbox_updated_at) if updated.positive?
|
||||
end
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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?
|
||||
|
||||
render inline: turbo_frame_tag("modal") # rubocop:disable Rails/RenderInline
|
||||
end
|
||||
end
|
|
@ -5,14 +5,22 @@ class Moderation::InboxController < ApplicationController
|
|||
|
||||
def index
|
||||
@user = User.find_by(screen_name: params[:user])
|
||||
@inboxes = @user.cursored_inbox(last_id: params[:last_id])
|
||||
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 = !@user.cursored_inbox(last_id: @inbox_last_id, size: 1).count.zero?
|
||||
@inbox_count = @user.inboxes.count
|
||||
@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
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
|
||||
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
|
||||
@type = params[:type]
|
||||
@reports = list_reports(type: @type, last_id: params[:last_id])
|
||||
filter = ReportFilter.new(filter_params)
|
||||
@reports = filter.cursored_results(last_id: params[:last_id])
|
||||
@reports_last_id = @reports.map(&:id).min
|
||||
@more_data_available = !list_reports(type: @type, last_id: @reports_last_id, size: 1).count.zero?
|
||||
@more_data_available = filter.cursored_results(last_id: @reports_last_id, size: 1).count.positive?
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
@ -17,13 +20,29 @@ class Moderation::ReportsController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def list_reports(type:, last_id:, size: nil)
|
||||
cursor_params = { last_id:, size: }.compact
|
||||
def filter_params
|
||||
params.slice(*ReportFilter::KEYS).permit(*ReportFilter::KEYS)
|
||||
end
|
||||
|
||||
if type == "all"
|
||||
Report.cursored_reports(**cursor_params)
|
||||
else
|
||||
Report.cursored_reports_of_type(type, **cursor_params)
|
||||
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
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
class NotificationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
after_action :mark_notifications_as_read, only: %i[index]
|
||||
|
||||
TYPE_MAPPINGS = {
|
||||
"answer" => Notification::QuestionAnswered.name,
|
||||
"comment" => Notification::Commented.name,
|
||||
|
@ -18,6 +16,7 @@ class NotificationsController < ApplicationController
|
|||
@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
|
||||
|
@ -52,8 +51,8 @@ class NotificationsController < ApplicationController
|
|||
# rubocop:disable Rails/SkipsModelValidations
|
||||
def mark_notifications_as_read
|
||||
# using .dup to not modify @notifications -- useful in tests
|
||||
@notifications&.dup&.update_all(new: false)
|
||||
current_user.touch(:notifications_updated_at)
|
||||
updated = @notifications&.dup&.update_all(new: false)
|
||||
current_user.touch(:notifications_updated_at) if updated.positive?
|
||||
end
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@ class QuestionController < ApplicationController
|
|||
@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:).count.zero?
|
||||
@subscribed = Subscription.where(user: current_user, answer_id: answer_ids).pluck(:answer_id) if user_signed_in?
|
||||
@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
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
# 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
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RelationshipsController < ApplicationController
|
||||
include TurboStreamable
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
turbo_stream_actions :create, :destroy
|
||||
|
||||
def create
|
||||
params.require :screen_name
|
||||
|
||||
UseCase::Relationship::Create.call(
|
||||
source_user: current_user,
|
||||
target_user: ::User.find_by!(screen_name: params[:screen_name]),
|
||||
type: params[:type],
|
||||
)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace("#{params[:type]}-#{params[:screen_name]}", partial: "relationships/destroy", locals: { type: params[:type], screen_name: params[:screen_name] }),
|
||||
render_toast(t(".#{params[:type]}.success"))
|
||||
]
|
||||
end
|
||||
|
||||
format.html { redirect_back(fallback_location: user_path(username: params[:screen_name])) }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
UseCase::Relationship::Destroy.call(
|
||||
source_user: current_user,
|
||||
target_user: ::User.find_by!(screen_name: params[:screen_name]),
|
||||
type: params[:type],
|
||||
)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace("#{params[:type]}-#{params[:screen_name]}", partial: "relationships/create", locals: { type: params[:type], screen_name: params[:screen_name] }),
|
||||
render_toast(t(".#{params[:type]}.success"))
|
||||
]
|
||||
end
|
||||
|
||||
format.html { redirect_back(fallback_location: user_path(username: params[:screen_name])) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -21,9 +21,12 @@ class Settings::ExportController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
def mark_notifications_as_read
|
||||
Notification::DataExported
|
||||
.where(recipient: current_user, new: true)
|
||||
.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
|
||||
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
|
||||
|
|
|
@ -19,6 +19,6 @@ class Settings::PrivacyController < ApplicationController
|
|||
else
|
||||
flash[:error] = t(".error")
|
||||
end
|
||||
redirect_to settings_privacy_path
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,6 @@ class Settings::ProfileController < ApplicationController
|
|||
flash[:error] = t(".error")
|
||||
end
|
||||
|
||||
redirect_to settings_profile_path
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,9 +12,10 @@ class Settings::ProfilePictureController < ApplicationController
|
|||
text += t(".notice.profile_header") if user_attributes[:profile_header]
|
||||
flash[:success] = text
|
||||
else
|
||||
flash[:error] = t(".error")
|
||||
# CarrierWave resets the image to the default upon an error
|
||||
current_user.reload
|
||||
end
|
||||
|
||||
redirect_to settings_profile_path
|
||||
render "settings/profile/edit"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@ class Settings::ThemeController < ApplicationController
|
|||
else
|
||||
flash[:error] = t(".error", errors: current_user.theme.errors.messages.flatten.join(" "))
|
||||
end
|
||||
redirect_to settings_theme_path
|
||||
render :edit
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# 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
|
|
@ -32,10 +32,9 @@ class TimelineController < ApplicationController
|
|||
|
||||
def paginate_timeline
|
||||
@timeline = yield(last_id: params[:last_id])
|
||||
timeline_ids = @timeline.map(&: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).count.zero?
|
||||
@subscribed_answer_ids = Subscription.where(user: current_user, answer_id: timeline_ids).pluck(:answer_id)
|
||||
@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" }
|
||||
|
|
|
@ -8,8 +8,8 @@ class UserController < ApplicationController
|
|||
after_action :mark_notification_as_read, only: %i[show]
|
||||
|
||||
def show
|
||||
@pinned_answers = @user.answers.pinned.order(pinned_at: :desc).limit(10)
|
||||
paginate_answers { |args| @user.cursored_answers(**args) }
|
||||
@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
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AjaxHelper
|
||||
end
|
||||
|
|
|
@ -4,13 +4,13 @@ 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: content) }, "\n")
|
||||
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: name, content: content) }, "\n")
|
||||
safe_join(values.map { |name, content| tag.meta(name:, content:) }, "\n")
|
||||
end
|
||||
|
||||
# @param user [User]
|
||||
|
@ -22,7 +22,7 @@ module ApplicationHelper::GraphMethods
|
|||
"og:url": user_url(user),
|
||||
"og:description": user.profile.description,
|
||||
"og:site_name": APP_CONFIG["site_name"],
|
||||
"profile:username": user.screen_name
|
||||
"profile:username": user.screen_name,
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -33,7 +33,7 @@ module ApplicationHelper::GraphMethods
|
|||
"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)
|
||||
"twitter:image": full_profile_picture_url(user),
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -45,7 +45,7 @@ module ApplicationHelper::GraphMethods
|
|||
"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"]
|
||||
"og:site_name": APP_CONFIG["site_name"],
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ module ApplicationHelper::TitleMethods
|
|||
def question_title(question)
|
||||
context_user = question.answers&.first&.user if question.direct
|
||||
name = user_screen_name question.user,
|
||||
context_user: context_user,
|
||||
context_user:,
|
||||
author_identifier: question.author_is_anonymous ? question.author_identifier : nil,
|
||||
url: false
|
||||
generate_title name, "asked", question.content
|
||||
|
|
|
@ -8,6 +8,7 @@ module BootstrapHelper
|
|||
badge_attr: {},
|
||||
icon: nil,
|
||||
class: "",
|
||||
id: nil,
|
||||
hotkey: nil,
|
||||
}.merge(options)
|
||||
|
||||
|
@ -24,24 +25,24 @@ module BootstrapHelper
|
|||
"#{content_tag(:i, '', class: "fa fa-#{options[:icon]}")} #{body}"
|
||||
end
|
||||
end
|
||||
if options[:badge].present? || options.dig(:badge_attr, :data)&.has_key?(:controller)
|
||||
if options[:badge].present? || options.dig(:badge_attr, :data)&.key?(:controller)
|
||||
badge_class = [
|
||||
"badge",
|
||||
("badge-#{options[:badge_color]}" unless options[:badge_color].nil?),
|
||||
("badge-pill" if options[:badge_pill])
|
||||
].compact.join(" ")
|
||||
|
||||
body += " #{content_tag(:span, options[:badge], class: badge_class, **options[:badge_attr])}".html_safe
|
||||
body += " #{content_tag(:span, options[:badge], class: badge_class, **options[:badge_attr])}".html_safe # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
|
||||
content_tag(:li, link_to(body.html_safe, path, class: "nav-link", data: { hotkey: options[:hotkey] }), class: classes)
|
||||
content_tag(:li, link_to(body.html_safe, path, class: "nav-link", data: { hotkey: options[:hotkey] }), class: classes, id: options[:id]) # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
|
||||
def list_group_item(body, path, options = {})
|
||||
options = {
|
||||
badge: nil,
|
||||
badge_color: nil,
|
||||
class: ""
|
||||
class: "",
|
||||
}.merge(options)
|
||||
|
||||
classes = [
|
||||
|
@ -53,25 +54,26 @@ module BootstrapHelper
|
|||
|
||||
unless options[:badge].nil? || (options[:badge]).zero?
|
||||
# TODO: make this prettier?
|
||||
body << " #{
|
||||
content_tag(:span, options[:badge], class: "badge#{
|
||||
badge = content_tag(:span, options[:badge], class: "badge#{
|
||||
" badge-#{options[:badge_color]}" unless options[:badge_color].nil?
|
||||
}")}"
|
||||
}",)
|
||||
end
|
||||
|
||||
content_tag(:a, body.html_safe, href: path, class: classes)
|
||||
html = if badge
|
||||
"#{body} #{badge}"
|
||||
else
|
||||
body
|
||||
end
|
||||
|
||||
content_tag(:a, html.html_safe, href: path, class: classes) # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
|
||||
def tooltip(body, tooltip_content, placement = "bottom")
|
||||
content_tag(:span, body, { :title => tooltip_content, "data-bs-toggle" => "tooltip", "data-bs-placement" => placement })
|
||||
content_tag(:span, body, { :title => tooltip_content, "data-controller" => "tooltip", "data-bs-placement" => placement })
|
||||
end
|
||||
|
||||
def time_tooltip(subject, placement = "bottom")
|
||||
tooltip time_ago_in_words(subject.created_at), localize(subject.created_at), placement
|
||||
end
|
||||
|
||||
def hidespan(body, hide)
|
||||
content_tag(:span, body, class: hide)
|
||||
tooltip time_ago_in_words(subject.created_at, scope: "datetime.distance_in_words.short"), localize(subject.created_at, format: :long), placement
|
||||
end
|
||||
|
||||
##
|
||||
|
|
|
@ -8,7 +8,7 @@ module FeedbackHelper
|
|||
avatarURL: current_user.profile_picture.url(:large),
|
||||
name: current_user.screen_name,
|
||||
id: current_user.id,
|
||||
email: current_user.email
|
||||
email: current_user.email,
|
||||
}
|
||||
|
||||
JWT.encode(user_data, APP_CONFIG.dig("canny", "sso"))
|
||||
|
|
|
@ -30,7 +30,7 @@ module MarkdownHelper
|
|||
def raw_markdown(content)
|
||||
renderer = Redcarpet::Render::HTML.new(**MARKDOWN_RENDERER_OPTS)
|
||||
md = Redcarpet::Markdown.new(renderer, **MARKDOWN_OPTS)
|
||||
raw md.render content
|
||||
raw md.render content # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
|
||||
def get_markdown(path, relative_to = Rails.root)
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
# 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
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# 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
|
|
@ -15,7 +15,7 @@ module SocialHelper::TelegramMethods
|
|||
id: answer.id,
|
||||
username: answer.user.screen_name,
|
||||
host: APP_CONFIG["hostname"],
|
||||
protocol: (APP_CONFIG["https"] ? :https : :http)
|
||||
protocol: (APP_CONFIG["https"] ? :https : :http),
|
||||
)
|
||||
|
||||
%(https://t.me/share/url?url=#{CGI.escape(url)}&text=#{CGI.escape(telegram_text(answer))})
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
require 'cgi'
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "cgi"
|
||||
|
||||
module SocialHelper::TumblrMethods
|
||||
def tumblr_title(answer)
|
||||
|
@ -13,10 +15,10 @@ module SocialHelper::TumblrMethods
|
|||
|
||||
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)
|
||||
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})"
|
||||
|
@ -24,10 +26,10 @@ module SocialHelper::TumblrMethods
|
|||
|
||||
def tumblr_share_url(answer)
|
||||
answer_url = answer_url(
|
||||
id: answer.id,
|
||||
id: answer.id,
|
||||
username: answer.user.screen_name,
|
||||
host: APP_CONFIG['hostname'],
|
||||
protocol: (APP_CONFIG['https'] ? :https : :http)
|
||||
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))}"
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
require 'cgi'
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "cgi"
|
||||
|
||||
module SocialHelper::TwitterMethods
|
||||
include MarkdownHelper
|
||||
|
||||
def prepare_tweet(answer, post_tag = nil)
|
||||
question_content = twitter_markdown answer.question.content.gsub(/\@(\w+)/, '\1')
|
||||
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
|
||||
answer_url = answer_url(
|
||||
id: answer.id,
|
||||
username: answer.user.screen_name,
|
||||
host: APP_CONFIG['hostname'],
|
||||
protocol: (APP_CONFIG['https'] ? :https : :http)
|
||||
)
|
||||
|
||||
parsed_tweet = { :valid => false }
|
||||
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]
|
||||
|
@ -23,14 +28,14 @@ module SocialHelper::TwitterMethods
|
|||
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(' ')
|
||||
tweet_text = components.compact.join(" ")
|
||||
|
||||
parsed_tweet = Twitter::TwitterText::Validation::parse_tweet(tweet_text)
|
||||
parsed_tweet = Twitter::TwitterText::Validation.parse_tweet(tweet_text)
|
||||
|
||||
question_content = question_content[0..-2]
|
||||
answer_content = answer_content[0..-2]
|
||||
|
|
|
@ -25,7 +25,7 @@ module ThemeHelper
|
|||
"input_color" => "input-bg",
|
||||
"input_text" => "input-text",
|
||||
"input_placeholder" => "input-placeholder",
|
||||
"muted_text" => "muted-text"
|
||||
"muted_text" => "muted-text",
|
||||
}.freeze
|
||||
|
||||
def render_theme
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
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,12 +1,15 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['twitter', 'tumblr', 'telegram', 'custom'];
|
||||
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 = {
|
||||
|
@ -20,8 +23,11 @@ export default class extends Controller {
|
|||
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());
|
||||
|
@ -37,6 +43,7 @@ export default class extends Controller {
|
|||
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'];
|
||||
|
||||
|
@ -48,4 +55,12 @@ export default class extends Controller {
|
|||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { Tooltip } from 'bootstrap';
|
||||
|
||||
export default class extends Controller {
|
||||
connect(): void {
|
||||
new Tooltip(this.element);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
export function commentHotkeyHandler(event: Event): void {
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const id = button.dataset.aId;
|
||||
const answerbox = button.closest('.answerbox');
|
||||
|
||||
document.querySelector(`#ab-comments-section-${id}`).classList.remove('d-none');
|
||||
document.querySelector<HTMLElement>(`[name="ab-comment-new"][data-a-id="${id}"]`).focus();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import registerEvents from "retrospring/utilities/registerEvents";
|
|||
import { commentDestroyHandler } from "./destroy";
|
||||
import { commentComposeEnd, commentComposeStart, commentCreateClickHandler, commentCreateKeyboardHandler } from "./new";
|
||||
import { commentReportHandler } from "./report";
|
||||
import { commentSmileHandler } from "./smile";
|
||||
import { commentToggleHandler } from "./toggle";
|
||||
import { commentHotkeyHandler } from "retrospring/features/answerbox/comment/hotkey";
|
||||
|
||||
|
@ -10,7 +9,6 @@ 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: '[name=ab-smile-comment]', handler: commentSmileHandler, 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 },
|
||||
|
|
|
@ -31,10 +31,6 @@ function createComment(input: HTMLInputElement, id: string, counter: Element, gr
|
|||
}
|
||||
input.value = '';
|
||||
counter.innerHTML = String(512);
|
||||
|
||||
const sub = document.querySelector<HTMLElement>(`[data-action=ab-submarine][data-a-id="${id}"]`);
|
||||
sub.dataset.torpedo = "no"
|
||||
sub.children[0].nextSibling.textContent = ' ' + I18n.translate('voc.unsubscribe');
|
||||
}
|
||||
|
||||
showNotification(data.message, data.success);
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import { post } from '@rails/request.js';
|
||||
|
||||
import I18n from 'retrospring/i18n';
|
||||
import { showNotification, showErrorNotification } from 'utilities/notifications';
|
||||
|
||||
export function commentSmileHandler(event: Event): void {
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const id = button.dataset.cId;
|
||||
const action = button.dataset.action;
|
||||
let count = Number(document.querySelector(`#ab-comment-smile-count-${id}`).innerHTML);
|
||||
let success = false;
|
||||
let targetUrl;
|
||||
|
||||
if (action === 'smile') {
|
||||
count++;
|
||||
targetUrl = '/ajax/create_comment_smile';
|
||||
}
|
||||
else if (action === 'unsmile') {
|
||||
count--;
|
||||
targetUrl = '/ajax/destroy_comment_smile';
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
post(targetUrl, {
|
||||
body: {
|
||||
id: id
|
||||
},
|
||||
contentType: 'application/json'
|
||||
})
|
||||
.then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
success = data.success;
|
||||
if (success) {
|
||||
document.querySelector(`#ab-comment-smile-count-${id}`).innerHTML = String(count);
|
||||
}
|
||||
|
||||
showNotification(data.message, data.success);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||
})
|
||||
.finally(() => {
|
||||
button.disabled = false;
|
||||
|
||||
if (success) {
|
||||
switch(action) {
|
||||
case 'smile':
|
||||
button.dataset.action = 'unsmile';
|
||||
break;
|
||||
case 'unsmile':
|
||||
button.dataset.action = 'smile';
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
export function commentToggleHandler(event: Event): void {
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const id = button.dataset.aId;
|
||||
const answerbox = button.closest('.answerbox');
|
||||
|
||||
document.querySelector(`#ab-comments-section-${id}`).classList.toggle('d-none');
|
||||
if (answerbox !== null) {
|
||||
answerbox.querySelector(`#ab-comments-section-${id}`).classList.toggle('d-none');
|
||||
}
|
||||
}
|
|
@ -2,17 +2,11 @@ import registerEvents from 'utilities/registerEvents';
|
|||
import registerAnswerboxCommentEvents from './comment';
|
||||
import { answerboxDestroyHandler } from './destroy';
|
||||
import { answerboxReportHandler } from './report';
|
||||
import { shareEventHandler } from './share';
|
||||
import { answerboxSmileHandler } from './smile';
|
||||
import { answerboxSubscribeHandler } from './subscribe';
|
||||
|
||||
export default (): void => {
|
||||
registerEvents([
|
||||
{ type: 'click', target: '[name=ab-share]', handler: shareEventHandler, global: true },
|
||||
{ type: 'click', target: '[data-action=ab-submarine]', handler: answerboxSubscribeHandler, global: true },
|
||||
{ type: 'click', target: '[data-action=ab-report]', handler: answerboxReportHandler, global: true },
|
||||
{ type: 'click', target: '[data-action=ab-destroy]', handler: answerboxDestroyHandler, global: true },
|
||||
{ type: 'click', target: '[name=ab-smile]', handler: answerboxSmileHandler, global: true }
|
||||
]);
|
||||
|
||||
registerAnswerboxCommentEvents();
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import noop from 'utilities/noop';
|
||||
|
||||
export function shareEventHandler(event: Event): void {
|
||||
event.preventDefault();
|
||||
const answerbox = (event.target as HTMLElement).closest('.answerbox');
|
||||
|
||||
navigator.share({
|
||||
url: answerbox.querySelector<HTMLAnchorElement>('.answerbox__answer-date > a, a.answerbox__permalink').href
|
||||
})
|
||||
.then(noop)
|
||||
.catch(noop)
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
import { post } from '@rails/request.js';
|
||||
|
||||
import I18n from 'retrospring/i18n';
|
||||
import { showNotification, showErrorNotification } from 'utilities/notifications';
|
||||
|
||||
export function answerboxSmileHandler(event: Event): void {
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const id = button.dataset.aId;
|
||||
const action = button.dataset.action;
|
||||
let count = Number(document.querySelector(`#ab-smile-count-${id}`).innerHTML);
|
||||
let success = false;
|
||||
let targetUrl;
|
||||
|
||||
if (action === 'smile') {
|
||||
count++;
|
||||
targetUrl = '/ajax/create_smile';
|
||||
}
|
||||
else if (action === 'unsmile') {
|
||||
count--;
|
||||
targetUrl = '/ajax/destroy_smile';
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
post(targetUrl, {
|
||||
body: {
|
||||
id: id
|
||||
},
|
||||
contentType: 'application/json'
|
||||
})
|
||||
.then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
success = data.success;
|
||||
if (success) {
|
||||
document.querySelector(`#ab-smile-count-${id}`).innerHTML = String(count);
|
||||
}
|
||||
|
||||
showNotification(data.message, data.success);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||
})
|
||||
.finally(() => {
|
||||
button.disabled = false;
|
||||
|
||||
if (success) {
|
||||
switch(action) {
|
||||
case 'smile':
|
||||
button.dataset.action = 'unsmile';
|
||||
break;
|
||||
case 'unsmile':
|
||||
button.dataset.action = 'smile';
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import { post } from '@rails/request.js';
|
||||
|
||||
import I18n from 'retrospring/i18n';
|
||||
import { showNotification, showErrorNotification } from 'utilities/notifications';
|
||||
|
||||
export function answerboxSubscribeHandler(event: Event): void {
|
||||
const button = event.target as HTMLButtonElement;
|
||||
const id = button.dataset.aId;
|
||||
let torpedo = 0;
|
||||
let targetUrl;
|
||||
event.preventDefault();
|
||||
|
||||
if (button.dataset.torpedo === 'yes') {
|
||||
torpedo = 1;
|
||||
}
|
||||
|
||||
if (torpedo) {
|
||||
targetUrl = '/ajax/subscribe';
|
||||
} else {
|
||||
targetUrl = '/ajax/unsubscribe';
|
||||
}
|
||||
|
||||
post(targetUrl, {
|
||||
body: {
|
||||
answer: id
|
||||
},
|
||||
contentType: 'application/json'
|
||||
})
|
||||
.then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
if (data.success) {
|
||||
button.dataset.torpedo = ["yes", "no"][torpedo];
|
||||
button.children[0].nextSibling.textContent = ' ' + (torpedo ? I18n.translate('voc.unsubscribe') : I18n.translate('voc.subscribe'));
|
||||
showNotification(I18n.translate(`frontend.subscription.${torpedo ? 'subscribe' : 'unsubscribe'}`));
|
||||
} else {
|
||||
showErrorNotification(I18n.translate(`frontend.subscription.fail.${torpedo ? 'subscribe' : 'unsubscribe'}`));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||
});
|
||||
}
|
|
@ -30,6 +30,17 @@ export function answerEntryHandler(event: Event): void {
|
|||
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);
|
||||
|
|
|
@ -7,7 +7,6 @@ import { showNotification, showErrorNotification } from 'utilities/notifications
|
|||
|
||||
export function deleteEntryHandler(event: Event): void {
|
||||
const element: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||
element.disabled = true;
|
||||
|
||||
const data = {
|
||||
id: element.getAttribute('data-ib-id')
|
||||
|
@ -22,11 +21,8 @@ export function deleteEntryHandler(event: Event): void {
|
|||
confirmButtonText: I18n.translate('voc.delete'),
|
||||
cancelButtonText: I18n.translate('voc.cancel'),
|
||||
closeOnConfirm: true
|
||||
}, (returnValue) => {
|
||||
if (returnValue === false) {
|
||||
element.disabled = false;
|
||||
return;
|
||||
}
|
||||
}, () => {
|
||||
element.disabled = true;
|
||||
|
||||
post('/ajax/delete_inbox', {
|
||||
body: data,
|
||||
|
@ -44,6 +40,7 @@ export function deleteEntryHandler(event: Event): void {
|
|||
(inboxEntry as HTMLElement).remove();
|
||||
})
|
||||
.catch(err => {
|
||||
element.disabled = false;
|
||||
console.log(err);
|
||||
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ export default (): void => {
|
|||
cheet('up up down down left right left right b a', () => {
|
||||
document.body.classList.add('fa-spin');
|
||||
|
||||
Array.from(document.querySelectorAll('.answerbox__question-text')).forEach((element: HTMLElement) => {
|
||||
Array.from(document.querySelectorAll('.question__text')).forEach((element: HTMLElement) => {
|
||||
element.innerText = ':^)';
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,8 @@ export function questionboxAllHandler(event: Event): void {
|
|||
body: {
|
||||
rcpt: 'followers',
|
||||
question: document.querySelector<HTMLInputElement>('textarea[name=qb-all-question]').value,
|
||||
anonymousQuestion: 'false'
|
||||
anonymousQuestion: 'false',
|
||||
sendToOwnInbox: (document.getElementById('qb-send-to-own-inbox') as HTMLInputElement).checked,
|
||||
},
|
||||
contentType: 'application/json'
|
||||
})
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue