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
|
USER root
|
||||||
|
|
||||||
ARG UID=1000
|
ARG UID=1000
|
||||||
ARG GID=1000
|
ARG GID=1000
|
||||||
|
ARG NODE_MAJOR=16
|
||||||
|
|
||||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
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 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 \
|
RUN apt-get update -qq \
|
||||||
&& apt-get install -y --no-install-recommends build-essential \
|
&& 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
|
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.
|
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.
|
* 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.
|
* 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
|
* Before submitting a pull-request, clean up the history by going over your
|
||||||
|
|
|
@ -18,6 +18,8 @@ updates:
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
open-pull-requests-limit: 99
|
open-pull-requests-limit: 99
|
||||||
|
ignore:
|
||||||
|
- dependency-name: 'carrierwave_backgrounder'
|
||||||
allow:
|
allow:
|
||||||
- dependency-type: direct
|
- 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
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Discover build-time variables
|
- name: Discover build-time variables
|
||||||
run: |
|
run: |
|
||||||
|
@ -38,7 +38,7 @@ jobs:
|
||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Login to registry
|
- name: Login to registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
@ -46,7 +46,7 @@ jobs:
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
build-args: |
|
build-args: |
|
||||||
BUNDLER_VERSION=${{ env.BUNDLER_VERSION }}
|
BUNDLER_VERSION=${{ env.BUNDLER_VERSION }}
|
||||||
|
|
|
@ -33,11 +33,11 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# 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).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
@ -62,4 +62,4 @@ jobs:
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- 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
|
name: Rubocop
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.7
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v39
|
uses: tj-actions/changed-files@v44
|
||||||
with:
|
with:
|
||||||
files: "**/*.rb"
|
files: "**/*.rb"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
@ -37,16 +37,16 @@ jobs:
|
||||||
name: ESLint
|
name: ESLint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.7
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v39
|
uses: tj-actions/changed-files@v44
|
||||||
with:
|
with:
|
||||||
files: "**/*.ts"
|
files: "**/*.ts"
|
||||||
- name: Set up Node 14
|
- name: Set up Node 16
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '16'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
- name: Install node modules
|
- name: Install node modules
|
||||||
|
@ -63,10 +63,10 @@ jobs:
|
||||||
haml-lint:
|
haml-lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.7
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v39
|
uses: tj-actions/changed-files@v44
|
||||||
with:
|
with:
|
||||||
files: "**/*.haml"
|
files: "**/*.haml"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
@ -86,16 +86,16 @@ jobs:
|
||||||
stylelint:
|
stylelint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.7
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v39
|
uses: tj-actions/changed-files@v44
|
||||||
with:
|
with:
|
||||||
files: "**/*.scss"
|
files: "**/*.scss"
|
||||||
- name: Set up Node 14
|
- name: Set up Node 16
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '16'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
- name: Install node modules
|
- name: Install node modules
|
||||||
|
@ -104,7 +104,7 @@ jobs:
|
||||||
yarn install --frozen-lockfile
|
yarn install --frozen-lockfile
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
- name: stylelint
|
- name: stylelint
|
||||||
uses: reviewdog/action-stylelint@v1.18.1
|
uses: reviewdog/action-stylelint@v1.26.0
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.github_token }}
|
github_token: ${{ secrets.github_token }}
|
||||||
reporter: github-pr-check
|
reporter: github-pr-check
|
||||||
|
|
|
@ -41,17 +41,17 @@ jobs:
|
||||||
BUNDLE_WITHOUT: 'production'
|
BUNDLE_WITHOUT: 'production'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.7
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt-get install -y libpq-dev libxml2-dev libxslt1-dev libmagickwand-dev imagemagick libidn11-dev
|
run: sudo apt update && sudo apt-get install -y libpq-dev libxml2-dev libxslt1-dev libmagickwand-dev imagemagick libidn11-dev
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
- name: Set up Node 14
|
- name: Set up Node 16
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '16'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- name: Copy default configuration
|
- name: Copy default configuration
|
||||||
run: |
|
run: |
|
||||||
|
@ -75,7 +75,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
|
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
|
||||||
REDIS_URL: "redis://localhost:${{ job.services.redis.ports[6379] }}"
|
REDIS_URL: "redis://localhost:${{ job.services.redis.ports[6379] }}"
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
file: ./coverage/coverage.xml
|
file: ./coverage/coverage.xml
|
||||||
|
|
37
.rubocop.yml
37
.rubocop.yml
|
@ -27,43 +27,36 @@ Lint/NestedMethodDefinition:
|
||||||
Exclude:
|
Exclude:
|
||||||
- api/sinatra/**/*
|
- api/sinatra/**/*
|
||||||
|
|
||||||
|
Lint/MissingSuper:
|
||||||
|
Exclude:
|
||||||
|
- app/components/**/*
|
||||||
|
|
||||||
|
|
||||||
### Metrics
|
### Metrics
|
||||||
|
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 20
|
Enabled: false
|
||||||
Exclude:
|
|
||||||
- 'db/**/*'
|
|
||||||
|
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 15
|
Enabled: false
|
||||||
Exclude:
|
|
||||||
- 'db/migrate/*.rb'
|
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Exclude:
|
Enabled: false
|
||||||
- '*.gemspec'
|
|
||||||
- '**/*.rake'
|
|
||||||
- 'api/**/*'
|
|
||||||
- 'app/api/routes.rb'
|
|
||||||
- 'config/initialize/**/*'
|
|
||||||
- 'config/initializers/**/*'
|
|
||||||
- 'spec/**/*'
|
|
||||||
|
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Exclude:
|
Enabled: false
|
||||||
- spec/**/*
|
|
||||||
|
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Severity: refactor
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/PerceivedComplexity:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Exclude:
|
Enabled: false
|
||||||
- 'app/api/routes.rb'
|
|
||||||
- 'spec/requests/**/*'
|
|
||||||
|
|
||||||
|
|
||||||
### Style / Layout
|
### Style / Layout
|
||||||
|
@ -137,3 +130,7 @@ Style/TrailingCommaInHashLiteral:
|
||||||
|
|
||||||
Style/TrailingCommaInArguments:
|
Style/TrailingCommaInArguments:
|
||||||
EnforcedStyleForMultiline: consistent_comma
|
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
|
# 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.title="Retrospring (production)"
|
||||||
LABEL org.opencontainers.image.description="Image containing everything to run Retrospring in production mode. Do not use this for development."
|
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"
|
LABEL org.opencontainers.image.url="https://github.com/Retrospring/retrospring"
|
||||||
|
|
||||||
ARG RETROSPRING_VERSION=2023.0131.1
|
ARG RETROSPRING_VERSION=2023.0131.1
|
||||||
ARG RUBY_VERSION=3.1.2
|
ARG RUBY_VERSION=3.2.3
|
||||||
ARG RUBY_INSTALL_VERSION=0.9.0
|
ARG RUBY_INSTALL_VERSION=0.9.3
|
||||||
ARG BUNDLER_VERSION=2.3.18
|
ARG BUNDLER_VERSION=2.5.5
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
|
|
||||||
# update and install dependencies
|
# 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 \
|
&& zypper in -y \
|
||||||
# build dependencies (ruby-install)
|
# build dependencies (ruby-install)
|
||||||
automake \
|
automake \
|
||||||
|
@ -25,18 +26,20 @@ RUN zypper up -y \
|
||||||
libffi-devel \
|
libffi-devel \
|
||||||
libopenssl-devel \
|
libopenssl-devel \
|
||||||
libyaml-devel \
|
libyaml-devel \
|
||||||
|
jemalloc-devel \
|
||||||
make \
|
make \
|
||||||
ncurses-devel \
|
ncurses-devel \
|
||||||
readline-devel \
|
readline-devel \
|
||||||
tar \
|
tar \
|
||||||
xz \
|
xz \
|
||||||
zlib-devel \
|
zlib-devel \
|
||||||
|
curl \
|
||||||
# build dependencies (app)
|
# build dependencies (app)
|
||||||
gcc-c++ \
|
gcc-c++ \
|
||||||
git \
|
git \
|
||||||
libidn-devel \
|
libidn-devel \
|
||||||
nodejs14 \
|
nodejs16 \
|
||||||
npm14 \
|
npm16 \
|
||||||
postgresql-devel \
|
postgresql-devel \
|
||||||
# runtime dependencies
|
# runtime dependencies
|
||||||
ImageMagick \
|
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 \
|
&& tar xvf ruby-install-${RUBY_INSTALL_VERSION}.tar.gz \
|
||||||
&& (cd ruby-install-${RUBY_INSTALL_VERSION} && make install) \
|
&& (cd ruby-install-${RUBY_INSTALL_VERSION} && make install) \
|
||||||
&& rm -rf ruby-install-${RUBY_INSTALL_VERSION} ruby-install-${RUBY_INSTALL_VERSION}.tar.gz \
|
&& 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}
|
&& gem install bundler:${BUNDLER_VERSION}
|
||||||
|
|
||||||
# create user and dirs to run retrospring in
|
# create user and dirs to run retrospring in
|
||||||
|
|
37
Gemfile
37
Gemfile
|
@ -3,11 +3,11 @@
|
||||||
source "https://rubygems.org"
|
source "https://rubygems.org"
|
||||||
|
|
||||||
gem "i18n-js", "4.0"
|
gem "i18n-js", "4.0"
|
||||||
gem "rails", "~> 6.1"
|
gem "rails", "~> 7.0.8"
|
||||||
gem "rails-i18n", "~> 7.0"
|
gem "rails-i18n", "~> 7.0"
|
||||||
|
|
||||||
gem "cssbundling-rails", "~> 1.2"
|
gem "cssbundling-rails", "~> 1.4"
|
||||||
gem "jsbundling-rails", "~> 1.1"
|
gem "jsbundling-rails", "~> 1.3"
|
||||||
gem "sassc-rails"
|
gem "sassc-rails"
|
||||||
gem "sprockets", "~> 4.2"
|
gem "sprockets", "~> 4.2"
|
||||||
gem "sprockets-rails", require: "sprockets/railtie"
|
gem "sprockets-rails", require: "sprockets/railtie"
|
||||||
|
@ -16,13 +16,13 @@ gem "pg"
|
||||||
|
|
||||||
gem "turbo-rails"
|
gem "turbo-rails"
|
||||||
|
|
||||||
gem "bcrypt", "~> 3.1.19"
|
gem "bcrypt", "~> 3.1.20"
|
||||||
|
|
||||||
gem "active_model_otp"
|
gem "active_model_otp"
|
||||||
gem "bootsnap", require: false
|
gem "bootsnap", require: false
|
||||||
gem "bootstrap_form", "~> 5.0"
|
gem "bootstrap_form", "~> 5.0"
|
||||||
gem "carrierwave", "~> 2.0"
|
gem "carrierwave", "~> 2.1"
|
||||||
gem "carrierwave_backgrounder", git: "https://github.com/raccube/carrierwave_backgrounder.git"
|
gem "carrierwave_backgrounder", "~> 0.4.2"
|
||||||
gem "colorize"
|
gem "colorize"
|
||||||
gem "devise", "~> 4.9"
|
gem "devise", "~> 4.9"
|
||||||
gem "devise-async"
|
gem "devise-async"
|
||||||
|
@ -30,12 +30,13 @@ gem "devise-i18n"
|
||||||
gem "fog-aws"
|
gem "fog-aws"
|
||||||
gem "fog-core"
|
gem "fog-core"
|
||||||
gem "fog-local"
|
gem "fog-local"
|
||||||
gem "haml", "~> 6.1"
|
gem "haml", "~> 6.3"
|
||||||
gem "hcaptcha", "~> 7.0"
|
gem "hcaptcha", git: "https://github.com/retrospring/hcaptcha", ref: "fix/flash-in-turbo-streams"
|
||||||
gem "mini_magick"
|
gem "mini_magick"
|
||||||
gem "oj"
|
gem "oj"
|
||||||
gem "rpush"
|
gem "rpush"
|
||||||
gem "rqrcode"
|
gem "rqrcode"
|
||||||
|
gem "web-push"
|
||||||
|
|
||||||
gem "rolify", "~> 6.0"
|
gem "rolify", "~> 6.0"
|
||||||
|
|
||||||
|
@ -49,6 +50,7 @@ gem "sentry-ruby"
|
||||||
gem "sentry-sidekiq"
|
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", "< 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'
|
gem "questiongenerator", "~> 1.2", git: 'https://lab.freak.university/FreakU/questiongenerator'
|
||||||
|
|
||||||
|
@ -66,11 +68,12 @@ gem "fake_email_validator"
|
||||||
# TLD validation
|
# TLD validation
|
||||||
gem "tldv", "~> 0.1.0"
|
gem "tldv", "~> 0.1.0"
|
||||||
|
|
||||||
gem "jwt", "~> 2.7"
|
gem "view_component"
|
||||||
|
|
||||||
|
gem "jwt", "~> 2.8"
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem "binding_of_caller"
|
gem "binding_of_caller"
|
||||||
gem "spring", "~> 4.1"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
gem "puma"
|
gem "puma"
|
||||||
|
@ -79,7 +82,7 @@ group :development, :test do
|
||||||
gem "better_errors"
|
gem "better_errors"
|
||||||
gem "bullet"
|
gem "bullet"
|
||||||
gem "database_cleaner"
|
gem "database_cleaner"
|
||||||
gem "dotenv-rails", "~> 2.8"
|
gem "dotenv-rails", "~> 3.1"
|
||||||
gem "factory_bot_rails", require: false
|
gem "factory_bot_rails", require: false
|
||||||
gem "faker"
|
gem "faker"
|
||||||
gem "haml_lint", require: false
|
gem "haml_lint", require: false
|
||||||
|
@ -89,11 +92,11 @@ group :development, :test do
|
||||||
gem "rake"
|
gem "rake"
|
||||||
gem "rspec-its", "~> 1.3"
|
gem "rspec-its", "~> 1.3"
|
||||||
gem "rspec-mocks"
|
gem "rspec-mocks"
|
||||||
gem "rspec-rails", "~> 6.0"
|
gem "rspec-rails", "~> 6.1"
|
||||||
gem "rspec-sidekiq", "~> 4.0", require: false
|
gem "rspec-sidekiq", "~> 5.0", require: false
|
||||||
gem "rubocop", "~> 1.56"
|
gem "rubocop", "~> 1.64"
|
||||||
gem "rubocop-rails", "~> 2.21"
|
gem "rubocop-rails", "~> 2.25"
|
||||||
gem "shoulda-matchers", "~> 5.3"
|
gem "shoulda-matchers", "~> 6.2"
|
||||||
gem "simplecov", require: false
|
gem "simplecov", require: false
|
||||||
gem "simplecov-cobertura", require: false
|
gem "simplecov-cobertura", require: false
|
||||||
gem "simplecov-json", require: false
|
gem "simplecov-json", require: false
|
||||||
|
@ -112,7 +115,7 @@ gem "pundit", "~> 2.3"
|
||||||
gem "rubyzip", "~> 2.3"
|
gem "rubyzip", "~> 2.3"
|
||||||
|
|
||||||
# to solve https://github.com/jwt/ruby-jwt/issues/526
|
# 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
|
# mail 2.8.0 breaks sendmail usage: https://github.com/mikel/mail/issues/1538
|
||||||
gem "mail", "~> 2.7.1"
|
gem "mail", "~> 2.7.1"
|
||||||
|
|
471
Gemfile.lock
471
Gemfile.lock
|
@ -1,10 +1,10 @@
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/raccube/carrierwave_backgrounder.git
|
remote: https://github.com/retrospring/hcaptcha
|
||||||
revision: 41b756f7514c0e410c561bc8b5ee321cd8cce1ee
|
revision: f8de70ee2d629ac34395902dbee724c21297960c
|
||||||
|
ref: fix/flash-in-turbo-streams
|
||||||
specs:
|
specs:
|
||||||
carrierwave_backgrounder (0.4.2)
|
hcaptcha (7.1.0)
|
||||||
carrierwave (>= 0.5, <= 2.1)
|
json
|
||||||
mime-types (>= 3.0.0)
|
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: https://lab.freak.university/FreakU/questiongenerator
|
remote: https://lab.freak.university/FreakU/questiongenerator
|
||||||
|
@ -15,115 +15,127 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.7.6)
|
actioncable (7.0.8.4)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 7.0.8.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.7.6)
|
actionmailbox (7.0.8.4)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 7.0.8.4)
|
||||||
activejob (= 6.1.7.6)
|
activejob (= 7.0.8.4)
|
||||||
activerecord (= 6.1.7.6)
|
activerecord (= 7.0.8.4)
|
||||||
activestorage (= 6.1.7.6)
|
activestorage (= 7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 7.0.8.4)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.7.6)
|
net-imap
|
||||||
actionpack (= 6.1.7.6)
|
net-pop
|
||||||
actionview (= 6.1.7.6)
|
net-smtp
|
||||||
activejob (= 6.1.7.6)
|
actionmailer (7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
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)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
|
net-imap
|
||||||
|
net-pop
|
||||||
|
net-smtp
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.7.6)
|
actionpack (7.0.8.4)
|
||||||
actionview (= 6.1.7.6)
|
actionview (= 7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 7.0.8.4)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.2.4)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.7.6)
|
actiontext (7.0.8.4)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 7.0.8.4)
|
||||||
activerecord (= 6.1.7.6)
|
activerecord (= 7.0.8.4)
|
||||||
activestorage (= 6.1.7.6)
|
activestorage (= 7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 7.0.8.4)
|
||||||
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.7.6)
|
actionview (7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 7.0.8.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
active_model_otp (2.3.2)
|
active_model_otp (2.3.4)
|
||||||
activemodel
|
activemodel
|
||||||
rotp (~> 6.2.0)
|
rotp (~> 6.3.0)
|
||||||
activejob (6.1.7.6)
|
activejob (7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 7.0.8.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.7.6)
|
activemodel (7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 7.0.8.4)
|
||||||
activemodel-serializers-xml (1.0.2)
|
activemodel-serializers-xml (1.0.2)
|
||||||
activemodel (> 5.x)
|
activemodel (> 5.x)
|
||||||
activesupport (> 5.x)
|
activesupport (> 5.x)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
activerecord (6.1.7.6)
|
activerecord (7.0.8.4)
|
||||||
activemodel (= 6.1.7.6)
|
activemodel (= 7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 7.0.8.4)
|
||||||
activestorage (6.1.7.6)
|
activestorage (7.0.8.4)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 7.0.8.4)
|
||||||
activejob (= 6.1.7.6)
|
activejob (= 7.0.8.4)
|
||||||
activerecord (= 6.1.7.6)
|
activerecord (= 7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 7.0.8.4)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.7.6)
|
activesupport (7.0.8.4)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
zeitwerk (~> 2.3)
|
addressable (2.8.6)
|
||||||
addressable (2.8.4)
|
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
base64 (0.1.1)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.19)
|
bcrypt (3.1.20)
|
||||||
better_errors (2.10.1)
|
better_errors (2.10.1)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
rouge (>= 1.0.0)
|
rouge (>= 1.0.0)
|
||||||
binding_of_caller (1.0.0)
|
bigdecimal (3.1.8)
|
||||||
debug_inspector (>= 0.0.1)
|
binding_of_caller (1.0.1)
|
||||||
bootsnap (1.16.0)
|
debug_inspector (>= 1.2.0)
|
||||||
|
bootsnap (1.18.3)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
bootstrap_form (5.1.0)
|
bootstrap_form (5.3.2)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 6.1)
|
||||||
activemodel (>= 5.2)
|
activemodel (>= 6.1)
|
||||||
builder (3.2.4)
|
builder (3.3.0)
|
||||||
bullet (7.0.7)
|
bullet (7.1.6)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
carrierwave (2.1.0)
|
carrierwave (2.1.1)
|
||||||
activemodel (>= 5.0.0)
|
activemodel (>= 5.0.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
addressable (~> 2.6)
|
addressable (~> 2.6)
|
||||||
image_processing (~> 1.1)
|
image_processing (~> 1.1)
|
||||||
mimemagic (>= 0.3.0)
|
mimemagic (>= 0.3.0)
|
||||||
mini_mime (>= 0.1.3)
|
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)
|
chunky_png (1.4.0)
|
||||||
colorize (1.1.0)
|
colorize (1.1.0)
|
||||||
concurrent-ruby (1.2.2)
|
concurrent-ruby (1.3.3)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.4.1)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
cssbundling-rails (1.2.0)
|
cssbundling-rails (1.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
|
csv (3.3.0)
|
||||||
database_cleaner (2.0.2)
|
database_cleaner (2.0.2)
|
||||||
database_cleaner-active_record (>= 2, < 3)
|
database_cleaner-active_record (>= 2, < 3)
|
||||||
database_cleaner-active_record (2.1.0)
|
database_cleaner-active_record (2.1.0)
|
||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0.0)
|
database_cleaner-core (~> 2.0.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
date (3.3.3)
|
date (3.3.4)
|
||||||
debug_inspector (1.1.0)
|
debug_inspector (1.2.0)
|
||||||
devise (4.9.2)
|
devise (4.9.4)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
|
@ -132,15 +144,15 @@ GEM
|
||||||
devise-async (1.0.0)
|
devise-async (1.0.0)
|
||||||
activejob (>= 5.0)
|
activejob (>= 5.0)
|
||||||
devise (>= 4.0)
|
devise (>= 4.0)
|
||||||
devise-i18n (1.10.3)
|
devise-i18n (1.12.0)
|
||||||
devise (>= 4.8.0)
|
devise (>= 4.9.0)
|
||||||
diff-lcs (1.5.0)
|
diff-lcs (1.5.1)
|
||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
dotenv (2.8.1)
|
dotenv (3.1.2)
|
||||||
dotenv-rails (2.8.1)
|
dotenv-rails (3.1.2)
|
||||||
dotenv (= 2.8.1)
|
dotenv (= 3.1.2)
|
||||||
railties (>= 3.2)
|
railties (>= 6.1)
|
||||||
dry-core (1.0.0)
|
dry-core (1.0.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
dry-inflector (1.0.0)
|
dry-inflector (1.0.0)
|
||||||
|
@ -149,30 +161,33 @@ GEM
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
dry-core (~> 1.0, < 2)
|
dry-core (~> 1.0, < 2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
dry-types (1.7.1)
|
dry-types (1.7.2)
|
||||||
|
bigdecimal (~> 3.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
dry-core (~> 1.0)
|
dry-core (~> 1.0)
|
||||||
dry-inflector (~> 1.0)
|
dry-inflector (~> 1.0)
|
||||||
dry-logic (~> 1.4)
|
dry-logic (~> 1.4)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
erubi (1.12.0)
|
erubi (1.13.0)
|
||||||
excon (0.99.0)
|
et-orbi (1.2.7)
|
||||||
factory_bot (6.2.0)
|
tzinfo
|
||||||
|
excon (0.110.0)
|
||||||
|
factory_bot (6.4.5)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.2.0)
|
factory_bot_rails (6.4.3)
|
||||||
factory_bot (~> 6.2.0)
|
factory_bot (~> 6.4)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
fake_email_validator (1.0.11)
|
fake_email_validator (1.0.11)
|
||||||
activemodel
|
activemodel
|
||||||
mail
|
mail
|
||||||
faker (3.1.1)
|
faker (3.4.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
ffi (1.15.5)
|
ffi (1.16.3)
|
||||||
fog-aws (3.19.0)
|
fog-aws (3.23.0)
|
||||||
fog-core (~> 2.1)
|
fog-core (~> 2.1)
|
||||||
fog-json (~> 1.1)
|
fog-json (~> 1.1)
|
||||||
fog-xml (~> 0.1)
|
fog-xml (~> 0.1)
|
||||||
fog-core (2.3.0)
|
fog-core (2.4.0)
|
||||||
builder
|
builder
|
||||||
excon (~> 0.71)
|
excon (~> 0.71)
|
||||||
formatador (>= 0.2, < 2.0)
|
formatador (>= 0.2, < 2.0)
|
||||||
|
@ -186,41 +201,44 @@ GEM
|
||||||
fog-core
|
fog-core
|
||||||
nokogiri (>= 1.5.11, < 2.0.0)
|
nokogiri (>= 1.5.11, < 2.0.0)
|
||||||
formatador (1.1.0)
|
formatador (1.1.0)
|
||||||
glob (0.3.1)
|
fugit (1.9.0)
|
||||||
globalid (1.1.0)
|
et-orbi (~> 1, >= 1.2.7)
|
||||||
activesupport (>= 5.0)
|
raabro (~> 1.4)
|
||||||
haml (6.1.2)
|
glob (0.4.0)
|
||||||
|
globalid (1.2.1)
|
||||||
|
activesupport (>= 6.1)
|
||||||
|
haml (6.3.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
haml_lint (0.50.0)
|
haml_lint (0.58.0)
|
||||||
haml (>= 4.0, < 6.2)
|
haml (>= 5.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
rubocop (>= 1.0)
|
rubocop (>= 1.0)
|
||||||
sysexits (~> 1.1)
|
sysexits (~> 1.1)
|
||||||
hcaptcha (7.1.0)
|
|
||||||
json
|
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
http-2 (0.11.0)
|
http-2 (0.11.0)
|
||||||
httparty (0.21.0)
|
httparty (0.22.0)
|
||||||
|
csv
|
||||||
mini_mime (>= 1.0.0)
|
mini_mime (>= 1.0.0)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
i18n (1.14.1)
|
i18n (1.14.5)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-js (4.0.0)
|
i18n-js (4.0.0)
|
||||||
glob
|
glob
|
||||||
i18n
|
i18n
|
||||||
idn-ruby (0.1.4)
|
idn-ruby (0.1.5)
|
||||||
image_processing (1.12.2)
|
image_processing (1.12.2)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
jsbundling-rails (1.1.2)
|
jsbundling-rails (1.3.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
json (2.6.3)
|
json (2.7.2)
|
||||||
json-schema (4.0.0)
|
json-schema (4.3.0)
|
||||||
addressable (>= 2.8)
|
addressable (>= 2.8)
|
||||||
jwt (2.7.1)
|
jwt (2.8.2)
|
||||||
|
base64
|
||||||
kaminari (1.2.2)
|
kaminari (1.2.2)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.2.2)
|
kaminari-actionview (= 1.2.2)
|
||||||
|
@ -234,88 +252,91 @@ GEM
|
||||||
kaminari-core (= 1.2.2)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-core (1.2.2)
|
kaminari-core (1.2.2)
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
launchy (2.5.0)
|
launchy (3.0.0)
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.8)
|
||||||
letter_opener (1.8.1)
|
childprocess (~> 5.0)
|
||||||
launchy (>= 2.2, < 3)
|
letter_opener (1.10.0)
|
||||||
lograge (0.13.0)
|
launchy (>= 2.2, < 4)
|
||||||
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.21.3)
|
loofah (2.22.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
marcel (1.0.2)
|
marcel (1.0.4)
|
||||||
method_source (1.0.0)
|
method_source (1.1.0)
|
||||||
mime-types (3.4.1)
|
mime-types (3.5.2)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2023.0218.1)
|
mime-types-data (3.2024.0604)
|
||||||
mimemagic (0.4.3)
|
mimemagic (0.4.3)
|
||||||
nokogiri (~> 1)
|
nokogiri (~> 1)
|
||||||
rake
|
rake
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.13.1)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.4)
|
mini_portile2 (2.8.7)
|
||||||
minitest (5.20.0)
|
minitest (5.24.0)
|
||||||
msgpack (1.6.0)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.7.1)
|
||||||
|
bigdecimal (~> 3.1)
|
||||||
nested_form (0.3.2)
|
nested_form (0.3.2)
|
||||||
net-http-persistent (4.0.1)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-http2 (0.18.4)
|
net-http2 (0.18.5)
|
||||||
http-2 (~> 0.11)
|
http-2 (~> 0.11)
|
||||||
net-imap (0.3.7)
|
net-imap (0.4.14)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.1)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.3.3)
|
net-smtp (0.5.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.5.9)
|
nio4r (2.7.0)
|
||||||
nokogiri (1.15.4)
|
nokogiri (1.16.6)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.1)
|
oj (3.16.4)
|
||||||
openssl (3.1.0)
|
bigdecimal (>= 3.0)
|
||||||
|
openssl (3.2.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
parallel (1.23.0)
|
parallel (1.24.0)
|
||||||
parser (3.2.2.3)
|
parser (3.3.2.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pg (1.5.4)
|
pg (1.5.6)
|
||||||
pghero (3.3.4)
|
pghero (3.5.0)
|
||||||
activerecord (>= 6)
|
activerecord (>= 6)
|
||||||
prometheus-client (4.2.1)
|
prometheus-client (4.2.2)
|
||||||
public_suffix (5.0.1)
|
public_suffix (5.0.4)
|
||||||
puma (6.3.1)
|
puma (6.4.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.1)
|
pundit (2.3.2)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
racc (1.7.1)
|
raabro (1.4.0)
|
||||||
rack (2.2.8)
|
racc (1.8.0)
|
||||||
|
rack (2.2.9)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (6.1.7.6)
|
rails (7.0.8.4)
|
||||||
actioncable (= 6.1.7.6)
|
actioncable (= 7.0.8.4)
|
||||||
actionmailbox (= 6.1.7.6)
|
actionmailbox (= 7.0.8.4)
|
||||||
actionmailer (= 6.1.7.6)
|
actionmailer (= 7.0.8.4)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 7.0.8.4)
|
||||||
actiontext (= 6.1.7.6)
|
actiontext (= 7.0.8.4)
|
||||||
actionview (= 6.1.7.6)
|
actionview (= 7.0.8.4)
|
||||||
activejob (= 6.1.7.6)
|
activejob (= 7.0.8.4)
|
||||||
activemodel (= 6.1.7.6)
|
activemodel (= 7.0.8.4)
|
||||||
activerecord (= 6.1.7.6)
|
activerecord (= 7.0.8.4)
|
||||||
activestorage (= 6.1.7.6)
|
activestorage (= 7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 7.0.8.4)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.7.6)
|
railties (= 7.0.8.4)
|
||||||
sprockets-rails (>= 2.0.0)
|
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
actionview (>= 5.0.1.rc1)
|
actionview (>= 5.0.1.rc1)
|
||||||
|
@ -327,7 +348,7 @@ GEM
|
||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (~> 1.14)
|
||||||
rails-i18n (7.0.8)
|
rails-i18n (7.0.9)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
rails_admin (3.1.2)
|
rails_admin (3.1.2)
|
||||||
|
@ -336,26 +357,28 @@ GEM
|
||||||
nested_form (~> 0.3)
|
nested_form (~> 0.3)
|
||||||
rails (>= 6.0, < 8)
|
rails (>= 6.0, < 8)
|
||||||
turbo-rails (~> 1.0)
|
turbo-rails (~> 1.0)
|
||||||
railties (6.1.7.6)
|
railties (7.0.8.4)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 7.0.8.4)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 7.0.8.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
|
zeitwerk (~> 2.5)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.2.1)
|
||||||
redcarpet (3.6.0)
|
redcarpet (3.6.0)
|
||||||
redis (4.8.0)
|
redis (4.8.1)
|
||||||
regexp_parser (2.8.1)
|
regexp_parser (2.9.2)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.0)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.2.6)
|
rexml (3.2.8)
|
||||||
|
strscan (>= 3.0.9)
|
||||||
rolify (6.0.1)
|
rolify (6.0.1)
|
||||||
rotp (6.2.2)
|
rotp (6.3.0)
|
||||||
rouge (4.1.2)
|
rouge (4.1.3)
|
||||||
rpush (7.0.1)
|
rpush (7.0.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
jwt (>= 1.5.6)
|
jwt (>= 1.5.6)
|
||||||
|
@ -370,54 +393,56 @@ GEM
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 1.0)
|
rqrcode_core (~> 1.0)
|
||||||
rqrcode_core (1.2.0)
|
rqrcode_core (1.2.0)
|
||||||
rspec-core (3.12.2)
|
rspec-core (3.13.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.12.3)
|
rspec-expectations (3.13.1)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-its (1.3.0)
|
rspec-its (1.3.0)
|
||||||
rspec-core (>= 3.0.0)
|
rspec-core (>= 3.0.0)
|
||||||
rspec-expectations (>= 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)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (6.0.3)
|
rspec-rails (6.1.3)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
rspec-core (~> 3.12)
|
rspec-core (~> 3.13)
|
||||||
rspec-expectations (~> 3.12)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.12)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.12)
|
rspec-support (~> 3.13)
|
||||||
rspec-sidekiq (4.0.2)
|
rspec-sidekiq (5.0.0)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
rspec-expectations (~> 3.0)
|
rspec-expectations (~> 3.0)
|
||||||
rspec-mocks (~> 3.0)
|
rspec-mocks (~> 3.0)
|
||||||
sidekiq (>= 5, < 8)
|
sidekiq (>= 5, < 8)
|
||||||
rspec-support (3.12.1)
|
rspec-support (3.13.1)
|
||||||
rubocop (1.56.3)
|
rubocop (1.64.1)
|
||||||
base64 (~> 0.1.1)
|
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.2.2.3)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.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)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.29.0)
|
rubocop-ast (1.31.3)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-rails (2.21.0)
|
rubocop-rails (2.25.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-vips (2.1.4)
|
ruby-vips (2.2.1)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
rubyzip (2.3.2)
|
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)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
sassc (2.4.0)
|
sassc (2.4.0)
|
||||||
|
@ -428,20 +453,25 @@ GEM
|
||||||
sprockets (> 3.0)
|
sprockets (> 3.0)
|
||||||
sprockets-rails
|
sprockets-rails
|
||||||
tilt
|
tilt
|
||||||
sentry-rails (5.10.0)
|
sentry-rails (5.17.3)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
sentry-ruby (~> 5.10.0)
|
sentry-ruby (~> 5.17.3)
|
||||||
sentry-ruby (5.10.0)
|
sentry-ruby (5.17.3)
|
||||||
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sentry-sidekiq (5.10.0)
|
sentry-sidekiq (5.17.3)
|
||||||
sentry-ruby (~> 5.10.0)
|
sentry-ruby (~> 5.17.3)
|
||||||
sidekiq (>= 3.0)
|
sidekiq (>= 3.0)
|
||||||
shoulda-matchers (5.3.0)
|
shoulda-matchers (6.2.0)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 5.2.0)
|
||||||
sidekiq (6.5.8)
|
sidekiq (6.5.12)
|
||||||
connection_pool (>= 2.2.5, < 3)
|
connection_pool (>= 2.2.5, < 3)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.5.0, < 5)
|
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)
|
simplecov (0.22.0)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
simplecov-html (~> 0.11)
|
simplecov-html (~> 0.11)
|
||||||
|
@ -454,23 +484,24 @@ GEM
|
||||||
json
|
json
|
||||||
simplecov
|
simplecov
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
spring (4.1.1)
|
|
||||||
sprockets (4.2.1)
|
sprockets (4.2.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (>= 2.2.4, < 4)
|
rack (>= 2.2.4, < 4)
|
||||||
sprockets-rails (3.4.2)
|
sprockets-rails (3.5.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 6.1)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
|
ssrf_filter (1.1.2)
|
||||||
|
strscan (3.1.0)
|
||||||
sysexits (1.2.0)
|
sysexits (1.2.0)
|
||||||
temple (0.10.2)
|
temple (0.10.3)
|
||||||
thor (1.2.2)
|
thor (1.3.1)
|
||||||
tilt (2.2.0)
|
tilt (2.3.0)
|
||||||
timeout (0.4.0)
|
timeout (0.4.1)
|
||||||
tldv (0.1.0)
|
tldv (0.1.0)
|
||||||
tldv-data (~> 1.0)
|
tldv-data (~> 1.0)
|
||||||
tldv-data (1.0.2023031000)
|
tldv-data (1.0.2023080900)
|
||||||
turbo-rails (1.4.0)
|
turbo-rails (1.5.0)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
activejob (>= 6.0.0)
|
activejob (>= 6.0.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
|
@ -481,40 +512,47 @@ GEM
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.8)
|
unf_ext (0.0.8.2)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.5.0)
|
||||||
uniform_notifier (1.16.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)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
|
web-push (3.0.1)
|
||||||
|
jwt (~> 2.0)
|
||||||
|
openssl (~> 3.0)
|
||||||
webpush (1.1.0)
|
webpush (1.1.0)
|
||||||
hkdf (~> 0.2)
|
hkdf (~> 0.2)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
websocket-driver (0.7.6)
|
websocket-driver (0.7.6)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
zeitwerk (2.6.11)
|
zeitwerk (2.6.16)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
active_model_otp
|
active_model_otp
|
||||||
bcrypt (~> 3.1.19)
|
bcrypt (~> 3.1.20)
|
||||||
better_errors
|
better_errors
|
||||||
binding_of_caller
|
binding_of_caller
|
||||||
bootsnap
|
bootsnap
|
||||||
bootstrap_form (~> 5.0)
|
bootstrap_form (~> 5.0)
|
||||||
bullet
|
bullet
|
||||||
carrierwave (~> 2.0)
|
carrierwave (~> 2.1)
|
||||||
carrierwave_backgrounder!
|
carrierwave_backgrounder (~> 0.4.2)
|
||||||
colorize
|
colorize
|
||||||
connection_pool
|
connection_pool
|
||||||
cssbundling-rails (~> 1.2)
|
cssbundling-rails (~> 1.4)
|
||||||
database_cleaner
|
database_cleaner
|
||||||
devise (~> 4.9)
|
devise (~> 4.9)
|
||||||
devise-async
|
devise-async
|
||||||
devise-i18n
|
devise-i18n
|
||||||
dotenv-rails (~> 2.8)
|
dotenv-rails (~> 3.1)
|
||||||
dry-initializer (~> 3.1)
|
dry-initializer (~> 3.1)
|
||||||
dry-types (~> 1.7)
|
dry-types (~> 1.7)
|
||||||
factory_bot_rails
|
factory_bot_rails
|
||||||
|
@ -523,14 +561,14 @@ DEPENDENCIES
|
||||||
fog-aws
|
fog-aws
|
||||||
fog-core
|
fog-core
|
||||||
fog-local
|
fog-local
|
||||||
haml (~> 6.1)
|
haml (~> 6.3)
|
||||||
haml_lint
|
haml_lint
|
||||||
hcaptcha (~> 7.0)
|
hcaptcha!
|
||||||
httparty
|
httparty
|
||||||
i18n-js (= 4.0)
|
i18n-js (= 4.0)
|
||||||
jsbundling-rails (~> 1.1)
|
jsbundling-rails (~> 1.3)
|
||||||
json-schema
|
json-schema
|
||||||
jwt (~> 2.7)
|
jwt (~> 2.8)
|
||||||
letter_opener
|
letter_opener
|
||||||
lograge
|
lograge
|
||||||
mail (~> 2.7.1)
|
mail (~> 2.7.1)
|
||||||
|
@ -539,14 +577,13 @@ DEPENDENCIES
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
oj
|
oj
|
||||||
openssl (~> 3.1)
|
openssl (~> 3.2)
|
||||||
pg
|
pg
|
||||||
pghero
|
pghero
|
||||||
prometheus-client (~> 4.2)
|
prometheus-client (~> 4.2)
|
||||||
puma
|
puma
|
||||||
pundit (~> 2.3)
|
pundit (~> 2.3)
|
||||||
questiongenerator (~> 1.2)!
|
rails (~> 7.0.8)
|
||||||
rails (~> 6.1)
|
|
||||||
rails-controller-testing
|
rails-controller-testing
|
||||||
rails-i18n (~> 7.0)
|
rails-i18n (~> 7.0)
|
||||||
rails_admin
|
rails_admin
|
||||||
|
@ -558,27 +595,29 @@ DEPENDENCIES
|
||||||
rqrcode
|
rqrcode
|
||||||
rspec-its (~> 1.3)
|
rspec-its (~> 1.3)
|
||||||
rspec-mocks
|
rspec-mocks
|
||||||
rspec-rails (~> 6.0)
|
rspec-rails (~> 6.1)
|
||||||
rspec-sidekiq (~> 4.0)
|
rspec-sidekiq (~> 5.0)
|
||||||
rubocop (~> 1.56)
|
rubocop (~> 1.64)
|
||||||
rubocop-rails (~> 2.21)
|
rubocop-rails (~> 2.25)
|
||||||
rubyzip (~> 2.3)
|
rubyzip (~> 2.3)
|
||||||
sanitize
|
sanitize
|
||||||
sassc-rails
|
sassc-rails
|
||||||
sentry-rails
|
sentry-rails
|
||||||
sentry-ruby
|
sentry-ruby
|
||||||
sentry-sidekiq
|
sentry-sidekiq
|
||||||
shoulda-matchers (~> 5.3)
|
shoulda-matchers (~> 6.2)
|
||||||
sidekiq (< 7)
|
sidekiq (< 7)
|
||||||
|
sidekiq-scheduler
|
||||||
simplecov
|
simplecov
|
||||||
simplecov-cobertura
|
simplecov-cobertura
|
||||||
simplecov-json
|
simplecov-json
|
||||||
spring (~> 4.1)
|
|
||||||
sprockets (~> 4.2)
|
sprockets (~> 4.2)
|
||||||
sprockets-rails
|
sprockets-rails
|
||||||
tldv (~> 0.1.0)
|
tldv (~> 0.1.0)
|
||||||
turbo-rails
|
turbo-rails
|
||||||
twitter-text
|
twitter-text
|
||||||
|
view_component
|
||||||
|
web-push
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.3.18
|
2.5.5
|
||||||
|
|
|
@ -4,7 +4,7 @@ Currently the only change is to enable long questions by default.
|
||||||
|
|
||||||
## Licence
|
## 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
|
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
|
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
|
Rails.application.load_tasks
|
||||||
|
|
||||||
namespace :justask do # rubocop:disable Metrics/BlockLength
|
namespace :justask do
|
||||||
desc "Gives admin status to a user."
|
desc "Gives admin status to a user."
|
||||||
task :admin, [:screen_name] => :environment do |_t, args|
|
task :admin, [:screen_name] => :environment do |_t, args|
|
||||||
abort "screen name required" if args[:screen_name].nil?
|
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 {
|
.pull-left {
|
||||||
float: 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";
|
@use "sass:map";
|
||||||
|
|
||||||
.answerbox {
|
.answerbox {
|
||||||
&__question-text,
|
|
||||||
&__question-user,
|
|
||||||
&__answer-user,
|
&__answer-user,
|
||||||
&__answer-date {
|
&__answer-date {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -25,7 +23,6 @@
|
||||||
margin-bottom: map.get($spacers, 3);
|
margin-bottom: map.get($spacers, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__question-user-avatar,
|
|
||||||
&__answer-user-avatar {
|
&__answer-user-avatar {
|
||||||
margin-right: map.get($spacers, 2);
|
margin-right: map.get($spacers, 2);
|
||||||
border-radius: $avatar-border-radius;
|
border-radius: $avatar-border-radius;
|
||||||
|
@ -38,8 +35,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action {
|
&__action {
|
||||||
padding-left: 0;
|
color: RGBA(var(--raised-text), 0.75);
|
||||||
padding-right: map.get($spacers, 1);
|
padding: var(--btn-padding-y);
|
||||||
|
margin-right: map.get($spacers, 1);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
& i {
|
& i {
|
||||||
|
@ -50,24 +48,28 @@
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:active {
|
&:active {
|
||||||
|
color: RGBA(var(--raised-text), 1);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[name="ab-smile"],
|
&.smile {
|
||||||
&[name="ab-smile-comment"] {
|
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&[data-action="unsmile"] {
|
&.unsmile {
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.dropdown-toggle::after {
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,9 +99,3 @@
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body:not(.cap-web-share) {
|
|
||||||
[name="ab-share"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.answerbox__question-text {
|
&.question__text {
|
||||||
max-height: 15rem;
|
max-height: 15rem;
|
||||||
|
|
||||||
@include media-breakpoint-up('sm') {
|
@include media-breakpoint-up('sm') {
|
||||||
|
|
|
@ -23,6 +23,10 @@
|
||||||
.text-muted {
|
.text-muted {
|
||||||
color: RGBA(var(--primary-text), 0.8) !important;
|
color: RGBA(var(--primary-text), 0.8) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,3 +61,10 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body:not(.cap-web-share) {
|
||||||
|
|
||||||
|
[data-controller="share"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,30 @@
|
||||||
|
@use "sass:map";
|
||||||
|
|
||||||
.question {
|
.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%;
|
width: 100%;
|
||||||
z-index: 999;
|
|
||||||
|
|
||||||
@include media-breakpoint-up('sm') {
|
@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 {
|
||||||
|
--btn-padding-x: 1rem;
|
||||||
|
--btn-border-radius: 2rem;
|
||||||
color: RGB(var(--body-text));
|
color: RGB(var(--body-text));
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-link:hover {
|
.btn-link:hover {
|
||||||
|
|
|
@ -20,3 +20,18 @@
|
||||||
background-color: var(--raised-accent);
|
background-color: var(--raised-accent);
|
||||||
border: 0;
|
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"
|
require "cgi"
|
||||||
|
|
||||||
class Ajax::AnswerController < AjaxController
|
class Ajax::AnswerController < AjaxController
|
||||||
include SocialHelper::TwitterMethods
|
include SocialHelper
|
||||||
include SocialHelper::TumblrMethods
|
|
||||||
include SocialHelper::TelegramMethods
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
params.require :id
|
params.require :id
|
||||||
|
@ -15,7 +13,7 @@ class Ajax::AnswerController < AjaxController
|
||||||
inbox = (params[:inbox] == "true")
|
inbox = (params[:inbox] == "true")
|
||||||
|
|
||||||
if inbox
|
if inbox
|
||||||
inbox_entry = Inbox.find(params[:id])
|
inbox_entry = InboxEntry.find(params[:id])
|
||||||
|
|
||||||
unless current_user == inbox_entry.user
|
unless current_user == inbox_entry.user
|
||||||
@response[:status] = :fail
|
@response[:status] = :fail
|
||||||
|
@ -46,9 +44,8 @@ class Ajax::AnswerController < AjaxController
|
||||||
|
|
||||||
return if inbox
|
return if inbox
|
||||||
|
|
||||||
# this assign is needed because shared/_answerbox relies on it, I think
|
|
||||||
@question = 1
|
@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
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -62,7 +59,7 @@ class Ajax::AnswerController < AjaxController
|
||||||
return
|
return
|
||||||
end
|
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
|
answer.destroy
|
||||||
|
|
||||||
@response[:status] = :okay
|
@response[:status] = :okay
|
||||||
|
@ -73,7 +70,10 @@ class Ajax::AnswerController < AjaxController
|
||||||
private
|
private
|
||||||
|
|
||||||
def sharing_hash(answer) = {
|
def sharing_hash(answer) = {
|
||||||
|
url: answer_share_url(answer),
|
||||||
|
text: prepare_tweet(answer, nil, true),
|
||||||
twitter: twitter_share_url(answer),
|
twitter: twitter_share_url(answer),
|
||||||
|
bluesky: bluesky_share_url(answer),
|
||||||
tumblr: tumblr_share_url(answer),
|
tumblr: tumblr_share_url(answer),
|
||||||
telegram: telegram_share_url(answer),
|
telegram: telegram_share_url(answer),
|
||||||
custom: CGI.escape(prepare_tweet(answer)),
|
custom: CGI.escape(prepare_tweet(answer)),
|
||||||
|
|
|
@ -14,10 +14,12 @@ class Ajax::CommentController < AjaxController
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
comments = Comment.where(answer:).includes([{ user: :profile }, :smiles])
|
||||||
|
|
||||||
@response[:status] = :okay
|
@response[:status] = :okay
|
||||||
@response[:message] = t(".success")
|
@response[:message] = t(".success")
|
||||||
@response[:success] = true
|
@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
|
@response[:count] = answer.comment_count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Ajax::InboxController < AjaxController
|
class Ajax::InboxController < AjaxController
|
||||||
def remove
|
def remove
|
||||||
params.require :id
|
params.require :id
|
||||||
|
|
||||||
inbox = Inbox.find(params[:id])
|
inbox = InboxEntry.find(params[:id])
|
||||||
|
|
||||||
unless current_user == inbox.user
|
unless current_user == inbox.user
|
||||||
@response[:status] = :fail
|
@response[:status] = :fail
|
||||||
|
@ -28,7 +30,7 @@ class Ajax::InboxController < AjaxController
|
||||||
raise unless user_signed_in?
|
raise unless user_signed_in?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
Inbox.where(user: current_user).each { |i| i.remove }
|
InboxEntry.where(user: current_user).find_each(&:remove)
|
||||||
rescue => e
|
rescue => e
|
||||||
Sentry.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
@response[:status] = :err
|
@response[:status] = :err
|
||||||
|
@ -43,10 +45,10 @@ class Ajax::InboxController < AjaxController
|
||||||
|
|
||||||
def remove_all_author
|
def remove_all_author
|
||||||
begin
|
begin
|
||||||
@target_user = User.where('LOWER(screen_name) = ?', params[:author].downcase).first!
|
@target_user = User.where("LOWER(screen_name) = ?", params[:author].downcase).first!
|
||||||
@inbox = current_user.inboxes.joins(:question)
|
@inbox = current_user.inbox_entries.joins(:question)
|
||||||
.where(questions: { user_id: @target_user.id, author_is_anonymous: false })
|
.where(questions: { user_id: @target_user.id, author_is_anonymous: false })
|
||||||
@inbox.each { |i| i.remove }
|
@inbox.each(&:remove)
|
||||||
rescue => e
|
rescue => e
|
||||||
Sentry.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
@response[:status] = :err
|
@response[:status] = :err
|
||||||
|
|
|
@ -84,7 +84,7 @@ class Ajax::ModerationController < AjaxController
|
||||||
target_user = User.find_by_screen_name!(params[:user])
|
target_user = User.find_by_screen_name!(params[:user])
|
||||||
|
|
||||||
@response[:message] = t(".error")
|
@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)
|
unless current_user.has_cached_role?(:administrator)
|
||||||
@response[:status] = :nopriv
|
@response[:status] = :nopriv
|
||||||
|
@ -94,7 +94,7 @@ class Ajax::ModerationController < AjaxController
|
||||||
|
|
||||||
@response[:checked] = status
|
@response[:checked] = status
|
||||||
type = params[:type].downcase
|
type = params[:type].downcase
|
||||||
target_role = {'admin' => 'administrator'}.fetch(type, type).to_sym
|
target_role = type.to_sym
|
||||||
|
|
||||||
if status
|
if status
|
||||||
target_user.add_role target_role
|
target_user.add_role target_role
|
||||||
|
|
|
@ -1,19 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Ajax::QuestionController < AjaxController
|
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
|
def create
|
||||||
params.require :question
|
params.require :question
|
||||||
params.require :anonymousQuestion
|
params.require :anonymousQuestion
|
||||||
|
@ -24,14 +11,15 @@ class Ajax::QuestionController < AjaxController
|
||||||
@response = {
|
@response = {
|
||||||
success: true,
|
success: true,
|
||||||
message: t(".success"),
|
message: t(".success"),
|
||||||
status: :okay
|
status: :okay,
|
||||||
}
|
}
|
||||||
|
|
||||||
if user_signed_in? && params[:rcpt] == "followers"
|
if user_signed_in? && params[:rcpt] == "followers"
|
||||||
UseCase::Question::CreateFollowers.call(
|
UseCase::Question::CreateFollowers.call(
|
||||||
source_user_id: current_user.id,
|
source_user_id: current_user.id,
|
||||||
content: params[:question],
|
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
|
return
|
||||||
end
|
end
|
||||||
|
@ -41,7 +29,20 @@ class Ajax::QuestionController < AjaxController
|
||||||
target_user_id: params[:rcpt],
|
target_user_id: params[:rcpt],
|
||||||
content: params[:question],
|
content: params[:question],
|
||||||
anonymous: params[:anonymousQuestion],
|
anonymous: params[:anonymousQuestion],
|
||||||
author_identifier: AnonymousBlock.get_identifier(request.remote_ip)
|
author_identifier: AnonymousBlock.get_identifier(request.remote_ip),
|
||||||
)
|
)
|
||||||
end
|
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
|
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)
|
@response[:message] = t(".subscription_count", count: current_user.web_push_subscriptions.count)
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsubscribe # rubocop:disable Metrics/AbcSize
|
def unsubscribe
|
||||||
removed = if params.key?(:endpoint)
|
removed = if params.key?(:endpoint)
|
||||||
current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).destroy_all
|
current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).destroy_all
|
||||||
else
|
else
|
||||||
|
|
|
@ -20,8 +20,8 @@ class AnonymousBlockController < ApplicationController
|
||||||
target_user: question.user
|
target_user: question.user
|
||||||
)
|
)
|
||||||
|
|
||||||
inbox_id = question.inboxes.first&.id
|
inbox_id = question.inbox_entries.first&.id
|
||||||
question.inboxes.first&.destroy
|
question.inbox_entries.first&.destroy
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.turbo_stream do
|
format.turbo_stream do
|
||||||
|
|
|
@ -8,13 +8,11 @@ class AnswerController < ApplicationController
|
||||||
turbo_stream_actions :pin, :unpin
|
turbo_stream_actions :pin, :unpin
|
||||||
|
|
||||||
def show
|
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
|
@display_all = true
|
||||||
@subscribed_answer_ids = []
|
|
||||||
|
|
||||||
return unless user_signed_in?
|
return unless user_signed_in?
|
||||||
|
|
||||||
@subscribed_answer_ids = Subscription.where(user: current_user, answer: @answer).pluck(:answer_id)
|
|
||||||
mark_notifications_as_read
|
mark_notifications_as_read
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -51,11 +49,12 @@ class AnswerController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
def mark_notifications_as_read
|
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)
|
.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::Commented", target_id: @answer.comments.pluck(:id)))
|
||||||
.or(Notification.where(type: "Notification::Smiled", target_id: @answer.smiles.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))))
|
.or(Notification.where(type: "Notification::CommentSmiled", target_id: @answer.comment_smiles.pluck(:id))))
|
||||||
.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
|
.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
|
||||||
|
current_user.touch(:notifications_updated_at) if updated.positive?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
|
||||||
around_action :switch_locale
|
around_action :switch_locale
|
||||||
before_action :banned?
|
before_action :banned?
|
||||||
before_action :find_active_announcements
|
before_action :find_active_announcements
|
||||||
|
before_action :set_has_new_reports
|
||||||
|
|
||||||
# check if user wants to read
|
# check if user wants to read
|
||||||
def switch_locale(&)
|
def switch_locale(&)
|
||||||
|
@ -30,13 +31,15 @@ class ApplicationController < ActionController::Base
|
||||||
# obligatory '2001: A Space Odyssey' reference
|
# obligatory '2001: A Space Odyssey' reference
|
||||||
flash[:notice] = t("user.sessions.create.banned", name:)
|
flash[:notice] = t("user.sessions.create.banned", name:)
|
||||||
current_ban = current_user.bans.current.first
|
current_ban = current_user.bans.current.first
|
||||||
unless current_ban&.reason.nil?
|
flash[:notice] += "\n#{t('user.sessions.create.reason', reason: current_ban.reason)}" unless current_ban&.reason&.empty?
|
||||||
flash[:notice] += "\n#{t('user.sessions.create.reason', reason: current_ban.reason)}"
|
|
||||||
end
|
flash[:notice] += if current_ban&.permanent?
|
||||||
unless current_ban&.permanent?
|
"\n#{t('user.sessions.create.permanent')}"
|
||||||
# TODO format banned_until
|
else
|
||||||
flash[:notice] += "\n#{t('user.sessions.create.until', time: current_ban.expires_at)}"
|
# TODO: format banned_until
|
||||||
|
"\n#{t('user.sessions.create.until', time: current_ban.expires_at)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
sign_out current_user
|
sign_out current_user
|
||||||
redirect_to new_user_session_path
|
redirect_to new_user_session_path
|
||||||
end
|
end
|
||||||
|
@ -46,6 +49,18 @@ class ApplicationController < ActionController::Base
|
||||||
@active_announcements ||= Announcement.find_active
|
@active_announcements ||= Announcement.find_active
|
||||||
end
|
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
|
include ApplicationHelper
|
||||||
|
|
||||||
protected
|
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])
|
@answers = yield(last_id: params[:last_id])
|
||||||
answer_ids = @answers.map(&:id)
|
answer_ids = @answers.map(&:id)
|
||||||
@answers_last_id = answer_ids.min
|
@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).select("answers.id").count.zero?
|
||||||
@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?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,6 +23,8 @@ module TurboStreamable
|
||||||
render_error t("errors.parameter_error", parameter: e.instance_of?(KeyError) ? e.key : e.param.capitalize)
|
render_error t("errors.parameter_error", parameter: e.instance_of?(KeyError) ? e.key : e.param.capitalize)
|
||||||
rescue Dry::Types::CoercionError, Dry::Types::ConstraintError
|
rescue Dry::Types::CoercionError, Dry::Types::ConstraintError
|
||||||
render_error t("errors.invalid_parameter")
|
render_error t("errors.invalid_parameter")
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
render_error e.record.errors.full_messages.flatten.join(" ")
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_error t("errors.record_not_found")
|
render_error t("errors.record_not_found")
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,36 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class DiscoverController < ApplicationController
|
class DiscoverController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
unless APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod?
|
return redirect_to root_path unless APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod?
|
||||||
return redirect_to root_path
|
|
||||||
end
|
|
||||||
|
|
||||||
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)
|
@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.where("created_at > ?", Time.now.ago(1.week)).order(:comment_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 > ?", Time.now.ago(1.week)).order(:answer_count).reverse_order.limit(top_x).includes(:user)
|
@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)
|
@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
|
# .user = the user
|
||||||
# .question_count = how many questions did the user ask
|
# .question_count = how many questions did the user ask
|
||||||
@users_with_most_questions = Question.select('user_id, COUNT(*) AS question_count').
|
@users_with_most_questions = Question.select("user_id, COUNT(*) AS question_count")
|
||||||
where("created_at > ?", Time.now.ago(1.week)).
|
.where("created_at > ?", week_ago)
|
||||||
where(author_is_anonymous: false).
|
.where(author_is_anonymous: false)
|
||||||
group(:user_id).
|
.group(:user_id)
|
||||||
order('question_count').
|
.order("question_count")
|
||||||
reverse_order.limit(top_x)
|
.reverse_order.limit(top_x)
|
||||||
|
|
||||||
# .user = the user
|
# .user = the user
|
||||||
# .answer_count = how many questions did the user answer
|
# .answer_count = how many questions did the user answer
|
||||||
@users_with_most_answers = Answer.select('user_id, COUNT(*) AS answer_count').
|
@users_with_most_answers = Answer.select("user_id, COUNT(*) AS answer_count")
|
||||||
where("created_at > ?", Time.now.ago(1.week)).
|
.where("created_at > ?", week_ago)
|
||||||
group(:user_id).
|
.group(:user_id)
|
||||||
order('answer_count').
|
.order("answer_count")
|
||||||
reverse_order.limit(top_x)
|
.reverse_order.limit(top_x)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,30 +3,17 @@
|
||||||
class InboxController < ApplicationController
|
class InboxController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
after_action :mark_inbox_entries_as_read, only: %i[show]
|
def show
|
||||||
|
|
||||||
def show # rubocop:disable Metrics/MethodLength
|
|
||||||
find_author
|
|
||||||
find_inbox_entries
|
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
|
@delete_id = find_delete_id
|
||||||
@disabled = true if @inbox.empty?
|
@disabled = true if @inbox.empty?
|
||||||
|
|
||||||
respond_to do |format|
|
mark_inbox_entries_as_read
|
||||||
format.html { render "show" }
|
|
||||||
format.turbo_stream do
|
|
||||||
render "show", layout: false, status: :see_other
|
|
||||||
|
|
||||||
# rubocop disabled as just flipping a flag doesn't need to have validations to be run
|
respond_to do |format|
|
||||||
@inbox.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
|
format.html
|
||||||
end
|
format.turbo_stream
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -36,7 +23,7 @@ class InboxController < ApplicationController
|
||||||
author_identifier: "justask",
|
author_identifier: "justask",
|
||||||
user: current_user)
|
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
|
increment_metric
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
@ -52,41 +39,29 @@ class InboxController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def find_author
|
def filter_params
|
||||||
return if params[:author].blank?
|
params.slice(*InboxFilter::KEYS).permit(*InboxFilter::KEYS)
|
||||||
|
|
||||||
@author = params[:author]
|
|
||||||
|
|
||||||
@author_user = User.where("LOWER(screen_name) = ?", @author.downcase).first
|
|
||||||
flash.now[:error] = t(".author.error", author: @author) unless @author_user
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_inbox_entries
|
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
|
@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?
|
@more_data_available = filter.cursored_results(last_id: @inbox_last_id, size: 1).count.positive?
|
||||||
@inbox_count = current_user.inboxes.then(&method(:filter_author_chain)).count
|
@inbox_count = filter.results.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_delete_id
|
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"
|
"ib-delete-all"
|
||||||
end
|
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
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
def mark_inbox_entries_as_read
|
def mark_inbox_entries_as_read
|
||||||
# using .dup to not modify @inbox -- useful in tests
|
# using .dup to not modify @inbox -- useful in tests
|
||||||
@inbox&.dup&.update_all(new: false)
|
updated = @inbox&.dup&.update_all(new: false)
|
||||||
current_user.touch(:inbox_updated_at)
|
current_user.touch(:inbox_updated_at) if updated.positive?
|
||||||
end
|
end
|
||||||
# rubocop:enable Rails/SkipsModelValidations
|
# 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
|
def index
|
||||||
@user = User.find_by(screen_name: params[:user])
|
@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
|
@inbox_last_id = @inboxes.map(&:id).min
|
||||||
@more_data_available = !@user.cursored_inbox(last_id: @inbox_last_id, size: 1).count.zero?
|
@more_data_available = !filter.cursored_results(last_id: @inbox_last_id, size: 1).count.zero?
|
||||||
@inbox_count = @user.inboxes.count
|
@inbox_count = @user.inbox_entries.count
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
format.turbo_stream { render "index", layout: false, status: :see_other }
|
format.turbo_stream { render "index", layout: false, status: :see_other }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(*InboxFilter::KEYS).permit(*InboxFilter::KEYS)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,12 +2,15 @@
|
||||||
|
|
||||||
class Moderation::ReportsController < ApplicationController
|
class Moderation::ReportsController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
before_action :set_filter_enabled
|
||||||
|
before_action :set_type_options
|
||||||
|
before_action :set_last_reports_visit
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@type = params[:type]
|
filter = ReportFilter.new(filter_params)
|
||||||
@reports = list_reports(type: @type, last_id: params[:last_id])
|
@reports = filter.cursored_results(last_id: params[:last_id])
|
||||||
@reports_last_id = @reports.map(&:id).min
|
@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|
|
respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
|
@ -17,13 +20,29 @@ class Moderation::ReportsController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def list_reports(type:, last_id:, size: nil)
|
def filter_params
|
||||||
cursor_params = { last_id:, size: }.compact
|
params.slice(*ReportFilter::KEYS).permit(*ReportFilter::KEYS)
|
||||||
|
end
|
||||||
|
|
||||||
if type == "all"
|
def set_filter_enabled
|
||||||
Report.cursored_reports(**cursor_params)
|
@filter_enabled = params.slice(*ReportFilter::KEYS)
|
||||||
else
|
.reject! { |_, value| value.empty? || value.nil? }
|
||||||
Report.cursored_reports_of_type(type, **cursor_params)
|
.values
|
||||||
end
|
.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
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
class NotificationsController < ApplicationController
|
class NotificationsController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
after_action :mark_notifications_as_read, only: %i[index]
|
|
||||||
|
|
||||||
TYPE_MAPPINGS = {
|
TYPE_MAPPINGS = {
|
||||||
"answer" => Notification::QuestionAnswered.name,
|
"answer" => Notification::QuestionAnswered.name,
|
||||||
"comment" => Notification::Commented.name,
|
"comment" => Notification::Commented.name,
|
||||||
|
@ -18,6 +16,7 @@ class NotificationsController < ApplicationController
|
||||||
@notifications = cursored_notifications_for(type: @type, last_id: params[:last_id])
|
@notifications = cursored_notifications_for(type: @type, last_id: params[:last_id])
|
||||||
paginate_notifications
|
paginate_notifications
|
||||||
@counters = count_unread_by_type
|
@counters = count_unread_by_type
|
||||||
|
mark_notifications_as_read
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
|
@ -52,8 +51,8 @@ class NotificationsController < ApplicationController
|
||||||
# rubocop:disable Rails/SkipsModelValidations
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
def mark_notifications_as_read
|
def mark_notifications_as_read
|
||||||
# using .dup to not modify @notifications -- useful in tests
|
# using .dup to not modify @notifications -- useful in tests
|
||||||
@notifications&.dup&.update_all(new: false)
|
updated = @notifications&.dup&.update_all(new: false)
|
||||||
current_user.touch(:notifications_updated_at)
|
current_user.touch(:notifications_updated_at) if updated.positive?
|
||||||
end
|
end
|
||||||
# rubocop:enable Rails/SkipsModelValidations
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,7 @@ class QuestionController < ApplicationController
|
||||||
@answers = @question.cursored_answers(last_id: params[:last_id], current_user:)
|
@answers = @question.cursored_answers(last_id: params[:last_id], current_user:)
|
||||||
answer_ids = @answers.map(&:id)
|
answer_ids = @answers.map(&:id)
|
||||||
@answers_last_id = answer_ids.min
|
@answers_last_id = answer_ids.min
|
||||||
@more_data_available = !@question.cursored_answers(last_id: @answers_last_id, size: 1, current_user:).count.zero?
|
@more_data_available = !@question.cursored_answers(last_id: @answers_last_id, size: 1, current_user:).select("answers.id").count.zero?
|
||||||
@subscribed = Subscription.where(user: current_user, answer_id: answer_ids).pluck(:answer_id) if user_signed_in?
|
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html
|
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
|
private
|
||||||
|
|
||||||
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
def mark_notifications_as_read
|
def mark_notifications_as_read
|
||||||
Notification::DataExported
|
updated = Notification::DataExported
|
||||||
.where(recipient: current_user, new: true)
|
.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
|
end
|
||||||
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,6 +19,6 @@ class Settings::PrivacyController < ApplicationController
|
||||||
else
|
else
|
||||||
flash[:error] = t(".error")
|
flash[:error] = t(".error")
|
||||||
end
|
end
|
||||||
redirect_to settings_privacy_path
|
render :edit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,6 @@ class Settings::ProfileController < ApplicationController
|
||||||
flash[:error] = t(".error")
|
flash[:error] = t(".error")
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to settings_profile_path
|
render :edit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,9 +12,10 @@ class Settings::ProfilePictureController < ApplicationController
|
||||||
text += t(".notice.profile_header") if user_attributes[:profile_header]
|
text += t(".notice.profile_header") if user_attributes[:profile_header]
|
||||||
flash[:success] = text
|
flash[:success] = text
|
||||||
else
|
else
|
||||||
flash[:error] = t(".error")
|
# CarrierWave resets the image to the default upon an error
|
||||||
|
current_user.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to settings_profile_path
|
render "settings/profile/edit"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Settings::ThemeController < ApplicationController
|
||||||
else
|
else
|
||||||
flash[:error] = t(".error", errors: current_user.theme.errors.messages.flatten.join(" "))
|
flash[:error] = t(".error", errors: current_user.theme.errors.messages.flatten.join(" "))
|
||||||
end
|
end
|
||||||
redirect_to settings_theme_path
|
render :edit
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
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
|
def paginate_timeline
|
||||||
@timeline = yield(last_id: params[:last_id])
|
@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
|
@timeline_last_id = timeline_ids.min
|
||||||
@more_data_available = !yield(last_id: @timeline_last_id, size: 1).count.zero?
|
@more_data_available = !yield(last_id: @timeline_last_id, size: 1).select("answers.id").count.zero?
|
||||||
@subscribed_answer_ids = Subscription.where(user: current_user, answer_id: timeline_ids).pluck(:answer_id)
|
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { render "timeline/timeline" }
|
format.html { render "timeline/timeline" }
|
||||||
|
|
|
@ -8,8 +8,8 @@ class UserController < ApplicationController
|
||||||
after_action :mark_notification_as_read, only: %i[show]
|
after_action :mark_notification_as_read, only: %i[show]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@pinned_answers = @user.answers.pinned.order(pinned_at: :desc).limit(10)
|
@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(**args) }
|
paginate_answers { |args| @user.cursored_answers(current_user_id: current_user, **args) }
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module AjaxHelper
|
module AjaxHelper
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,13 +4,13 @@ module ApplicationHelper::GraphMethods
|
||||||
# Creates <meta> tags for OpenGraph properties from a hash
|
# Creates <meta> tags for OpenGraph properties from a hash
|
||||||
# @param values [Hash]
|
# @param values [Hash]
|
||||||
def opengraph_meta_tags(values)
|
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
|
end
|
||||||
|
|
||||||
# Creates <meta> tags from a hash
|
# Creates <meta> tags from a hash
|
||||||
# @param values [Hash]
|
# @param values [Hash]
|
||||||
def meta_tags(values)
|
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
|
end
|
||||||
|
|
||||||
# @param user [User]
|
# @param user [User]
|
||||||
|
@ -22,7 +22,7 @@ module ApplicationHelper::GraphMethods
|
||||||
"og:url": user_url(user),
|
"og:url": user_url(user),
|
||||||
"og:description": user.profile.description,
|
"og:description": user.profile.description,
|
||||||
"og:site_name": APP_CONFIG["site_name"],
|
"og:site_name": APP_CONFIG["site_name"],
|
||||||
"profile:username": user.screen_name
|
"profile:username": user.screen_name,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ module ApplicationHelper::GraphMethods
|
||||||
"twitter:site": "@retrospring",
|
"twitter:site": "@retrospring",
|
||||||
"twitter:title": user.profile.motivation_header.presence || "Ask me anything!",
|
"twitter:title": user.profile.motivation_header.presence || "Ask me anything!",
|
||||||
"twitter:description": "Ask #{user.profile.safe_name} anything on Retrospring",
|
"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
|
end
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ module ApplicationHelper::GraphMethods
|
||||||
"og:image": full_profile_picture_url(answer.user),
|
"og:image": full_profile_picture_url(answer.user),
|
||||||
"og:url": answer_url(answer.user.screen_name, answer.id),
|
"og:url": answer_url(answer.user.screen_name, answer.id),
|
||||||
"og:description": answer.content,
|
"og:description": answer.content,
|
||||||
"og:site_name": APP_CONFIG["site_name"]
|
"og:site_name": APP_CONFIG["site_name"],
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ module ApplicationHelper::TitleMethods
|
||||||
def question_title(question)
|
def question_title(question)
|
||||||
context_user = question.answers&.first&.user if question.direct
|
context_user = question.answers&.first&.user if question.direct
|
||||||
name = user_screen_name question.user,
|
name = user_screen_name question.user,
|
||||||
context_user: context_user,
|
context_user:,
|
||||||
author_identifier: question.author_is_anonymous ? question.author_identifier : nil,
|
author_identifier: question.author_is_anonymous ? question.author_identifier : nil,
|
||||||
url: false
|
url: false
|
||||||
generate_title name, "asked", question.content
|
generate_title name, "asked", question.content
|
||||||
|
|
|
@ -8,6 +8,7 @@ module BootstrapHelper
|
||||||
badge_attr: {},
|
badge_attr: {},
|
||||||
icon: nil,
|
icon: nil,
|
||||||
class: "",
|
class: "",
|
||||||
|
id: nil,
|
||||||
hotkey: nil,
|
hotkey: nil,
|
||||||
}.merge(options)
|
}.merge(options)
|
||||||
|
|
||||||
|
@ -24,24 +25,24 @@ module BootstrapHelper
|
||||||
"#{content_tag(:i, '', class: "fa fa-#{options[:icon]}")} #{body}"
|
"#{content_tag(:i, '', class: "fa fa-#{options[:icon]}")} #{body}"
|
||||||
end
|
end
|
||||||
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_class = [
|
||||||
"badge",
|
"badge",
|
||||||
("badge-#{options[:badge_color]}" unless options[:badge_color].nil?),
|
("badge-#{options[:badge_color]}" unless options[:badge_color].nil?),
|
||||||
("badge-pill" if options[:badge_pill])
|
("badge-pill" if options[:badge_pill])
|
||||||
].compact.join(" ")
|
].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
|
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
|
end
|
||||||
|
|
||||||
def list_group_item(body, path, options = {})
|
def list_group_item(body, path, options = {})
|
||||||
options = {
|
options = {
|
||||||
badge: nil,
|
badge: nil,
|
||||||
badge_color: nil,
|
badge_color: nil,
|
||||||
class: ""
|
class: "",
|
||||||
}.merge(options)
|
}.merge(options)
|
||||||
|
|
||||||
classes = [
|
classes = [
|
||||||
|
@ -53,25 +54,26 @@ module BootstrapHelper
|
||||||
|
|
||||||
unless options[:badge].nil? || (options[:badge]).zero?
|
unless options[:badge].nil? || (options[:badge]).zero?
|
||||||
# TODO: make this prettier?
|
# TODO: make this prettier?
|
||||||
body << " #{
|
badge = content_tag(:span, options[:badge], class: "badge#{
|
||||||
content_tag(:span, options[:badge], class: "badge#{
|
|
||||||
" badge-#{options[:badge_color]}" unless options[:badge_color].nil?
|
" badge-#{options[:badge_color]}" unless options[:badge_color].nil?
|
||||||
}")}"
|
}",)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def tooltip(body, tooltip_content, placement = "bottom")
|
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
|
end
|
||||||
|
|
||||||
def time_tooltip(subject, placement = "bottom")
|
def time_tooltip(subject, placement = "bottom")
|
||||||
tooltip time_ago_in_words(subject.created_at), localize(subject.created_at), placement
|
tooltip time_ago_in_words(subject.created_at, scope: "datetime.distance_in_words.short"), localize(subject.created_at, format: :long), placement
|
||||||
end
|
|
||||||
|
|
||||||
def hidespan(body, hide)
|
|
||||||
content_tag(:span, body, class: hide)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -8,7 +8,7 @@ module FeedbackHelper
|
||||||
avatarURL: current_user.profile_picture.url(:large),
|
avatarURL: current_user.profile_picture.url(:large),
|
||||||
name: current_user.screen_name,
|
name: current_user.screen_name,
|
||||||
id: current_user.id,
|
id: current_user.id,
|
||||||
email: current_user.email
|
email: current_user.email,
|
||||||
}
|
}
|
||||||
|
|
||||||
JWT.encode(user_data, APP_CONFIG.dig("canny", "sso"))
|
JWT.encode(user_data, APP_CONFIG.dig("canny", "sso"))
|
||||||
|
|
|
@ -30,7 +30,7 @@ module MarkdownHelper
|
||||||
def raw_markdown(content)
|
def raw_markdown(content)
|
||||||
renderer = Redcarpet::Render::HTML.new(**MARKDOWN_RENDERER_OPTS)
|
renderer = Redcarpet::Render::HTML.new(**MARKDOWN_RENDERER_OPTS)
|
||||||
md = Redcarpet::Markdown.new(renderer, **MARKDOWN_OPTS)
|
md = Redcarpet::Markdown.new(renderer, **MARKDOWN_OPTS)
|
||||||
raw md.render content
|
raw md.render content # rubocop:disable Rails/OutputSafety
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_markdown(path, relative_to = Rails.root)
|
def get_markdown(path, relative_to = Rails.root)
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module SocialHelper
|
module SocialHelper
|
||||||
|
include SocialHelper::BlueskyMethods
|
||||||
include SocialHelper::TwitterMethods
|
include SocialHelper::TwitterMethods
|
||||||
include SocialHelper::TumblrMethods
|
include SocialHelper::TumblrMethods
|
||||||
include SocialHelper::TelegramMethods
|
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
|
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,
|
id: answer.id,
|
||||||
username: answer.user.screen_name,
|
username: answer.user.screen_name,
|
||||||
host: APP_CONFIG["hostname"],
|
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))})
|
%(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
|
module SocialHelper::TumblrMethods
|
||||||
def tumblr_title(answer)
|
def tumblr_title(answer)
|
||||||
|
@ -15,8 +17,8 @@ module SocialHelper::TumblrMethods
|
||||||
answer_url = answer_url(
|
answer_url = answer_url(
|
||||||
id: answer.id,
|
id: answer.id,
|
||||||
username: answer.user.screen_name,
|
username: answer.user.screen_name,
|
||||||
host: APP_CONFIG['hostname'],
|
host: APP_CONFIG["hostname"],
|
||||||
protocol: (APP_CONFIG['https'] ? :https : :http)
|
protocol: (APP_CONFIG["https"] ? :https : :http),
|
||||||
)
|
)
|
||||||
|
|
||||||
"#{answer.content}\n\n[Smile or comment on the answer here](#{answer_url})"
|
"#{answer.content}\n\n[Smile or comment on the answer here](#{answer_url})"
|
||||||
|
@ -26,8 +28,8 @@ module SocialHelper::TumblrMethods
|
||||||
answer_url = answer_url(
|
answer_url = answer_url(
|
||||||
id: answer.id,
|
id: answer.id,
|
||||||
username: answer.user.screen_name,
|
username: answer.user.screen_name,
|
||||||
host: APP_CONFIG['hostname'],
|
host: APP_CONFIG["hostname"],
|
||||||
protocol: (APP_CONFIG['https'] ? :https : :http)
|
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))}"
|
"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
|
module SocialHelper::TwitterMethods
|
||||||
include MarkdownHelper
|
include MarkdownHelper
|
||||||
|
|
||||||
def prepare_tweet(answer, post_tag = nil)
|
def prepare_tweet(answer, post_tag = nil, omit_url = false)
|
||||||
question_content = twitter_markdown answer.question.content.gsub(/\@(\w+)/, '\1')
|
question_content = twitter_markdown answer.question.content.gsub(/@(\w+)/, '\1')
|
||||||
original_question_length = question_content.length
|
original_question_length = question_content.length
|
||||||
answer_content = twitter_markdown answer.content
|
answer_content = twitter_markdown answer.content
|
||||||
original_answer_length = answer_content.length
|
original_answer_length = answer_content.length
|
||||||
|
|
||||||
|
unless omit_url
|
||||||
answer_url = answer_url(
|
answer_url = answer_url(
|
||||||
id: answer.id,
|
id: answer.id,
|
||||||
username: answer.user.screen_name,
|
username: answer.user.screen_name,
|
||||||
host: APP_CONFIG['hostname'],
|
host: APP_CONFIG["hostname"],
|
||||||
protocol: (APP_CONFIG['https'] ? :https : :http)
|
protocol: (APP_CONFIG["https"] ? :https : :http),
|
||||||
)
|
)
|
||||||
|
end
|
||||||
|
|
||||||
parsed_tweet = { :valid => false }
|
parsed_tweet = { valid: false }
|
||||||
tweet_text = ""
|
tweet_text = ""
|
||||||
|
|
||||||
until parsed_tweet[:valid]
|
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}"
|
shortened_answer = "#{answer_content[0..123]}#{'…' if original_answer_length > [124, answer_content.length].min}"
|
||||||
components = [
|
components = [
|
||||||
shortened_question,
|
shortened_question,
|
||||||
'—',
|
"—",
|
||||||
shortened_answer,
|
shortened_answer,
|
||||||
post_tag,
|
post_tag,
|
||||||
answer_url
|
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]
|
question_content = question_content[0..-2]
|
||||||
answer_content = answer_content[0..-2]
|
answer_content = answer_content[0..-2]
|
||||||
|
|
|
@ -25,7 +25,7 @@ module ThemeHelper
|
||||||
"input_color" => "input-bg",
|
"input_color" => "input-bg",
|
||||||
"input_text" => "input-text",
|
"input_text" => "input-text",
|
||||||
"input_placeholder" => "input-placeholder",
|
"input_placeholder" => "input-placeholder",
|
||||||
"muted_text" => "muted-text"
|
"muted_text" => "muted-text",
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def render_theme
|
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';
|
import { Controller } from '@hotwired/stimulus';
|
||||||
|
|
||||||
export default class extends Controller {
|
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 twitterTarget: HTMLAnchorElement;
|
||||||
|
declare readonly blueskyTarget: HTMLAnchorElement;
|
||||||
declare readonly tumblrTarget: HTMLAnchorElement;
|
declare readonly tumblrTarget: HTMLAnchorElement;
|
||||||
declare readonly telegramTarget: HTMLAnchorElement;
|
declare readonly telegramTarget: HTMLAnchorElement;
|
||||||
declare readonly customTarget: HTMLAnchorElement;
|
declare readonly customTarget: HTMLAnchorElement;
|
||||||
|
declare readonly otherTarget: HTMLButtonElement;
|
||||||
|
declare readonly clipboardTarget: HTMLButtonElement;
|
||||||
declare readonly hasCustomTarget: boolean;
|
declare readonly hasCustomTarget: boolean;
|
||||||
|
|
||||||
static values = {
|
static values = {
|
||||||
|
@ -20,8 +23,11 @@ export default class extends Controller {
|
||||||
connect(): void {
|
connect(): void {
|
||||||
if (this.autoCloseValue) {
|
if (this.autoCloseValue) {
|
||||||
this.twitterTarget.addEventListener('click', () => this.close());
|
this.twitterTarget.addEventListener('click', () => this.close());
|
||||||
|
this.blueskyTarget.addEventListener('click', () => this.close());
|
||||||
this.tumblrTarget.addEventListener('click', () => this.close());
|
this.tumblrTarget.addEventListener('click', () => this.close());
|
||||||
this.telegramTarget.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) {
|
if (this.hasCustomTarget) {
|
||||||
this.customTarget.addEventListener('click', () => this.close());
|
this.customTarget.addEventListener('click', () => this.close());
|
||||||
|
@ -37,6 +43,7 @@ export default class extends Controller {
|
||||||
this.element.classList.remove('d-none');
|
this.element.classList.remove('d-none');
|
||||||
|
|
||||||
this.twitterTarget.href = this.configValue['twitter'];
|
this.twitterTarget.href = this.configValue['twitter'];
|
||||||
|
this.blueskyTarget.href = this.configValue['bluesky'];
|
||||||
this.tumblrTarget.href = this.configValue['tumblr'];
|
this.tumblrTarget.href = this.configValue['tumblr'];
|
||||||
this.telegramTarget.href = this.configValue['telegram'];
|
this.telegramTarget.href = this.configValue['telegram'];
|
||||||
|
|
||||||
|
@ -48,4 +55,12 @@ export default class extends Controller {
|
||||||
close(): void {
|
close(): void {
|
||||||
(this.element.closest(".inbox-entry")).remove();
|
(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 {
|
export function commentHotkeyHandler(event: Event): void {
|
||||||
const button = event.target as HTMLButtonElement;
|
const button = event.target as HTMLButtonElement;
|
||||||
const id = button.dataset.aId;
|
const id = button.dataset.aId;
|
||||||
|
const answerbox = button.closest('.answerbox');
|
||||||
|
|
||||||
document.querySelector(`#ab-comments-section-${id}`).classList.remove('d-none');
|
if (answerbox !== null) {
|
||||||
document.querySelector<HTMLElement>(`[name="ab-comment-new"][data-a-id="${id}"]`).focus();
|
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 { commentDestroyHandler } from "./destroy";
|
||||||
import { commentComposeEnd, commentComposeStart, commentCreateClickHandler, commentCreateKeyboardHandler } from "./new";
|
import { commentComposeEnd, commentComposeStart, commentCreateClickHandler, commentCreateKeyboardHandler } from "./new";
|
||||||
import { commentReportHandler } from "./report";
|
import { commentReportHandler } from "./report";
|
||||||
import { commentSmileHandler } from "./smile";
|
|
||||||
import { commentToggleHandler } from "./toggle";
|
import { commentToggleHandler } from "./toggle";
|
||||||
import { commentHotkeyHandler } from "retrospring/features/answerbox/comment/hotkey";
|
import { commentHotkeyHandler } from "retrospring/features/answerbox/comment/hotkey";
|
||||||
|
|
||||||
|
@ -10,7 +9,6 @@ export default (): void => {
|
||||||
registerEvents([
|
registerEvents([
|
||||||
{ type: 'click', target: '[name=ab-comments]', handler: commentToggleHandler, global: true },
|
{ 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-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-report]', handler: commentReportHandler, global: true },
|
||||||
{ type: 'click', target: '[data-action=ab-comment-destroy]', handler: commentDestroyHandler, global: true },
|
{ type: 'click', target: '[data-action=ab-comment-destroy]', handler: commentDestroyHandler, global: true },
|
||||||
{ type: 'compositionstart', target: '[name=ab-comment-new]', handler: commentComposeStart, global: true },
|
{ type: '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 = '';
|
input.value = '';
|
||||||
counter.innerHTML = String(512);
|
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);
|
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 {
|
export function commentToggleHandler(event: Event): void {
|
||||||
const button = event.target as HTMLButtonElement;
|
const button = event.target as HTMLButtonElement;
|
||||||
const id = button.dataset.aId;
|
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 registerAnswerboxCommentEvents from './comment';
|
||||||
import { answerboxDestroyHandler } from './destroy';
|
import { answerboxDestroyHandler } from './destroy';
|
||||||
import { answerboxReportHandler } from './report';
|
import { answerboxReportHandler } from './report';
|
||||||
import { shareEventHandler } from './share';
|
|
||||||
import { answerboxSmileHandler } from './smile';
|
|
||||||
import { answerboxSubscribeHandler } from './subscribe';
|
|
||||||
|
|
||||||
export default (): void => {
|
export default (): void => {
|
||||||
registerEvents([
|
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-report]', handler: answerboxReportHandler, global: true },
|
||||||
{ type: 'click', target: '[data-action=ab-destroy]', handler: answerboxDestroyHandler, global: true },
|
{ type: 'click', target: '[data-action=ab-destroy]', handler: answerboxDestroyHandler, global: true },
|
||||||
{ type: 'click', target: '[name=ab-smile]', handler: answerboxSmileHandler, global: true }
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
registerAnswerboxCommentEvents();
|
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);
|
updateDeleteButton(false);
|
||||||
showNotification(data.message);
|
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');
|
const sharing = inboxEntry.querySelector<HTMLElement>('.inbox-entry__sharing');
|
||||||
if (sharing != null) {
|
if (sharing != null) {
|
||||||
sharing.dataset.inboxSharingConfigValue = JSON.stringify(data.sharing);
|
sharing.dataset.inboxSharingConfigValue = JSON.stringify(data.sharing);
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { showNotification, showErrorNotification } from 'utilities/notifications
|
||||||
|
|
||||||
export function deleteEntryHandler(event: Event): void {
|
export function deleteEntryHandler(event: Event): void {
|
||||||
const element: HTMLButtonElement = event.target as HTMLButtonElement;
|
const element: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||||
element.disabled = true;
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
id: element.getAttribute('data-ib-id')
|
id: element.getAttribute('data-ib-id')
|
||||||
|
@ -22,11 +21,8 @@ export function deleteEntryHandler(event: Event): void {
|
||||||
confirmButtonText: I18n.translate('voc.delete'),
|
confirmButtonText: I18n.translate('voc.delete'),
|
||||||
cancelButtonText: I18n.translate('voc.cancel'),
|
cancelButtonText: I18n.translate('voc.cancel'),
|
||||||
closeOnConfirm: true
|
closeOnConfirm: true
|
||||||
}, (returnValue) => {
|
}, () => {
|
||||||
if (returnValue === false) {
|
element.disabled = true;
|
||||||
element.disabled = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
post('/ajax/delete_inbox', {
|
post('/ajax/delete_inbox', {
|
||||||
body: data,
|
body: data,
|
||||||
|
@ -44,6 +40,7 @@ export function deleteEntryHandler(event: Event): void {
|
||||||
(inboxEntry as HTMLElement).remove();
|
(inboxEntry as HTMLElement).remove();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
element.disabled = false;
|
||||||
console.log(err);
|
console.log(err);
|
||||||
showErrorNotification(I18n.translate('frontend.error.message'));
|
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@ export default (): void => {
|
||||||
cheet('up up down down left right left right b a', () => {
|
cheet('up up down down left right left right b a', () => {
|
||||||
document.body.classList.add('fa-spin');
|
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 = ':^)';
|
element.innerText = ':^)';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,8 @@ export function questionboxAllHandler(event: Event): void {
|
||||||
body: {
|
body: {
|
||||||
rcpt: 'followers',
|
rcpt: 'followers',
|
||||||
question: document.querySelector<HTMLInputElement>('textarea[name=qb-all-question]').value,
|
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'
|
contentType: 'application/json'
|
||||||
})
|
})
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue