Merge remote-tracking branch 'origin/main'

This commit is contained in:
Kay Faraday 2024-07-20 15:15:27 -07:00
commit 1aca0560f6
371 changed files with 4788 additions and 3184 deletions

View File

@ -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 \

2
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,2 @@
71894d6c4987547533606258447b576ecb604c2b
1532741485af266f7ff04a1d6529abe9807a6815

View File

@ -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

View File

@ -18,6 +18,8 @@ updates:
schedule:
interval: weekly
open-pull-requests-limit: 99
ignore:
- dependency-name: 'carrierwave_backgrounder'
allow:
- dependency-type: direct

5
.github/labeler.yml vendored Normal file
View File

@ -0,0 +1,5 @@
feature:
- head-branch: ['^feature', 'feature']
bugfix:
- head-branch: ['^bugfix', 'bugfix']

17
.github/release.yml vendored Normal file
View File

@ -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

View File

@ -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 }}

View File

@ -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

13
.github/workflows/labeler.yml vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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/**/*

View File

@ -1 +1 @@
3.1.2
3.2.3

View File

@ -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
View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -0,0 +1 @@
// This is a stub so that we don't have to install actiontext

View File

@ -0,0 +1 @@
// This is a stub so that we don't have to install Trix

View File

@ -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;
}

View File

@ -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,24 +48,28 @@
&: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"] {
&.unsmile {
color: var(--success);
&:hover {
color: var(--danger);
}
}
&.dropdown-toggle::after {
margin-left: 0;
}
}
@ -97,9 +99,3 @@
display: inline;
}
}
body:not(.cap-web-share) {
[name="ab-share"] {
display: none;
}
}

View File

@ -12,7 +12,7 @@
}
}
&.answerbox__question-text {
&.question__text {
max-height: 15rem;
@include media-breakpoint-up('sm') {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,5 +1,8 @@
.btn {
--btn-padding-x: 1rem;
--btn-border-radius: 2rem;
color: RGB(var(--body-text));
font-weight: bold;
}
.btn-link:hover {

View File

@ -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;
}

View File

@ -0,0 +1 @@
// This is a stub so that we don't have to install Trix

View File

@ -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

View File

@ -0,0 +1,4 @@
%img{ class: avatar_classes,
alt: alt_text,
src: avatar_image,
loading: :lazy }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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)),

View File

@ -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

View File

@ -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)
@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 { |i| i.remove }
@inbox.each(&:remove)
rescue => e
Sentry.capture_exception(e)
@response[:status] = :err

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)}"
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
if type == "all"
Report.cursored_reports(**cursor_params)
else
Report.cursored_reports_of_type(type, **cursor_params)
def filter_params
params.slice(*ReportFilter::KEYS).permit(*ReportFilter::KEYS)
end
def set_filter_enabled
@filter_enabled = params.slice(*ReportFilter::KEYS)
.reject! { |_, value| value.empty? || value.nil? }
.values
.any?
end
def set_type_options
@type_options = [
[t("voc.all"), ""],
[t("activerecord.models.answer.one"), :answer],
[t("activerecord.models.comment.one"), :comment],
[t("activerecord.models.question.one"), :question],
[t("activerecord.models.user.one"), :user]
]
end
def set_last_reports_visit
current_user.last_reports_visit = DateTime.now
current_user.save
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -21,9 +21,12 @@ class Settings::ExportController < ApplicationController
private
# rubocop:disable Rails/SkipsModelValidations
def mark_notifications_as_read
Notification::DataExported
updated = Notification::DataExported
.where(recipient: current_user, new: true)
.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
.update_all(new: false)
current_user.touch(:notifications_updated_at) if updated.positive?
end
# rubocop:enable Rails/SkipsModelValidations
end

View File

@ -19,6 +19,6 @@ class Settings::PrivacyController < ApplicationController
else
flash[:error] = t(".error")
end
redirect_to settings_privacy_path
render :edit
end
end

View File

@ -14,6 +14,6 @@ class Settings::ProfileController < ApplicationController
flash[:error] = t(".error")
end
redirect_to settings_profile_path
render :edit
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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" }

View File

@ -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

View File

@ -1,2 +1,4 @@
# frozen_string_literal: true
module AjaxHelper
end

View File

@ -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

View File

@ -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

View File

@ -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
##

View File

@ -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"))

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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))})

View File

@ -1,4 +1,6 @@
require 'cgi'
# frozen_string_literal: true
require "cgi"
module SocialHelper::TumblrMethods
def tumblr_title(answer)
@ -15,8 +17,8 @@ module SocialHelper::TumblrMethods
answer_url = answer_url(
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),
)
"#{answer.content}\n\n[Smile or comment on the answer here](#{answer_url})"
@ -26,8 +28,8 @@ module SocialHelper::TumblrMethods
answer_url = answer_url(
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))}"

View File

@ -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
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)
host: APP_CONFIG["hostname"],
protocol: (APP_CONFIG["https"] ? :https : :http),
)
end
parsed_tweet = { :valid => false }
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]

View File

@ -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

View File

@ -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"));
}
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,8 @@
import { Controller } from '@hotwired/stimulus';
import { Tooltip } from 'bootstrap';
export default class extends Controller {
connect(): void {
new Tooltip(this.element);
}
}

View File

@ -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();
}
}

View File

@ -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 },

View File

@ -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);

View File

@ -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;
}
}
});
}

View File

@ -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');
}
}

View File

@ -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();

View File

@ -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)
}

View File

@ -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;
}
}
});
}

View File

@ -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'));
});
}

View File

@ -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);

View File

@ -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'));
});

View File

@ -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 = ':^)';
});
});

View File

@ -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