Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `.github/workflows/build-image.yml`: Fix erroneous deletion in a previous merge. - `Gemfile`: Conflict caused by glitch-soc-only hCaptcha dependency - `app/controllers/auth/sessions_controller.rb`: Minor conflict due to glitch-soc's theming system. - `app/controllers/filters_controller.rb`: Minor conflict due to glitch-soc's theming system. - `app/serializers/rest/status_serializer.rb`: Minor conflict due to glitch-soc having an extra `local_only` propertyremotes/1703361221475462875/rebase/4.0.0rc1
commit
fe5f6bc7ed
|
@ -133,6 +133,12 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
command: ./bin/rails tests:migrations:populate_v2_4
|
command: ./bin/rails tests:migrations:populate_v2_4
|
||||||
name: Populate database with test data
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20180707154237
|
||||||
|
name: Run migrations up to v2.4.3
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2_4_3
|
||||||
|
name: Populate database with test data
|
||||||
- run:
|
- run:
|
||||||
command: ./bin/rails db:migrate
|
command: ./bin/rails db:migrate
|
||||||
name: Run all remaining migrations
|
name: Run all remaining migrations
|
||||||
|
@ -167,14 +173,22 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
command: ./bin/rails tests:migrations:populate_v2_4
|
command: ./bin/rails tests:migrations:populate_v2_4
|
||||||
name: Populate database with test data
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20180707154237
|
||||||
|
name: Run migrations up to v2.4.3
|
||||||
|
environment:
|
||||||
|
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2_4_3
|
||||||
|
name: Populate database with test data
|
||||||
- run:
|
- run:
|
||||||
command: ./bin/rails db:migrate
|
command: ./bin/rails db:migrate
|
||||||
name: Run all pre-deployment migrations
|
name: Run all remaining pre-deployment migrations
|
||||||
environment:
|
environment:
|
||||||
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||||
- run:
|
- run:
|
||||||
command: ./bin/rails db:migrate
|
command: ./bin/rails db:migrate
|
||||||
name: Run all post-deployment remaining migrations
|
name: Run all post-deployment migrations
|
||||||
- run:
|
- run:
|
||||||
command: ./bin/rails tests:migrations:check_database
|
command: ./bin/rails tests:migrations:check_database
|
||||||
name: Check migration result
|
name: Check migration result
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
name: Build container image
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/build-image.yml
|
||||||
|
- Dockerfile
|
||||||
|
jobs:
|
||||||
|
build-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: docker/setup-qemu-action@v2
|
||||||
|
- uses: docker/setup-buildx-action@v2
|
||||||
|
- uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
- uses: docker/metadata-action@v4
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: ghcr.io/${{ github.repository_owner }}/mastodon
|
||||||
|
flavor: |
|
||||||
|
latest=auto
|
||||||
|
tags: |
|
||||||
|
type=edge,branch=main
|
||||||
|
type=match,pattern=v(.*),group=0
|
||||||
|
type=ref,event=pr
|
||||||
|
- uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:latest
|
||||||
|
cache-to: type=inline
|
|
@ -5,7 +5,7 @@ SHELL ["/bin/bash", "-c"]
|
||||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||||
|
|
||||||
# Install Node v16 (LTS)
|
# Install Node v16 (LTS)
|
||||||
ENV NODE_VER="16.14.2"
|
ENV NODE_VER="16.15.1"
|
||||||
RUN ARCH= && \
|
RUN ARCH= && \
|
||||||
dpkgArch="$(dpkg --print-architecture)" && \
|
dpkgArch="$(dpkg --print-architecture)" && \
|
||||||
case "${dpkgArch##*-}" in \
|
case "${dpkgArch##*-}" in \
|
||||||
|
|
5
Gemfile
5
Gemfile
|
@ -13,7 +13,7 @@ gem 'thor', '~> 1.2'
|
||||||
gem 'rack', '~> 2.2.3'
|
gem 'rack', '~> 2.2.3'
|
||||||
|
|
||||||
gem 'hamlit-rails', '~> 0.2'
|
gem 'hamlit-rails', '~> 0.2'
|
||||||
gem 'pg', '~> 1.3'
|
gem 'pg', '~> 1.4'
|
||||||
gem 'makara', '~> 0.5'
|
gem 'makara', '~> 0.5'
|
||||||
gem 'pghero', '~> 2.8'
|
gem 'pghero', '~> 2.8'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
@ -53,7 +53,7 @@ gem 'fastimage'
|
||||||
gem 'hiredis', '~> 0.6'
|
gem 'hiredis', '~> 0.6'
|
||||||
gem 'redis-namespace', '~> 1.8'
|
gem 'redis-namespace', '~> 1.8'
|
||||||
gem 'htmlentities', '~> 4.3'
|
gem 'htmlentities', '~> 4.3'
|
||||||
gem 'http', '~> 5.0'
|
gem 'http', '~> 5.1'
|
||||||
gem 'http_accept_language', '~> 2.1'
|
gem 'http_accept_language', '~> 2.1'
|
||||||
gem 'httplog', '~> 1.5.0'
|
gem 'httplog', '~> 1.5.0'
|
||||||
gem 'idn-ruby', require: 'idn'
|
gem 'idn-ruby', require: 'idn'
|
||||||
|
@ -157,3 +157,4 @@ gem 'connection_pool', require: false
|
||||||
gem 'xorcist', '~> 1.1'
|
gem 'xorcist', '~> 1.1'
|
||||||
|
|
||||||
gem 'hcaptcha', '~> 7.1'
|
gem 'hcaptcha', '~> 7.1'
|
||||||
|
gem 'cocoon', '~> 1.2'
|
||||||
|
|
14
Gemfile.lock
14
Gemfile.lock
|
@ -163,6 +163,7 @@ GEM
|
||||||
elasticsearch-dsl
|
elasticsearch-dsl
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
|
cocoon (1.2.15)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
color_diff (0.1)
|
color_diff (0.1)
|
||||||
concurrent-ruby (1.1.10)
|
concurrent-ruby (1.1.10)
|
||||||
|
@ -293,12 +294,12 @@ GEM
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
html_tokenizer (0.0.7)
|
html_tokenizer (0.0.7)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (5.0.4)
|
http (5.1.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
http-cookie (~> 1.0)
|
http-cookie (~> 1.0)
|
||||||
http-form_data (~> 2.2)
|
http-form_data (~> 2.2)
|
||||||
llhttp-ffi (~> 0.4.0)
|
llhttp-ffi (~> 0.4.0)
|
||||||
http-cookie (1.0.4)
|
http-cookie (1.0.5)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http-form_data (2.3.0)
|
http-form_data (2.3.0)
|
||||||
http_accept_language (2.1.1)
|
http_accept_language (2.1.1)
|
||||||
|
@ -448,7 +449,7 @@ GEM
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.3.5)
|
pg (1.4.0)
|
||||||
pghero (2.8.3)
|
pghero (2.8.3)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
pkg-config (1.4.7)
|
pkg-config (1.4.7)
|
||||||
|
@ -679,7 +680,7 @@ GEM
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.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.1.0)
|
unicode-display_width (2.1.0)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
validate_email (0.1.6)
|
validate_email (0.1.6)
|
||||||
|
@ -749,6 +750,7 @@ DEPENDENCIES
|
||||||
charlock_holmes (~> 0.7.7)
|
charlock_holmes (~> 0.7.7)
|
||||||
chewy (~> 7.2)
|
chewy (~> 7.2)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
|
cocoon (~> 1.2)
|
||||||
color_diff (~> 0.1)
|
color_diff (~> 0.1)
|
||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
connection_pool
|
connection_pool
|
||||||
|
@ -771,7 +773,7 @@ DEPENDENCIES
|
||||||
hcaptcha (~> 7.1)
|
hcaptcha (~> 7.1)
|
||||||
hiredis (~> 0.6)
|
hiredis (~> 0.6)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
http (~> 5.0)
|
http (~> 5.1)
|
||||||
http_accept_language (~> 2.1)
|
http_accept_language (~> 2.1)
|
||||||
httplog (~> 1.5.0)
|
httplog (~> 1.5.0)
|
||||||
i18n-tasks (~> 1.0)
|
i18n-tasks (~> 1.0)
|
||||||
|
@ -799,7 +801,7 @@ DEPENDENCIES
|
||||||
omniauth-saml (~> 1.10)
|
omniauth-saml (~> 1.10)
|
||||||
ox (~> 2.14)
|
ox (~> 2.14)
|
||||||
parslet
|
parslet
|
||||||
pg (~> 1.3)
|
pg (~> 1.4)
|
||||||
pghero (~> 2.8)
|
pghero (~> 2.8)
|
||||||
pkg-config (~> 1.4)
|
pkg-config (~> 1.4)
|
||||||
posix-spawn
|
posix-spawn
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::DomainAllowsController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
include AccountableConcern
|
||||||
|
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_allows' }, only: [:index, :show]
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_allows' }, except: [:index, :show]
|
||||||
|
before_action :require_staff!
|
||||||
|
before_action :set_domain_allows, only: :index
|
||||||
|
before_action :set_domain_allow, only: [:show, :destroy]
|
||||||
|
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
PAGINATION_PARAMS = %i(limit).freeze
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize :domain_allow, :create?
|
||||||
|
|
||||||
|
@domain_allow = DomainAllow.find_by(resource_params)
|
||||||
|
|
||||||
|
if @domain_allow.nil?
|
||||||
|
@domain_allow = DomainAllow.create!(resource_params)
|
||||||
|
log_action :create, @domain_allow
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :domain_allow, :index?
|
||||||
|
render json: @domain_allows, each_serializer: REST::Admin::DomainAllowSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @domain_allow, :show?
|
||||||
|
render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @domain_allow, :destroy?
|
||||||
|
UnallowDomainService.new.call(@domain_allow)
|
||||||
|
log_action :destroy, @domain_allow
|
||||||
|
render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_domain_allows
|
||||||
|
@domain_allows = filtered_domain_allows.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_domain_allow
|
||||||
|
@domain_allow = DomainAllow.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def filtered_domain_allows
|
||||||
|
# TODO: no filtering yet
|
||||||
|
DomainAllow.all
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_admin_domain_allows_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_admin_domain_allows_url(pagination_params(min_id: pagination_since_id)) unless @domain_allows.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@domain_allows.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@domain_allows.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@domain_allows.size == limit_param(LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(:domain)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,50 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Filters::KeywordsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show]
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
before_action :set_keywords, only: :index
|
||||||
|
before_action :set_keyword, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @keywords, each_serializer: REST::FilterKeywordSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@keyword = current_account.custom_filters.find(params[:filter_id]).keywords.create!(resource_params)
|
||||||
|
|
||||||
|
render json: @keyword, serializer: REST::FilterKeywordSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @keyword, serializer: REST::FilterKeywordSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@keyword.update!(resource_params)
|
||||||
|
|
||||||
|
render json: @keyword, serializer: REST::FilterKeywordSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@keyword.destroy!
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_keywords
|
||||||
|
filter = current_account.custom_filters.includes(:keywords).find(params[:filter_id])
|
||||||
|
@keywords = filter.keywords
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_keyword
|
||||||
|
@keyword = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account: current_account }).find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(:keyword, :whole_word)
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,21 +8,32 @@ class Api::V1::FiltersController < Api::BaseController
|
||||||
before_action :set_filter, only: [:show, :update, :destroy]
|
before_action :set_filter, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: @filters, each_serializer: REST::FilterSerializer
|
render json: @filters, each_serializer: REST::V1::FilterSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@filter = current_account.custom_filters.create!(resource_params)
|
ApplicationRecord.transaction do
|
||||||
render json: @filter, serializer: REST::FilterSerializer
|
filter_category = current_account.custom_filters.create!(resource_params)
|
||||||
|
@filter = filter_category.keywords.create!(keyword_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: @filter, serializer: REST::V1::FilterSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @filter, serializer: REST::FilterSerializer
|
render json: @filter, serializer: REST::V1::FilterSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@filter.update!(resource_params)
|
ApplicationRecord.transaction do
|
||||||
render json: @filter, serializer: REST::FilterSerializer
|
@filter.update!(keyword_params)
|
||||||
|
@filter.custom_filter.assign_attributes(filter_params)
|
||||||
|
raise Mastodon::ValidationError, I18n.t('filters.errors.deprecated_api_multiple_keywords') if @filter.custom_filter.changed? && @filter.custom_filter.keywords.count > 1
|
||||||
|
|
||||||
|
@filter.custom_filter.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: @filter, serializer: REST::V1::FilterSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -33,14 +44,22 @@ class Api::V1::FiltersController < Api::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_filters
|
def set_filters
|
||||||
@filters = current_account.custom_filters
|
@filters = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account: current_account })
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_filter
|
def set_filter
|
||||||
@filter = current_account.custom_filters.find(params[:id])
|
@filter = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account: current_account }).find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
|
params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
resource_params.slice(:expires_in, :irreversible, :context)
|
||||||
|
end
|
||||||
|
|
||||||
|
def keyword_params
|
||||||
|
resource_params.slice(:phrase, :whole_word)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||||
def data_params
|
def data_params
|
||||||
return {} if params[:data].blank?
|
return {} if params[:data].blank?
|
||||||
|
|
||||||
params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
params.require(:data).permit(:policy, alerts: Notification::TYPES)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V2::FiltersController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show]
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_filters, only: :index
|
||||||
|
before_action :set_filter, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @filters, each_serializer: REST::FilterSerializer, rules_requested: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@filter = current_account.custom_filters.create!(resource_params)
|
||||||
|
|
||||||
|
render json: @filter, serializer: REST::FilterSerializer, rules_requested: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @filter, serializer: REST::FilterSerializer, rules_requested: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@filter.update!(resource_params)
|
||||||
|
|
||||||
|
render json: @filter, serializer: REST::FilterSerializer, rules_requested: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@filter.destroy!
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_filters
|
||||||
|
@filters = current_account.custom_filters.includes(:keywords)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_filter
|
||||||
|
@filter = current_account.custom_filters.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,12 +8,18 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
skip_before_action :update_user_sign_in
|
skip_before_action :update_user_sign_in
|
||||||
|
|
||||||
prepend_before_action :set_pack
|
prepend_before_action :set_pack
|
||||||
|
prepend_before_action :check_suspicious!, only: [:create]
|
||||||
|
|
||||||
include TwoFactorAuthenticationConcern
|
include TwoFactorAuthenticationConcern
|
||||||
|
|
||||||
before_action :set_instance_presenter, only: [:new]
|
before_action :set_instance_presenter, only: [:new]
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
|
def check_suspicious!
|
||||||
|
user = find_user
|
||||||
|
@login_is_suspicious = suspicious_sign_in?(user) unless user.nil?
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
super do |resource|
|
super do |resource|
|
||||||
# We only need to call this if this hasn't already been
|
# We only need to call this if this hasn't already been
|
||||||
|
@ -148,7 +154,7 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
user_agent: request.user_agent
|
user_agent: request.user_agent
|
||||||
)
|
)
|
||||||
|
|
||||||
UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if suspicious_sign_in?(user)
|
UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if @login_is_suspicious
|
||||||
end
|
end
|
||||||
|
|
||||||
def suspicious_sign_in?(user)
|
def suspicious_sign_in?(user)
|
||||||
|
|
|
@ -4,17 +4,17 @@ class FiltersController < ApplicationController
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_filters, only: :index
|
|
||||||
before_action :set_filter, only: [:edit, :update, :destroy]
|
before_action :set_filter, only: [:edit, :update, :destroy]
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@filters = current_account.custom_filters.order(:phrase)
|
@filters = current_account.custom_filters.includes(:keywords).order(:phrase)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@filter = current_account.custom_filters.build
|
@filter = current_account.custom_filters.build(action: :warn)
|
||||||
|
@filter.keywords.build
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -48,16 +48,12 @@ class FiltersController < ApplicationController
|
||||||
use_pack 'settings'
|
use_pack 'settings'
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_filters
|
|
||||||
@filters = current_account.custom_filters
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_filter
|
def set_filter
|
||||||
@filter = current_account.custom_filters.find(params[:id])
|
@filter = current_account.custom_filters.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:custom_filter).permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
|
params.require(:custom_filter).permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
|
|
|
@ -16,7 +16,11 @@ module RoutingHelper
|
||||||
def full_asset_url(source, **options)
|
def full_asset_url(source, **options)
|
||||||
source = ActionController::Base.helpers.asset_url(source, **options) unless use_storage?
|
source = ActionController::Base.helpers.asset_url(source, **options) unless use_storage?
|
||||||
|
|
||||||
URI.join(root_url, source).to_s
|
URI.join(asset_host, source).to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_host
|
||||||
|
Rails.configuration.action_controller.asset_host || root_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def full_pack_url(source, **options)
|
def full_pack_url(source, **options)
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import api from '../api';
|
|
||||||
|
|
||||||
export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
|
|
||||||
export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
|
|
||||||
export const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
|
|
||||||
|
|
||||||
export const fetchFilters = () => (dispatch, getState) => {
|
|
||||||
dispatch({
|
|
||||||
type: FILTERS_FETCH_REQUEST,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
api(getState)
|
|
||||||
.get('/api/v1/filters')
|
|
||||||
.then(({ data }) => dispatch({
|
|
||||||
type: FILTERS_FETCH_SUCCESS,
|
|
||||||
filters: data,
|
|
||||||
skipLoading: true,
|
|
||||||
}))
|
|
||||||
.catch(err => dispatch({
|
|
||||||
type: FILTERS_FETCH_FAIL,
|
|
||||||
err,
|
|
||||||
skipLoading: true,
|
|
||||||
skipAlert: true,
|
|
||||||
}));
|
|
||||||
};
|
|
|
@ -5,6 +5,7 @@ export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
|
||||||
export const STATUS_IMPORT = 'STATUS_IMPORT';
|
export const STATUS_IMPORT = 'STATUS_IMPORT';
|
||||||
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
|
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
|
||||||
export const POLLS_IMPORT = 'POLLS_IMPORT';
|
export const POLLS_IMPORT = 'POLLS_IMPORT';
|
||||||
|
export const FILTERS_IMPORT = 'FILTERS_IMPORT';
|
||||||
|
|
||||||
function pushUnique(array, object) {
|
function pushUnique(array, object) {
|
||||||
if (array.every(element => element.id !== object.id)) {
|
if (array.every(element => element.id !== object.id)) {
|
||||||
|
@ -28,6 +29,10 @@ export function importStatuses(statuses) {
|
||||||
return { type: STATUSES_IMPORT, statuses };
|
return { type: STATUSES_IMPORT, statuses };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function importFilters(filters) {
|
||||||
|
return { type: FILTERS_IMPORT, filters };
|
||||||
|
}
|
||||||
|
|
||||||
export function importPolls(polls) {
|
export function importPolls(polls) {
|
||||||
return { type: POLLS_IMPORT, polls };
|
return { type: POLLS_IMPORT, polls };
|
||||||
}
|
}
|
||||||
|
@ -61,11 +66,16 @@ export function importFetchedStatuses(statuses) {
|
||||||
const accounts = [];
|
const accounts = [];
|
||||||
const normalStatuses = [];
|
const normalStatuses = [];
|
||||||
const polls = [];
|
const polls = [];
|
||||||
|
const filters = [];
|
||||||
|
|
||||||
function processStatus(status) {
|
function processStatus(status) {
|
||||||
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
|
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
|
||||||
pushUnique(accounts, status.account);
|
pushUnique(accounts, status.account);
|
||||||
|
|
||||||
|
if (status.filtered) {
|
||||||
|
status.filtered.forEach(result => pushUnique(filters, result.filter));
|
||||||
|
}
|
||||||
|
|
||||||
if (status.reblog && status.reblog.id) {
|
if (status.reblog && status.reblog.id) {
|
||||||
processStatus(status.reblog);
|
processStatus(status.reblog);
|
||||||
}
|
}
|
||||||
|
@ -80,6 +90,7 @@ export function importFetchedStatuses(statuses) {
|
||||||
dispatch(importPolls(polls));
|
dispatch(importPolls(polls));
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
dispatch(importStatuses(normalStatuses));
|
dispatch(importStatuses(normalStatuses));
|
||||||
|
dispatch(importFilters(filters));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,14 @@ export function normalizeAccount(account) {
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeFilterResult(result) {
|
||||||
|
const normalResult = { ...result };
|
||||||
|
|
||||||
|
normalResult.filter = normalResult.filter.id;
|
||||||
|
|
||||||
|
return normalResult;
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeStatus(status, normalOldStatus) {
|
export function normalizeStatus(status, normalOldStatus) {
|
||||||
const normalStatus = { ...status };
|
const normalStatus = { ...status };
|
||||||
normalStatus.account = status.account.id;
|
normalStatus.account = status.account.id;
|
||||||
|
@ -54,6 +62,10 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||||
normalStatus.poll = status.poll.id;
|
normalStatus.poll = status.poll.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.filtered) {
|
||||||
|
normalStatus.filtered = status.filtered.map(normalizeFilterResult);
|
||||||
|
}
|
||||||
|
|
||||||
// Only calculate these values when status first encountered and
|
// Only calculate these values when status first encountered and
|
||||||
// when the underlying values change. Otherwise keep the ones
|
// when the underlying values change. Otherwise keep the ones
|
||||||
// already in the reducer
|
// already in the reducer
|
||||||
|
|
|
@ -12,10 +12,8 @@ import { saveSettings } from './settings';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { unescapeHTML } from '../utils/html';
|
import { unescapeHTML } from '../utils/html';
|
||||||
import { getFiltersRegex } from '../selectors';
|
|
||||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||||
import compareId from 'mastodon/compare_id';
|
import compareId from 'mastodon/compare_id';
|
||||||
import { searchTextFromRawStatus } from 'mastodon/actions/importer/normalizer';
|
|
||||||
import { requestNotificationPermission } from '../utils/notifications';
|
import { requestNotificationPermission } from '../utils/notifications';
|
||||||
|
|
||||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||||
|
@ -62,20 +60,17 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
|
const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
|
||||||
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
||||||
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
||||||
const filters = getFiltersRegex(getState(), { contextType: 'notifications' });
|
|
||||||
|
|
||||||
let filtered = false;
|
let filtered = false;
|
||||||
|
|
||||||
if (['mention', 'status'].includes(notification.type)) {
|
if (['mention', 'status'].includes(notification.type) && notification.status.filtered) {
|
||||||
const dropRegex = filters[0];
|
const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications'));
|
||||||
const regex = filters[1];
|
|
||||||
const searchIndex = searchTextFromRawStatus(notification.status);
|
|
||||||
|
|
||||||
if (dropRegex && dropRegex.test(searchIndex)) {
|
if (filters.some(result => result.filter.filter_action === 'hide')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
filtered = regex && regex.test(searchIndex);
|
filtered = filters.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['follow_request'].includes(notification.type)) {
|
if (['follow_request'].includes(notification.type)) {
|
||||||
|
@ -91,6 +86,10 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
dispatch(importFetchedStatus(notification.status));
|
dispatch(importFetchedStatus(notification.status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (notification.report) {
|
||||||
|
dispatch(importFetchedAccount(notification.report.target_account));
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: NOTIFICATIONS_UPDATE,
|
type: NOTIFICATIONS_UPDATE,
|
||||||
notification,
|
notification,
|
||||||
|
@ -134,6 +133,7 @@ const excludeTypesFromFilter = filter => {
|
||||||
'status',
|
'status',
|
||||||
'update',
|
'update',
|
||||||
'admin.sign_up',
|
'admin.sign_up',
|
||||||
|
'admin.report',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return allTypes.filterNot(item => item === filter).toJS();
|
return allTypes.filterNot(item => item === filter).toJS();
|
||||||
|
@ -179,6 +179,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
|
||||||
|
|
||||||
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||||
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||||
|
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||||
|
|
||||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
|
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
|
||||||
fetchRelatedRelationships(dispatch, response.data);
|
fetchRelatedRelationships(dispatch, response.data);
|
||||||
|
|
|
@ -21,7 +21,6 @@ import {
|
||||||
updateReaction as updateAnnouncementsReaction,
|
updateReaction as updateAnnouncementsReaction,
|
||||||
deleteAnnouncement,
|
deleteAnnouncement,
|
||||||
} from './announcements';
|
} from './announcements';
|
||||||
import { fetchFilters } from './filters';
|
|
||||||
import { getLocale } from '../locales';
|
import { getLocale } from '../locales';
|
||||||
|
|
||||||
const { messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
|
@ -97,9 +96,6 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
case 'conversation':
|
case 'conversation':
|
||||||
dispatch(updateConversations(JSON.parse(data.payload)));
|
dispatch(updateConversations(JSON.parse(data.payload)));
|
||||||
break;
|
break;
|
||||||
case 'filters_changed':
|
|
||||||
dispatch(fetchFilters());
|
|
||||||
break;
|
|
||||||
case 'announcement':
|
case 'announcement':
|
||||||
dispatch(updateAnnouncements(JSON.parse(data.payload)));
|
dispatch(updateAnnouncements(JSON.parse(data.payload)));
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -132,8 +132,16 @@ export default class IconButton extends React.PureComponent {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
contents = (
|
return (
|
||||||
<a href={href} target='_blank' rel='noopener noreferrer'>
|
<a
|
||||||
|
href={href}
|
||||||
|
aria-label={title}
|
||||||
|
title={title}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
className={classes}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
{contents}
|
{contents}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
@ -116,6 +116,7 @@ class Status extends ImmutablePureComponent {
|
||||||
state = {
|
state = {
|
||||||
showMedia: defaultMediaVisibility(this.props.status),
|
showMedia: defaultMediaVisibility(this.props.status),
|
||||||
statusId: undefined,
|
statusId: undefined,
|
||||||
|
forceFilter: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
static getDerivedStateFromProps(nextProps, prevState) {
|
||||||
|
@ -277,6 +278,15 @@ class Status extends ImmutablePureComponent {
|
||||||
this.handleToggleMediaVisibility();
|
this.handleToggleMediaVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleUnfilterClick = e => {
|
||||||
|
this.setState({ forceFilter: false });
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFilterClick = () => {
|
||||||
|
this.setState({ forceFilter: true });
|
||||||
|
}
|
||||||
|
|
||||||
_properStatus () {
|
_properStatus () {
|
||||||
const { status } = this.props;
|
const { status } = this.props;
|
||||||
|
|
||||||
|
@ -328,7 +338,8 @@ class Status extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
|
const matchedFilters = status.get('filtered') || status.getIn(['reblog', 'filtered']);
|
||||||
|
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
|
||||||
const minHandlers = this.props.muted ? {} : {
|
const minHandlers = this.props.muted ? {} : {
|
||||||
moveUp: this.handleHotkeyMoveUp,
|
moveUp: this.handleHotkeyMoveUp,
|
||||||
moveDown: this.handleHotkeyMoveDown,
|
moveDown: this.handleHotkeyMoveDown,
|
||||||
|
@ -337,7 +348,11 @@ class Status extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={minHandlers}>
|
<HotKeys handlers={minHandlers}>
|
||||||
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
|
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
|
||||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
|
||||||
|
{' '}
|
||||||
|
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
|
||||||
|
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
@ -496,7 +511,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
{media}
|
{media}
|
||||||
|
|
||||||
<StatusActionBar scrollKey={scrollKey} status={status} account={account} {...other} />
|
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters && this.handleFilterClick} {...other} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
|
|
|
@ -38,6 +38,7 @@ const messages = defineMessages({
|
||||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
||||||
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
||||||
|
hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
|
||||||
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
|
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
|
||||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
|
@ -76,6 +77,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
onMuteConversation: PropTypes.func,
|
onMuteConversation: PropTypes.func,
|
||||||
onPin: PropTypes.func,
|
onPin: PropTypes.func,
|
||||||
onBookmark: PropTypes.func,
|
onBookmark: PropTypes.func,
|
||||||
|
onFilter: PropTypes.func,
|
||||||
withDismiss: PropTypes.bool,
|
withDismiss: PropTypes.bool,
|
||||||
withCounters: PropTypes.bool,
|
withCounters: PropTypes.bool,
|
||||||
scrollKey: PropTypes.string,
|
scrollKey: PropTypes.string,
|
||||||
|
@ -207,6 +209,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
this.props.onMuteConversation(this.props.status);
|
this.props.onMuteConversation(this.props.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleFilter = () => {
|
||||||
|
this.props.onFilter();
|
||||||
|
}
|
||||||
|
|
||||||
handleCopy = () => {
|
handleCopy = () => {
|
||||||
const url = this.props.status.get('url');
|
const url = this.props.status.get('url');
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
|
@ -226,6 +232,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleFilterClick = () => {
|
||||||
|
this.props.onFilter();
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
|
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
|
||||||
|
|
||||||
|
@ -329,6 +340,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
|
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const filterButton = this.props.onFilter && (
|
||||||
|
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleFilterClick} />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||||
|
@ -337,6 +352,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
|
{filterButton}
|
||||||
|
|
||||||
<div className='status__action-bar-dropdown'>
|
<div className='status__action-bar-dropdown'>
|
||||||
<DropdownMenuContainer
|
<DropdownMenuContainer
|
||||||
scrollKey={scrollKey}
|
scrollKey={scrollKey}
|
||||||
|
|
|
@ -178,6 +178,19 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isStaff && (
|
||||||
|
<div role='group' aria-labelledby='notifications-admin-report'>
|
||||||
|
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></span>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.report']} onChange={onChange} label={alertStr} />
|
||||||
|
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'admin.report']} onChange={this.onPushChange} label={pushStr} />}
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'admin.report']} onChange={onChange} label={showStr} />
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'admin.report']} onChange={onChange} label={soundStr} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
import StatusContainer from 'mastodon/containers/status_container';
|
import StatusContainer from 'mastodon/containers/status_container';
|
||||||
import AccountContainer from 'mastodon/containers/account_container';
|
import AccountContainer from 'mastodon/containers/account_container';
|
||||||
|
import Report from './report';
|
||||||
import FollowRequestContainer from '../containers/follow_request_container';
|
import FollowRequestContainer from '../containers/follow_request_container';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import Permalink from 'mastodon/components/permalink';
|
import Permalink from 'mastodon/components/permalink';
|
||||||
|
@ -21,6 +22,7 @@ const messages = defineMessages({
|
||||||
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
||||||
update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
|
update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
|
||||||
adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' },
|
adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' },
|
||||||
|
adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationForScreenReader = (intl, message, timestamp) => {
|
const notificationForScreenReader = (intl, message, timestamp) => {
|
||||||
|
@ -367,6 +369,32 @@ class Notification extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderAdminReport (notification, account, link) {
|
||||||
|
const { intl, unread, report } = this.props;
|
||||||
|
|
||||||
|
const targetAccount = report.get('target_account');
|
||||||
|
const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') };
|
||||||
|
const targetLink = <bdi><Permalink className='notification__display-name' href={targetAccount.get('url')} title={targetAccount.get('acct')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
<div className={classNames('notification notification-admin-report focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminReport, { name: account.get('acct'), target: notification.getIn(['report', 'target_account', 'acct']) }), notification.get('created_at'))}>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<Icon id='flag' fixedWidth />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span title={notification.get('created_at')}>
|
||||||
|
<FormattedMessage id='notification.admin.report' defaultMessage='{name} reported {target}' values={{ name: link, target: targetLink }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Report account={account} report={notification.get('report')} hidden={this.props.hidden} />
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { notification } = this.props;
|
const { notification } = this.props;
|
||||||
const account = notification.get('account');
|
const account = notification.get('account');
|
||||||
|
@ -392,6 +420,8 @@ class Notification extends ImmutablePureComponent {
|
||||||
return this.renderPoll(notification, account);
|
return this.renderPoll(notification, account);
|
||||||
case 'admin.sign_up':
|
case 'admin.sign_up':
|
||||||
return this.renderAdminSignUp(notification, account, link);
|
return this.renderAdminSignUp(notification, account, link);
|
||||||
|
case 'admin.report':
|
||||||
|
return this.renderAdminReport(notification, account, link);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import AvatarOverlay from 'mastodon/components/avatar_overlay';
|
||||||
|
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
openReport: { id: 'report_notification.open', defaultMessage: 'Open report' },
|
||||||
|
other: { id: 'report_notification.categories.other', defaultMessage: 'Other' },
|
||||||
|
spam: { id: 'report_notification.categories.spam', defaultMessage: 'Spam' },
|
||||||
|
violation: { id: 'report_notification.categories.violation', defaultMessage: 'Rule violation' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class Report extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
report: ImmutablePropTypes.map.isRequired,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, hidden, report, account } = this.props;
|
||||||
|
|
||||||
|
if (!report) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{report.get('id')}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='notification__report'>
|
||||||
|
<div className='notification__report__avatar'>
|
||||||
|
<AvatarOverlay account={report.get('target_account')} friend={account} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='notification__report__details'>
|
||||||
|
<div>
|
||||||
|
<RelativeTimestamp timestamp={report.get('created_at')} short={false} /> · <FormattedMessage id='report_notification.attached_statuses' defaultMessage='{count, plural, one {{count} post} other {{count} posts}} attached' values={{ count: report.get('status_ids').size }} />
|
||||||
|
<br />
|
||||||
|
<strong>{intl.formatMessage(messages[report.get('category')])}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='notification__report__actions'>
|
||||||
|
<a href={`/admin/reports/${report.get('id')}`} className='button' target='_blank' rel='noopener noreferrer'>{intl.formatMessage(messages.openReport)}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeGetNotification, makeGetStatus } from '../../../selectors';
|
import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
|
||||||
import Notification from '../components/notification';
|
import Notification from '../components/notification';
|
||||||
import { initBoostModal } from '../../../actions/boosts';
|
import { initBoostModal } from '../../../actions/boosts';
|
||||||
import { mentionCompose } from '../../../actions/compose';
|
import { mentionCompose } from '../../../actions/compose';
|
||||||
|
@ -18,12 +18,14 @@ import { boostModal } from '../../../initial_state';
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getNotification = makeGetNotification();
|
const getNotification = makeGetNotification();
|
||||||
const getStatus = makeGetStatus();
|
const getStatus = makeGetStatus();
|
||||||
|
const getReport = makeGetReport();
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
const notification = getNotification(state, props.notification, props.accountId);
|
const notification = getNotification(state, props.notification, props.accountId);
|
||||||
return {
|
return {
|
||||||
notification: notification,
|
notification: notification,
|
||||||
status: notification.get('status') ? getStatus(state, { id: notification.get('status') }) : null,
|
status: notification.get('status') ? getStatus(state, { id: notification.get('status') }) : null,
|
||||||
|
report: notification.get('report') ? getReport(state, notification.get('report'), notification.getIn(['report', 'target_account', 'id'])) : null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { debounce } from 'lodash';
|
||||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import { expandNotifications } from '../../actions/notifications';
|
import { expandNotifications } from '../../actions/notifications';
|
||||||
import { fetchFilters } from '../../actions/filters';
|
|
||||||
import { fetchRules } from '../../actions/rules';
|
import { fetchRules } from '../../actions/rules';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
||||||
|
@ -368,7 +367,7 @@ class UI extends React.PureComponent {
|
||||||
this.props.dispatch(fetchMarkers());
|
this.props.dispatch(fetchMarkers());
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
|
||||||
setTimeout(() => this.props.dispatch(fetchRules()), 3000);
|
setTimeout(() => this.props.dispatch(fetchRules()), 3000);
|
||||||
|
|
||||||
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
||||||
|
|
|
@ -2532,6 +2532,10 @@
|
||||||
{
|
{
|
||||||
"defaultMessage": "New sign-ups:",
|
"defaultMessage": "New sign-ups:",
|
||||||
"id": "notifications.column_settings.admin.sign_up"
|
"id": "notifications.column_settings.admin.sign_up"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "New reports:",
|
||||||
|
"id": "notifications.column_settings.admin.report"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/notifications/components/column_settings.json"
|
"path": "app/javascript/mastodon/features/notifications/components/column_settings.json"
|
||||||
|
@ -2625,6 +2629,10 @@
|
||||||
"defaultMessage": "{name} signed up",
|
"defaultMessage": "{name} signed up",
|
||||||
"id": "notification.admin.sign_up"
|
"id": "notification.admin.sign_up"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "{name} reported {target}",
|
||||||
|
"id": "notification.admin.report"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "{name} has requested to follow you",
|
"defaultMessage": "{name} has requested to follow you",
|
||||||
"id": "notification.follow_request"
|
"id": "notification.follow_request"
|
||||||
|
@ -2653,6 +2661,31 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/notifications/components/notifications_permission_banner.json"
|
"path": "app/javascript/mastodon/features/notifications/components/notifications_permission_banner.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Open report",
|
||||||
|
"id": "report_notification.open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Other",
|
||||||
|
"id": "report_notification.categories.other"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Spam",
|
||||||
|
"id": "report_notification.categories.spam"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Rule violation",
|
||||||
|
"id": "report_notification.categories.violation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "{count, plural, one {{count} post} other {{count} posts}} attached",
|
||||||
|
"id": "report_notification.attached_statuses"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/notifications/components/report.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -319,6 +319,7 @@
|
||||||
"navigation_bar.preferences": "Preferences",
|
"navigation_bar.preferences": "Preferences",
|
||||||
"navigation_bar.public_timeline": "Federated timeline",
|
"navigation_bar.public_timeline": "Federated timeline",
|
||||||
"navigation_bar.security": "Security",
|
"navigation_bar.security": "Security",
|
||||||
|
"notification.admin.report": "{name} reported {target}",
|
||||||
"notification.admin.sign_up": "{name} signed up",
|
"notification.admin.sign_up": "{name} signed up",
|
||||||
"notification.favourite": "{name} favourited your post",
|
"notification.favourite": "{name} favourited your post",
|
||||||
"notification.follow": "{name} followed you",
|
"notification.follow": "{name} followed you",
|
||||||
|
@ -331,6 +332,7 @@
|
||||||
"notification.update": "{name} edited a post",
|
"notification.update": "{name} edited a post",
|
||||||
"notifications.clear": "Clear notifications",
|
"notifications.clear": "Clear notifications",
|
||||||
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
||||||
|
"notifications.column_settings.admin.report": "New reports:",
|
||||||
"notifications.column_settings.admin.sign_up": "New sign-ups:",
|
"notifications.column_settings.admin.sign_up": "New sign-ups:",
|
||||||
"notifications.column_settings.alert": "Desktop notifications",
|
"notifications.column_settings.alert": "Desktop notifications",
|
||||||
"notifications.column_settings.favourite": "Favourites:",
|
"notifications.column_settings.favourite": "Favourites:",
|
||||||
|
@ -436,6 +438,11 @@
|
||||||
"report.thanks.title_actionable": "Thanks for reporting, we'll look into this.",
|
"report.thanks.title_actionable": "Thanks for reporting, we'll look into this.",
|
||||||
"report.unfollow": "Unfollow @{name}",
|
"report.unfollow": "Unfollow @{name}",
|
||||||
"report.unfollow_explanation": "You are following this account. To not see their posts in your home feed anymore, unfollow them.",
|
"report.unfollow_explanation": "You are following this account. To not see their posts in your home feed anymore, unfollow them.",
|
||||||
|
"report_notification.attached_statuses": "{count, plural, one {{count} post} other {{count} posts}} attached",
|
||||||
|
"report_notification.categories.other": "Other",
|
||||||
|
"report_notification.categories.spam": "Spam",
|
||||||
|
"report_notification.categories.violation": "Rule violation",
|
||||||
|
"report_notification.open": "Open report",
|
||||||
"search.placeholder": "Search",
|
"search.placeholder": "Search",
|
||||||
"search_popout.search_format": "Advanced search format",
|
"search_popout.search_format": "Advanced search format",
|
||||||
"search_popout.tips.full_text": "Simple text returns posts you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns posts you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
|
|
|
@ -7,13 +7,13 @@
|
||||||
"account.block_domain": "Bloki domajnon {domain}",
|
"account.block_domain": "Bloki domajnon {domain}",
|
||||||
"account.blocked": "Blokita",
|
"account.blocked": "Blokita",
|
||||||
"account.browse_more_on_origin_server": "Vidi pli ĉe la originala profilo",
|
"account.browse_more_on_origin_server": "Vidi pli ĉe la originala profilo",
|
||||||
"account.cancel_follow_request": "Nuligi peton de sekvado",
|
"account.cancel_follow_request": "Nuligi la demandon de sekvado",
|
||||||
"account.direct": "Rekte mesaĝi @{name}",
|
"account.direct": "Rekte mesaĝi @{name}",
|
||||||
"account.disable_notifications": "Ĉesu sciigi min kiam @{name} mesaĝi",
|
"account.disable_notifications": "Ĉesu sciigi min kiam @{name} mesaĝi",
|
||||||
"account.domain_blocked": "Domajno blokita",
|
"account.domain_blocked": "Domajno blokita",
|
||||||
"account.edit_profile": "Redakti profilon",
|
"account.edit_profile": "Redakti la profilon",
|
||||||
"account.enable_notifications": "Sciigi min kiam @{name} mesaĝi",
|
"account.enable_notifications": "Sciigi min kiam @{name} mesaĝas",
|
||||||
"account.endorse": "Montri en profilo",
|
"account.endorse": "Rekomendi ĉe via profilo",
|
||||||
"account.follow": "Sekvi",
|
"account.follow": "Sekvi",
|
||||||
"account.followers": "Sekvantoj",
|
"account.followers": "Sekvantoj",
|
||||||
"account.followers.empty": "Ankoraŭ neniu sekvas tiun uzanton.",
|
"account.followers.empty": "Ankoraŭ neniu sekvas tiun uzanton.",
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
"account.following_counter": "{count, plural, one {{counter} Sekvato} other {{counter} Sekvatoj}}",
|
"account.following_counter": "{count, plural, one {{counter} Sekvato} other {{counter} Sekvatoj}}",
|
||||||
"account.follows.empty": "Tiu uzanto ankoraŭ ne sekvas iun.",
|
"account.follows.empty": "Tiu uzanto ankoraŭ ne sekvas iun.",
|
||||||
"account.follows_you": "Sekvas vin",
|
"account.follows_you": "Sekvas vin",
|
||||||
"account.hide_reblogs": "Kaŝi plusendojn de @{name}",
|
"account.hide_reblogs": "Kaŝi la plusendojn de @{name}",
|
||||||
"account.joined": "Kuniĝis {date}",
|
"account.joined": "Kuniĝis {date}",
|
||||||
"account.link_verified_on": "La posedanto de tiu ligilo estis kontrolita je {date}",
|
"account.link_verified_on": "La posedanto de tiu ligilo estis kontrolita je {date}",
|
||||||
"account.locked_info": "La privateco de tiu konto estas elektita kiel fermita. La posedanto povas mane akcepti tiun, kiu povas sekvi rin.",
|
"account.locked_info": "La privateco de tiu konto estas elektita kiel fermita. La posedanto povas mane akcepti tiun, kiu povas sekvi rin.",
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
"account.muted": "Silentigita",
|
"account.muted": "Silentigita",
|
||||||
"account.posts": "Mesaĝoj",
|
"account.posts": "Mesaĝoj",
|
||||||
"account.posts_with_replies": "Mesaĝoj kaj respondoj",
|
"account.posts_with_replies": "Mesaĝoj kaj respondoj",
|
||||||
"account.report": "Signali @{name}",
|
"account.report": "Raporti @{name}",
|
||||||
"account.requested": "Atendo de aprobo. Alklaku por nuligi peton de sekvado",
|
"account.requested": "Atendo de aprobo. Alklaku por nuligi peton de sekvado",
|
||||||
"account.share": "Kundividi la profilon de @{name}",
|
"account.share": "Kundividi la profilon de @{name}",
|
||||||
"account.show_reblogs": "Montri la plusendojn de @{name}",
|
"account.show_reblogs": "Montri la plusendojn de @{name}",
|
||||||
|
@ -42,62 +42,62 @@
|
||||||
"account.unblock": "Malbloki @{name}",
|
"account.unblock": "Malbloki @{name}",
|
||||||
"account.unblock_domain": "Malbloki {domain}",
|
"account.unblock_domain": "Malbloki {domain}",
|
||||||
"account.unblock_short": "Malbloki",
|
"account.unblock_short": "Malbloki",
|
||||||
"account.unendorse": "Ne montri en profilo",
|
"account.unendorse": "Ne rekomendi ĉe la profilo",
|
||||||
"account.unfollow": "Ne plu sekvi",
|
"account.unfollow": "Ne plu sekvi",
|
||||||
"account.unmute": "Malsilentigi @{name}",
|
"account.unmute": "Ne plu silentigi @{name}",
|
||||||
"account.unmute_notifications": "Malsilentigi sciigojn de @{name}",
|
"account.unmute_notifications": "Reebligi la sciigojn de @{name}",
|
||||||
"account.unmute_short": "Malsilentigi",
|
"account.unmute_short": "Ne plu silentigi",
|
||||||
"account_note.placeholder": "Alklaku por aldoni noton",
|
"account_note.placeholder": "Klaku por aldoni noton",
|
||||||
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
|
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
|
||||||
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
|
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
|
||||||
"admin.dashboard.retention.average": "Averaĝa",
|
"admin.dashboard.retention.average": "Averaĝa",
|
||||||
"admin.dashboard.retention.cohort": "Registriĝo monato",
|
"admin.dashboard.retention.cohort": "Monato de registriĝo",
|
||||||
"admin.dashboard.retention.cohort_size": "Novaj uzantoj",
|
"admin.dashboard.retention.cohort_size": "Novaj uzantoj",
|
||||||
"alert.rate_limited.message": "Bonvolu reprovi post {retry_time, time, medium}.",
|
"alert.rate_limited.message": "Bonvolu reprovi post {retry_time, time, medium}.",
|
||||||
"alert.rate_limited.title": "Mesaĝkvante limigita",
|
"alert.rate_limited.title": "Mesaĝkvante limigita",
|
||||||
"alert.unexpected.message": "Neatendita eraro okazis.",
|
"alert.unexpected.message": "Neatendita eraro okazis.",
|
||||||
"alert.unexpected.title": "Ups!",
|
"alert.unexpected.title": "Aj!",
|
||||||
"announcement.announcement": "Anonco",
|
"announcement.announcement": "Anonco",
|
||||||
"attachments_list.unprocessed": "(neprilaborita)",
|
"attachments_list.unprocessed": "(neprilaborita)",
|
||||||
"autosuggest_hashtag.per_week": "{count} semajne",
|
"autosuggest_hashtag.per_week": "{count} semajne",
|
||||||
"boost_modal.combo": "Vi povas premi {combo} por preterpasi sekvafoje",
|
"boost_modal.combo": "Vi povas premi {combo} por preterpasi sekvafoje",
|
||||||
"bundle_column_error.body": "Io misfunkciis en la ŝargado de ĉi tiu elemento.",
|
"bundle_column_error.body": "Io misfunkciis en la ŝargado de ĉi tiu elemento.",
|
||||||
"bundle_column_error.retry": "Bonvolu reprovi",
|
"bundle_column_error.retry": "Provu refoje",
|
||||||
"bundle_column_error.title": "Reta eraro",
|
"bundle_column_error.title": "Eraro de reto",
|
||||||
"bundle_modal_error.close": "Fermi",
|
"bundle_modal_error.close": "Fermi",
|
||||||
"bundle_modal_error.message": "Io misfunkciis en la ŝargado de ĉi tiu elemento.",
|
"bundle_modal_error.message": "Io misfunkciis en la ŝargado de ĉi tiu elemento.",
|
||||||
"bundle_modal_error.retry": "Bonvolu reprovi",
|
"bundle_modal_error.retry": "Provu refoje",
|
||||||
"column.blocks": "Blokitaj uzantoj",
|
"column.blocks": "Blokitaj uzantoj",
|
||||||
"column.bookmarks": "Legosignoj",
|
"column.bookmarks": "Legosignoj",
|
||||||
"column.community": "Loka templinio",
|
"column.community": "Loka templinio",
|
||||||
"column.direct": "Rektaj mesaĝoj",
|
"column.direct": "Rektaj mesaĝoj",
|
||||||
"column.directory": "Trarigardi profilojn",
|
"column.directory": "Trarigardi profilojn",
|
||||||
"column.domain_blocks": "Blokitaj domajnoj",
|
"column.domain_blocks": "Blokitaj domajnoj",
|
||||||
"column.favourites": "Stelumoj",
|
"column.favourites": "Preferaĵoj",
|
||||||
"column.follow_requests": "Demandoj de sekvado",
|
"column.follow_requests": "Demandoj de sekvado",
|
||||||
"column.home": "Hejmo",
|
"column.home": "Hejmo",
|
||||||
"column.lists": "Listoj",
|
"column.lists": "Listoj",
|
||||||
"column.mutes": "Silentigitaj uzantoj",
|
"column.mutes": "Silentigitaj uzantoj",
|
||||||
"column.notifications": "Sciigoj",
|
"column.notifications": "Sciigoj",
|
||||||
"column.pins": "Alpinglitaj mesaĝoj",
|
"column.pins": "Alpinglitaj mesaĝoj",
|
||||||
"column.public": "Fratara templinio",
|
"column.public": "Federata templinio",
|
||||||
"column_back_button.label": "Reveni",
|
"column_back_button.label": "Reveni",
|
||||||
"column_header.hide_settings": "Kaŝi agordojn",
|
"column_header.hide_settings": "Kaŝi la agordojn",
|
||||||
"column_header.moveLeft_settings": "Movi kolumnon maldekstren",
|
"column_header.moveLeft_settings": "Movi kolumnon maldekstren",
|
||||||
"column_header.moveRight_settings": "Movi kolumnon dekstren",
|
"column_header.moveRight_settings": "Movi kolumnon dekstren",
|
||||||
"column_header.pin": "Alpingli",
|
"column_header.pin": "Alpingli",
|
||||||
"column_header.show_settings": "Montri agordojn",
|
"column_header.show_settings": "Montri la agordojn",
|
||||||
"column_header.unpin": "Depingli",
|
"column_header.unpin": "Depingli",
|
||||||
"column_subheading.settings": "Agordado",
|
"column_subheading.settings": "Agordoj",
|
||||||
"community.column_settings.local_only": "Nur loka",
|
"community.column_settings.local_only": "Nur loka",
|
||||||
"community.column_settings.media_only": "Nur aŭdovidaĵoj",
|
"community.column_settings.media_only": "Nur aŭdovidaĵoj",
|
||||||
"community.column_settings.remote_only": "Nur malproksima",
|
"community.column_settings.remote_only": "Nur fora",
|
||||||
"compose.language.change": "Ŝanĝi lingvon",
|
"compose.language.change": "Ŝanĝi lingvon",
|
||||||
"compose.language.search": "Serĉi lingvojn...",
|
"compose.language.search": "Serĉi lingvojn...",
|
||||||
"compose_form.direct_message_warning_learn_more": "Lerni pli",
|
"compose_form.direct_message_warning_learn_more": "Lerni pli",
|
||||||
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
|
"compose_form.encryption_warning": "La mesaĵoj en Mastodono ne estas ĉifrita de tutvojo. Ne kundividu sentemajn informojn ĉe Mastodono.",
|
||||||
"compose_form.hashtag_warning": "Ĉi tiu mesaĝo ne estos listigita per ajna kradvorto. Nur publikaj mesaĝoj estas serĉeblaj per kradvortoj.",
|
"compose_form.hashtag_warning": "Ĉi tiu mesaĝo ne estos listigita per ajna kradvorto. Nur publikaj mesaĝoj estas serĉeblaj per kradvortoj.",
|
||||||
"compose_form.lock_disclaimer": "Via konta ne estas {locked}. Iu ajn povas sekvi vin por vidi viajn mesaĝojn, kiuj estas nur por sekvantoj.",
|
"compose_form.lock_disclaimer": "Via konto ne estas {locked}. Iu ajn povas sekvi vin por vidi viajn mesaĝojn nur al la sekvantoj.",
|
||||||
"compose_form.lock_disclaimer.lock": "ŝlosita",
|
"compose_form.lock_disclaimer.lock": "ŝlosita",
|
||||||
"compose_form.placeholder": "Kion vi pensas?",
|
"compose_form.placeholder": "Kion vi pensas?",
|
||||||
"compose_form.poll.add_option": "Aldoni elekteblon",
|
"compose_form.poll.add_option": "Aldoni elekteblon",
|
||||||
|
@ -116,7 +116,7 @@
|
||||||
"compose_form.spoiler.unmarked": "Teksto ne kaŝita",
|
"compose_form.spoiler.unmarked": "Teksto ne kaŝita",
|
||||||
"compose_form.spoiler_placeholder": "Skribu vian averton ĉi tie",
|
"compose_form.spoiler_placeholder": "Skribu vian averton ĉi tie",
|
||||||
"confirmation_modal.cancel": "Nuligi",
|
"confirmation_modal.cancel": "Nuligi",
|
||||||
"confirmations.block.block_and_report": "Bloki kaj signali",
|
"confirmations.block.block_and_report": "Bloki kaj raporti",
|
||||||
"confirmations.block.confirm": "Bloki",
|
"confirmations.block.confirm": "Bloki",
|
||||||
"confirmations.block.message": "Ĉu vi certas, ke vi volas bloki {name}?",
|
"confirmations.block.message": "Ĉu vi certas, ke vi volas bloki {name}?",
|
||||||
"confirmations.delete.confirm": "Forigi",
|
"confirmations.delete.confirm": "Forigi",
|
||||||
|
@ -124,7 +124,7 @@
|
||||||
"confirmations.delete_list.confirm": "Forigi",
|
"confirmations.delete_list.confirm": "Forigi",
|
||||||
"confirmations.delete_list.message": "Ĉu vi certas, ke vi volas porĉiame forigi ĉi tiun liston?",
|
"confirmations.delete_list.message": "Ĉu vi certas, ke vi volas porĉiame forigi ĉi tiun liston?",
|
||||||
"confirmations.discard_edit_media.confirm": "Ne konservi",
|
"confirmations.discard_edit_media.confirm": "Ne konservi",
|
||||||
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
|
"confirmations.discard_edit_media.message": "Vi havas nekonservitan ŝanĝon de la priskribo aŭ de la antaŭvido de aŭdvidaĵo, ĉu vi forigu ĝin?",
|
||||||
"confirmations.domain_block.confirm": "Bloki la tutan domajnon",
|
"confirmations.domain_block.confirm": "Bloki la tutan domajnon",
|
||||||
"confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas tute bloki {domain}? Plej ofte, trafa blokado kaj silentigado sufiĉas kaj preferindas. Vi ne vidos enhavon de tiu domajno en publika templinio aŭ en viaj sciigoj. Viaj sekvantoj de tiu domajno estos forigitaj.",
|
"confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas tute bloki {domain}? Plej ofte, trafa blokado kaj silentigado sufiĉas kaj preferindas. Vi ne vidos enhavon de tiu domajno en publika templinio aŭ en viaj sciigoj. Viaj sekvantoj de tiu domajno estos forigitaj.",
|
||||||
"confirmations.logout.confirm": "Adiaŭi",
|
"confirmations.logout.confirm": "Adiaŭi",
|
||||||
|
@ -133,7 +133,7 @@
|
||||||
"confirmations.mute.explanation": "Ĉi-tio kaŝos mesaĝojn el ili kaj mesaĝojn kiuj mencias ilin, sed ili ankoraŭ rajtos vidi viajn mesaĝojn kaj sekvi vin.",
|
"confirmations.mute.explanation": "Ĉi-tio kaŝos mesaĝojn el ili kaj mesaĝojn kiuj mencias ilin, sed ili ankoraŭ rajtos vidi viajn mesaĝojn kaj sekvi vin.",
|
||||||
"confirmations.mute.message": "Ĉu vi certas, ke vi volas silentigi {name}?",
|
"confirmations.mute.message": "Ĉu vi certas, ke vi volas silentigi {name}?",
|
||||||
"confirmations.redraft.confirm": "Forigi kaj reskribi",
|
"confirmations.redraft.confirm": "Forigi kaj reskribi",
|
||||||
"confirmations.redraft.message": "Ĉu vi certas ke vi volas forigi tiun mesaĝon kaj reskribi ĝin? Ĉiuj diskonigoj kaj stelumoj estos perditaj, kaj respondoj al la originala mesaĝo estos senparentaj.",
|
"confirmations.redraft.message": "Ĉu vi certas ke vi volas forigi kaj reskribi la mesaĝon? Ĝiaj preferitaĵoj kaj ĝiaj plusendoj estos perditaj, kaj la respondoj al la originala mesaĝo estos orfaj.",
|
||||||
"confirmations.reply.confirm": "Respondi",
|
"confirmations.reply.confirm": "Respondi",
|
||||||
"confirmations.reply.message": "Respondi nun anstataŭigos la mesaĝon, kiun vi nun skribas. Ĉu vi certas, ke vi volas daŭrigi?",
|
"confirmations.reply.message": "Respondi nun anstataŭigos la mesaĝon, kiun vi nun skribas. Ĉu vi certas, ke vi volas daŭrigi?",
|
||||||
"confirmations.unfollow.confirm": "Ne plu sekvi",
|
"confirmations.unfollow.confirm": "Ne plu sekvi",
|
||||||
|
@ -172,8 +172,8 @@
|
||||||
"empty_column.direct": "Vi ankoraŭ ne havas rektan mesaĝon. Kiam vi sendos aŭ ricevos iun, ĝi aperos ĉi tie.",
|
"empty_column.direct": "Vi ankoraŭ ne havas rektan mesaĝon. Kiam vi sendos aŭ ricevos iun, ĝi aperos ĉi tie.",
|
||||||
"empty_column.domain_blocks": "Ankoraŭ neniu domajno estas blokita.",
|
"empty_column.domain_blocks": "Ankoraŭ neniu domajno estas blokita.",
|
||||||
"empty_column.explore_statuses": "Nenio tendencas nun. Rekontrolu poste!",
|
"empty_column.explore_statuses": "Nenio tendencas nun. Rekontrolu poste!",
|
||||||
"empty_column.favourited_statuses": "Vi ankoraŭ ne stelumis mesaĝon. Kiam vi stelumos iun, tiu aperos ĉi tie.",
|
"empty_column.favourited_statuses": "Vi ankoraŭ ne havas mesaĝon en la preferaĵoj. Kiam vi aldonas ion, ĝi aperos ĉi tie.",
|
||||||
"empty_column.favourites": "Ankoraŭ neniu stelumis tiun mesaĝon. Kiam iu faros tion, tiu aperos ĉi tie.",
|
"empty_column.favourites": "Ankoraŭ neniu preferis la mesaĝon. Kiam iu faros ĉi tion, ili aperos ĉi tie.",
|
||||||
"empty_column.follow_recommendations": "Ŝajnas, ke neniuj sugestoj povis esti generitaj por vi. Vi povas provi uzi serĉon por serĉi homojn, kiujn vi eble konas, aŭ esplori tendencajn kradvortojn.",
|
"empty_column.follow_recommendations": "Ŝajnas, ke neniuj sugestoj povis esti generitaj por vi. Vi povas provi uzi serĉon por serĉi homojn, kiujn vi eble konas, aŭ esplori tendencajn kradvortojn.",
|
||||||
"empty_column.follow_requests": "Vi ne ankoraŭ havas iun peton de sekvado. Kiam vi ricevos unu, ĝi aperos ĉi tie.",
|
"empty_column.follow_requests": "Vi ne ankoraŭ havas iun peton de sekvado. Kiam vi ricevos unu, ĝi aperos ĉi tie.",
|
||||||
"empty_column.hashtag": "Ankoraŭ estas nenio per ĉi tiu kradvorto.",
|
"empty_column.hashtag": "Ankoraŭ estas nenio per ĉi tiu kradvorto.",
|
||||||
|
@ -198,10 +198,10 @@
|
||||||
"explore.trending_tags": "Kradvortoj",
|
"explore.trending_tags": "Kradvortoj",
|
||||||
"follow_recommendations.done": "Farita",
|
"follow_recommendations.done": "Farita",
|
||||||
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
|
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
|
||||||
"follow_recommendations.lead": "La mesaĝoj de personoj kiujn vi sekvas, aperos kronologie en via abonfluo. Ne timu erari, vi povas ĉesi sekvi facile iam ajn!",
|
"follow_recommendations.lead": "La mesaĝoj de personoj kiujn vi sekvas, kronologie aperos en via hejma templinio. Ne timu erari, vi povas ĉesi sekvi facile iam ajn!",
|
||||||
"follow_request.authorize": "Rajtigi",
|
"follow_request.authorize": "Rajtigi",
|
||||||
"follow_request.reject": "Rifuzi",
|
"follow_request.reject": "Rifuzi",
|
||||||
"follow_requests.unlocked_explanation": "Kvankam via konto ne estas ŝlosita, la teamo de {domain} pensis ke vi eble volas kontroli la demandojn de sekvado de ĉi tiuj kontoj permane.",
|
"follow_requests.unlocked_explanation": "Kvankam via konto ne estas ŝlosita, la teamo de {domain} pensis ke vi eble volas permane kontroli la demandojn de sekvado de ĉi tiuj kontoj.",
|
||||||
"generic.saved": "Konservita",
|
"generic.saved": "Konservita",
|
||||||
"getting_started.developers": "Programistoj",
|
"getting_started.developers": "Programistoj",
|
||||||
"getting_started.directory": "Profilujo",
|
"getting_started.directory": "Profilujo",
|
||||||
|
@ -237,9 +237,9 @@
|
||||||
"keyboard_shortcuts.direct": "malfermi la kolumnon de rektaj mesaĝoj",
|
"keyboard_shortcuts.direct": "malfermi la kolumnon de rektaj mesaĝoj",
|
||||||
"keyboard_shortcuts.down": "iri suben en la listo",
|
"keyboard_shortcuts.down": "iri suben en la listo",
|
||||||
"keyboard_shortcuts.enter": "malfermi mesaĝon",
|
"keyboard_shortcuts.enter": "malfermi mesaĝon",
|
||||||
"keyboard_shortcuts.favourite": "stelumi",
|
"keyboard_shortcuts.favourite": "Aldoni la mesaĝon al preferaĵoj",
|
||||||
"keyboard_shortcuts.favourites": "malfermi la liston de stelumoj",
|
"keyboard_shortcuts.favourites": "Malfermi la liston de preferaĵoj",
|
||||||
"keyboard_shortcuts.federated": "Malfermi la frataran templinion",
|
"keyboard_shortcuts.federated": "Malfermi la federatan templinion",
|
||||||
"keyboard_shortcuts.heading": "Klavaraj mallongigoj",
|
"keyboard_shortcuts.heading": "Klavaraj mallongigoj",
|
||||||
"keyboard_shortcuts.home": "Malfermi la hejman templinion",
|
"keyboard_shortcuts.home": "Malfermi la hejman templinion",
|
||||||
"keyboard_shortcuts.hotkey": "Rapidklavo",
|
"keyboard_shortcuts.hotkey": "Rapidklavo",
|
||||||
|
@ -279,7 +279,7 @@
|
||||||
"lists.replies_policy.followed": "Iu sekvanta uzanto",
|
"lists.replies_policy.followed": "Iu sekvanta uzanto",
|
||||||
"lists.replies_policy.list": "Membroj de la listo",
|
"lists.replies_policy.list": "Membroj de la listo",
|
||||||
"lists.replies_policy.none": "Neniu",
|
"lists.replies_policy.none": "Neniu",
|
||||||
"lists.replies_policy.title": "Montri respondon al:",
|
"lists.replies_policy.title": "Montri respondojn al:",
|
||||||
"lists.search": "Serĉi inter la homoj, kiujn vi sekvas",
|
"lists.search": "Serĉi inter la homoj, kiujn vi sekvas",
|
||||||
"lists.subheading": "Viaj listoj",
|
"lists.subheading": "Viaj listoj",
|
||||||
"load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
|
"load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
|
||||||
|
@ -312,10 +312,10 @@
|
||||||
"navigation_bar.personal": "Persone",
|
"navigation_bar.personal": "Persone",
|
||||||
"navigation_bar.pins": "Alpinglitaj mesaĝoj",
|
"navigation_bar.pins": "Alpinglitaj mesaĝoj",
|
||||||
"navigation_bar.preferences": "Preferoj",
|
"navigation_bar.preferences": "Preferoj",
|
||||||
"navigation_bar.public_timeline": "Fratara templinio",
|
"navigation_bar.public_timeline": "Federata templinio",
|
||||||
"navigation_bar.security": "Sekureco",
|
"navigation_bar.security": "Sekureco",
|
||||||
"notification.admin.sign_up": "{name} registris",
|
"notification.admin.sign_up": "{name} registris",
|
||||||
"notification.favourite": "{name} stelumis vian mesaĝon",
|
"notification.favourite": "{name} preferis vian mesaĝon",
|
||||||
"notification.follow": "{name} eksekvis vin",
|
"notification.follow": "{name} eksekvis vin",
|
||||||
"notification.follow_request": "{name} petis sekvi vin",
|
"notification.follow_request": "{name} petis sekvi vin",
|
||||||
"notification.mention": "{name} menciis vin",
|
"notification.mention": "{name} menciis vin",
|
||||||
|
@ -328,10 +328,10 @@
|
||||||
"notifications.clear_confirmation": "Ĉu vi certas, ke vi volas porĉiame forviŝi ĉiujn viajn sciigojn?",
|
"notifications.clear_confirmation": "Ĉu vi certas, ke vi volas porĉiame forviŝi ĉiujn viajn sciigojn?",
|
||||||
"notifications.column_settings.admin.sign_up": "Novaj registriĝoj:",
|
"notifications.column_settings.admin.sign_up": "Novaj registriĝoj:",
|
||||||
"notifications.column_settings.alert": "Retumilaj sciigoj",
|
"notifications.column_settings.alert": "Retumilaj sciigoj",
|
||||||
"notifications.column_settings.favourite": "Stelumoj:",
|
"notifications.column_settings.favourite": "Preferaĵoj:",
|
||||||
"notifications.column_settings.filter_bar.advanced": "Montri ĉiujn kategoriojn",
|
"notifications.column_settings.filter_bar.advanced": "Montri ĉiujn kategoriojn",
|
||||||
"notifications.column_settings.filter_bar.category": "Rapida filtra breto",
|
"notifications.column_settings.filter_bar.category": "Rapida filtra breto",
|
||||||
"notifications.column_settings.filter_bar.show_bar": "Montru filtrilon",
|
"notifications.column_settings.filter_bar.show_bar": "Montri la breton de filtrilo",
|
||||||
"notifications.column_settings.follow": "Novaj sekvantoj:",
|
"notifications.column_settings.follow": "Novaj sekvantoj:",
|
||||||
"notifications.column_settings.follow_request": "Novaj petoj de sekvado:",
|
"notifications.column_settings.follow_request": "Novaj petoj de sekvado:",
|
||||||
"notifications.column_settings.mention": "Mencioj:",
|
"notifications.column_settings.mention": "Mencioj:",
|
||||||
|
@ -346,7 +346,7 @@
|
||||||
"notifications.column_settings.update": "Redaktoj:",
|
"notifications.column_settings.update": "Redaktoj:",
|
||||||
"notifications.filter.all": "Ĉiuj",
|
"notifications.filter.all": "Ĉiuj",
|
||||||
"notifications.filter.boosts": "Plusendoj",
|
"notifications.filter.boosts": "Plusendoj",
|
||||||
"notifications.filter.favourites": "Stelumoj",
|
"notifications.filter.favourites": "Preferaĵoj",
|
||||||
"notifications.filter.follows": "Sekvoj",
|
"notifications.filter.follows": "Sekvoj",
|
||||||
"notifications.filter.mentions": "Mencioj",
|
"notifications.filter.mentions": "Mencioj",
|
||||||
"notifications.filter.polls": "Balotenketaj rezultoj",
|
"notifications.filter.polls": "Balotenketaj rezultoj",
|
||||||
|
@ -381,7 +381,7 @@
|
||||||
"privacy.unlisted.short": "Nelistigita",
|
"privacy.unlisted.short": "Nelistigita",
|
||||||
"refresh": "Refreŝigu",
|
"refresh": "Refreŝigu",
|
||||||
"regeneration_indicator.label": "Ŝargado…",
|
"regeneration_indicator.label": "Ŝargado…",
|
||||||
"regeneration_indicator.sublabel": "Via hejma fluo pretiĝas!",
|
"regeneration_indicator.sublabel": "Via abonfluo estas preparata!",
|
||||||
"relative_time.days": "{number}t",
|
"relative_time.days": "{number}t",
|
||||||
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
|
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
|
||||||
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
|
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
|
||||||
|
@ -397,18 +397,18 @@
|
||||||
"report.block": "Bloki",
|
"report.block": "Bloki",
|
||||||
"report.block_explanation": "You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.",
|
"report.block_explanation": "You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.",
|
||||||
"report.categories.other": "Aliaj",
|
"report.categories.other": "Aliaj",
|
||||||
"report.categories.spam": "Spamo",
|
"report.categories.spam": "Trudaĵo",
|
||||||
"report.categories.violation": "Content violates one or more server rules",
|
"report.categories.violation": "Content violates one or more server rules",
|
||||||
"report.category.subtitle": "Elektu la plej bonan kongruon",
|
"report.category.subtitle": "Elektu la plej bonan kongruon",
|
||||||
"report.category.title": "Diru al ni kio okazas pri ĉi tiu {type}",
|
"report.category.title": "Diru al ni kio okazas pri ĉi tiu {type}",
|
||||||
"report.category.title_account": "profilo",
|
"report.category.title_account": "profilo",
|
||||||
"report.category.title_status": "afiŝo",
|
"report.category.title_status": "afiŝo",
|
||||||
"report.close": "Farita",
|
"report.close": "Farita",
|
||||||
"report.comment.title": "Is there anything else you think we should know?",
|
"report.comment.title": "Ĉu estas io alia kion vi pensas ke ni devas scii?",
|
||||||
"report.forward": "Plusendi al {target}",
|
"report.forward": "Plusendi al {target}",
|
||||||
"report.forward_hint": "La konto estas en alia servilo. Ĉu sendi sennomigitan kopion de la signalo ankaŭ tien?",
|
"report.forward_hint": "La konto estas de alia servilo. Ĉu vi volas sendi anoniman kopion de la informo ankaŭ al tie?",
|
||||||
"report.mute": "Silentigi",
|
"report.mute": "Silentigi",
|
||||||
"report.mute_explanation": "Vi ne vidos iliajn afiŝojn. Ili ankoraŭ povas sekvi vin kaj vidi viajn afiŝojn, kaj ne scios ke si estas silentigitaj.",
|
"report.mute_explanation": "Vi ne vidos iliajn afiŝojn. Ili ankoraŭ povas sekvi vin kaj vidi viajn afiŝojn, kaj ne scios ke ili estas silentigitaj.",
|
||||||
"report.next": "Sekva",
|
"report.next": "Sekva",
|
||||||
"report.placeholder": "Pliaj komentoj",
|
"report.placeholder": "Pliaj komentoj",
|
||||||
"report.reasons.dislike": "Mi ne ŝatas ĝin",
|
"report.reasons.dislike": "Mi ne ŝatas ĝin",
|
||||||
|
@ -417,20 +417,20 @@
|
||||||
"report.reasons.other_description": "La problemo ne taŭgas en aliaj kategorioj",
|
"report.reasons.other_description": "La problemo ne taŭgas en aliaj kategorioj",
|
||||||
"report.reasons.spam": "Ĝi estas trudaĵo",
|
"report.reasons.spam": "Ĝi estas trudaĵo",
|
||||||
"report.reasons.spam_description": "Malicious links, fake engagement, or repetitive replies",
|
"report.reasons.spam_description": "Malicious links, fake engagement, or repetitive replies",
|
||||||
"report.reasons.violation": "Ĝi malrespektas servilajn regulojn",
|
"report.reasons.violation": "Ĝi malobservas la regulojn de la servilo",
|
||||||
"report.reasons.violation_description": "You are aware that it breaks specific rules",
|
"report.reasons.violation_description": "You are aware that it breaks specific rules",
|
||||||
"report.rules.subtitle": "Elektu ĉiujn, kiuj validas",
|
"report.rules.subtitle": "Elektu ĉiujn, kiuj validas",
|
||||||
"report.rules.title": "Kiuj reguloj estas malobservataj?",
|
"report.rules.title": "Kiuj reguloj estas malobservataj?",
|
||||||
"report.statuses.subtitle": "Elektu ĉiujn, kiuj validas",
|
"report.statuses.subtitle": "Elektu ĉiujn, kiuj validas",
|
||||||
"report.statuses.title": "Are there any posts that back up this report?",
|
"report.statuses.title": "Are there any posts that back up this report?",
|
||||||
"report.submit": "Sendi",
|
"report.submit": "Sendi",
|
||||||
"report.target": "Signali {target}",
|
"report.target": "Raporto pri {target}",
|
||||||
"report.thanks.take_action": "Here are your options for controlling what you see on Mastodon:",
|
"report.thanks.take_action": "Here are your options for controlling what you see on Mastodon:",
|
||||||
"report.thanks.take_action_actionable": "While we review this, you can take action against @{name}:",
|
"report.thanks.take_action_actionable": "While we review this, you can take action against @{name}:",
|
||||||
"report.thanks.title": "Ĉu vi ne volas vidi ĉi tion?",
|
"report.thanks.title": "Ĉu vi ne volas vidi ĉi tion?",
|
||||||
"report.thanks.title_actionable": "Dankon pro raporti, ni esploros ĉi tion.",
|
"report.thanks.title_actionable": "Dankon pro raporti, ni esploros ĉi tion.",
|
||||||
"report.unfollow": "Malsekvi @{name}",
|
"report.unfollow": "Malsekvi @{name}",
|
||||||
"report.unfollow_explanation": "Vi estas sekvanta ĉi tiun konton. Por ne plu vidi ties afiŝojn en via hejma templinio, malsekvu ilin.",
|
"report.unfollow_explanation": "Vi sekvas ĉi tiun konton. Por ne plu vidi ĝiajn abonfluojn en via hejma templinio, ĉesu sekvi ĝin.",
|
||||||
"search.placeholder": "Serĉi",
|
"search.placeholder": "Serĉi",
|
||||||
"search_popout.search_format": "Detala serĉo",
|
"search_popout.search_format": "Detala serĉo",
|
||||||
"search_popout.tips.full_text": "Simplaj tekstoj montras la mesaĝojn, kiujn vi skribis, stelumis, diskonigis, aŭ en kiuj vi estis menciita, sed ankaŭ kongruajn uzantnomojn, montratajn nomojn, kaj kradvortojn.",
|
"search_popout.tips.full_text": "Simplaj tekstoj montras la mesaĝojn, kiujn vi skribis, stelumis, diskonigis, aŭ en kiuj vi estis menciita, sed ankaŭ kongruajn uzantnomojn, montratajn nomojn, kaj kradvortojn.",
|
||||||
|
@ -459,7 +459,7 @@
|
||||||
"status.edited": "Redaktita {date}",
|
"status.edited": "Redaktita {date}",
|
||||||
"status.edited_x_times": "Redactita {count, plural, one {{count} fojon} other {{count} fojojn}}",
|
"status.edited_x_times": "Redactita {count, plural, one {{count} fojon} other {{count} fojojn}}",
|
||||||
"status.embed": "Enkorpigi",
|
"status.embed": "Enkorpigi",
|
||||||
"status.favourite": "Stelumi",
|
"status.favourite": "Preferaĵo",
|
||||||
"status.filtered": "Filtrita",
|
"status.filtered": "Filtrita",
|
||||||
"status.history.created": "{name} kreis {date}",
|
"status.history.created": "{name} kreis {date}",
|
||||||
"status.history.edited": "{name} redaktis {date}",
|
"status.history.edited": "{name} redaktis {date}",
|
||||||
|
@ -469,8 +469,8 @@
|
||||||
"status.more": "Pli",
|
"status.more": "Pli",
|
||||||
"status.mute": "Silentigi @{name}",
|
"status.mute": "Silentigi @{name}",
|
||||||
"status.mute_conversation": "Silentigi konversacion",
|
"status.mute_conversation": "Silentigi konversacion",
|
||||||
"status.open": "Grandigi ĉi tiun mesaĝon",
|
"status.open": "Disvolvi la mesaĝon",
|
||||||
"status.pin": "Alpingli profile",
|
"status.pin": "Alpingli al la profilo",
|
||||||
"status.pinned": "Alpinglita mesaĝo",
|
"status.pinned": "Alpinglita mesaĝo",
|
||||||
"status.read_more": "Legi pli",
|
"status.read_more": "Legi pli",
|
||||||
"status.reblog": "Plusendi",
|
"status.reblog": "Plusendi",
|
||||||
|
@ -481,20 +481,20 @@
|
||||||
"status.remove_bookmark": "Forigi legosignon",
|
"status.remove_bookmark": "Forigi legosignon",
|
||||||
"status.reply": "Respondi",
|
"status.reply": "Respondi",
|
||||||
"status.replyAll": "Respondi al la fadeno",
|
"status.replyAll": "Respondi al la fadeno",
|
||||||
"status.report": "Signali @{name}",
|
"status.report": "Raporti @{name}",
|
||||||
"status.sensitive_warning": "Tikla enhavo",
|
"status.sensitive_warning": "Tikla enhavo",
|
||||||
"status.share": "Diskonigi",
|
"status.share": "Kundividi",
|
||||||
"status.show_less": "Malgrandigi",
|
"status.show_less": "Montri malpli",
|
||||||
"status.show_less_all": "Malgrandigi ĉiujn",
|
"status.show_less_all": "Montri malpli ĉiun",
|
||||||
"status.show_more": "Grandigi",
|
"status.show_more": "Montri pli",
|
||||||
"status.show_more_all": "Malfoldi ĉiun",
|
"status.show_more_all": "Montri pli ĉiun",
|
||||||
"status.show_thread": "Montri la fadenon",
|
"status.show_thread": "Montri la mesaĝaron",
|
||||||
"status.uncached_media_warning": "Nedisponebla",
|
"status.uncached_media_warning": "Nedisponebla",
|
||||||
"status.unmute_conversation": "Malsilentigi la konversacion",
|
"status.unmute_conversation": "Malsilentigi la konversacion",
|
||||||
"status.unpin": "Depingli de profilo",
|
"status.unpin": "Depingli de profilo",
|
||||||
"suggestions.dismiss": "Forigi la proponon",
|
"suggestions.dismiss": "Forigi la proponon",
|
||||||
"suggestions.header": "Vi povus interesiĝi pri…",
|
"suggestions.header": "Vi povus interesiĝi pri…",
|
||||||
"tabs_bar.federated_timeline": "Fratara templinio",
|
"tabs_bar.federated_timeline": "Federata",
|
||||||
"tabs_bar.home": "Hejmo",
|
"tabs_bar.home": "Hejmo",
|
||||||
"tabs_bar.local_timeline": "Loka templinio",
|
"tabs_bar.local_timeline": "Loka templinio",
|
||||||
"tabs_bar.notifications": "Sciigoj",
|
"tabs_bar.notifications": "Sciigoj",
|
||||||
|
@ -539,7 +539,7 @@
|
||||||
"video.close": "Fermi la videon",
|
"video.close": "Fermi la videon",
|
||||||
"video.download": "Elŝuti dosieron",
|
"video.download": "Elŝuti dosieron",
|
||||||
"video.exit_fullscreen": "Eksigi plenekrana",
|
"video.exit_fullscreen": "Eksigi plenekrana",
|
||||||
"video.expand": "Grandigi la videon",
|
"video.expand": "Pligrandigi la videon",
|
||||||
"video.fullscreen": "Igi plenekrana",
|
"video.fullscreen": "Igi plenekrana",
|
||||||
"video.hide": "Kaŝi la videon",
|
"video.hide": "Kaŝi la videon",
|
||||||
"video.mute": "Silentigi",
|
"video.mute": "Silentigi",
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"account.blocked": "Bloqueada",
|
"account.blocked": "Bloqueada",
|
||||||
"account.browse_more_on_origin_server": "Busca máis no perfil orixinal",
|
"account.browse_more_on_origin_server": "Busca máis no perfil orixinal",
|
||||||
"account.cancel_follow_request": "Desbotar solicitude de seguimento",
|
"account.cancel_follow_request": "Desbotar solicitude de seguimento",
|
||||||
"account.direct": "Mensaxe directa @{name}",
|
"account.direct": "Mensaxe directa a @{name}",
|
||||||
"account.disable_notifications": "Deixar de notificarme cando @{name} publica",
|
"account.disable_notifications": "Deixar de notificarme cando @{name} publica",
|
||||||
"account.domain_blocked": "Dominio agochado",
|
"account.domain_blocked": "Dominio agochado",
|
||||||
"account.edit_profile": "Editar perfil",
|
"account.edit_profile": "Editar perfil",
|
||||||
|
|
|
@ -106,7 +106,7 @@
|
||||||
"compose_form.poll.remove_option": "เอาตัวเลือกนี้ออก",
|
"compose_form.poll.remove_option": "เอาตัวเลือกนี้ออก",
|
||||||
"compose_form.poll.switch_to_multiple": "เปลี่ยนการสำรวจความคิดเห็นเป็นอนุญาตหลายตัวเลือก",
|
"compose_form.poll.switch_to_multiple": "เปลี่ยนการสำรวจความคิดเห็นเป็นอนุญาตหลายตัวเลือก",
|
||||||
"compose_form.poll.switch_to_single": "เปลี่ยนการสำรวจความคิดเห็นเป็นอนุญาตตัวเลือกเดี่ยว",
|
"compose_form.poll.switch_to_single": "เปลี่ยนการสำรวจความคิดเห็นเป็นอนุญาตตัวเลือกเดี่ยว",
|
||||||
"compose_form.publish": "Publish",
|
"compose_form.publish": "เผยแพร่",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
"compose_form.save_changes": "บันทึกการเปลี่ยนแปลง",
|
"compose_form.save_changes": "บันทึกการเปลี่ยนแปลง",
|
||||||
"compose_form.sensitive.hide": "{count, plural, other {ทำเครื่องหมายสื่อว่าละเอียดอ่อน}}",
|
"compose_form.sensitive.hide": "{count, plural, other {ทำเครื่องหมายสื่อว่าละเอียดอ่อน}}",
|
||||||
|
|
|
@ -1,10 +1,34 @@
|
||||||
import { FILTERS_FETCH_SUCCESS } from '../actions/filters';
|
import { FILTERS_IMPORT } from '../actions/importer';
|
||||||
import { List as ImmutableList, fromJS } from 'immutable';
|
import { Map as ImmutableMap, is, fromJS } from 'immutable';
|
||||||
|
|
||||||
export default function filters(state = ImmutableList(), action) {
|
const normalizeFilter = (state, filter) => {
|
||||||
|
const normalizedFilter = fromJS({
|
||||||
|
id: filter.id,
|
||||||
|
title: filter.title,
|
||||||
|
context: filter.context,
|
||||||
|
filter_action: filter.filter_action,
|
||||||
|
expires_at: filter.expires_at ? Date.parse(filter.expires_at) : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (is(state.get(filter.id), normalizedFilter)) {
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
return state.set(filter.id, normalizedFilter);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeFilters = (state, filters) => {
|
||||||
|
filters.forEach(filter => {
|
||||||
|
state = normalizeFilter(state, filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function filters(state = ImmutableMap(), action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case FILTERS_FETCH_SUCCESS:
|
case FILTERS_IMPORT:
|
||||||
return fromJS(action.filters);
|
return normalizeFilters(state, action.filters);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import {
|
||||||
} from '../actions/app';
|
} from '../actions/app';
|
||||||
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
|
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
|
||||||
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import compareId from '../compare_id';
|
import compareId from '../compare_id';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
|
@ -52,6 +52,7 @@ const notificationToMap = notification => ImmutableMap({
|
||||||
account: notification.account.id,
|
account: notification.account.id,
|
||||||
created_at: notification.created_at,
|
created_at: notification.created_at,
|
||||||
status: notification.status ? notification.status.id : null,
|
status: notification.status ? notification.status.id : null,
|
||||||
|
report: notification.report ? fromJS(notification.report) : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeNotification = (state, notification, usePendingItems) => {
|
const normalizeNotification = (state, notification, usePendingItems) => {
|
||||||
|
|
|
@ -39,6 +39,7 @@ const initialState = ImmutableMap({
|
||||||
status: false,
|
status: false,
|
||||||
update: false,
|
update: false,
|
||||||
'admin.sign_up': false,
|
'admin.sign_up': false,
|
||||||
|
'admin.report': false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
quickFilter: ImmutableMap({
|
quickFilter: ImmutableMap({
|
||||||
|
@ -60,6 +61,7 @@ const initialState = ImmutableMap({
|
||||||
status: true,
|
status: true,
|
||||||
update: true,
|
update: true,
|
||||||
'admin.sign_up': true,
|
'admin.sign_up': true,
|
||||||
|
'admin.report': true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sounds: ImmutableMap({
|
sounds: ImmutableMap({
|
||||||
|
@ -72,6 +74,7 @@ const initialState = ImmutableMap({
|
||||||
status: true,
|
status: true,
|
||||||
update: true,
|
update: true,
|
||||||
'admin.sign_up': true,
|
'admin.sign_up': true,
|
||||||
|
'admin.report': true,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
@ -40,15 +40,15 @@ const toServerSideType = columnType => {
|
||||||
const escapeRegExp = string =>
|
const escapeRegExp = string =>
|
||||||
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||||
|
|
||||||
const regexFromFilters = filters => {
|
const regexFromKeywords = keywords => {
|
||||||
if (filters.size === 0) {
|
if (keywords.size === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RegExp(filters.map(filter => {
|
return new RegExp(keywords.map(keyword_filter => {
|
||||||
let expr = escapeRegExp(filter.get('phrase'));
|
let expr = escapeRegExp(keyword_filter.get('keyword'));
|
||||||
|
|
||||||
if (filter.get('whole_word')) {
|
if (keyword_filter.get('whole_word')) {
|
||||||
if (/^[\w]/.test(expr)) {
|
if (/^[\w]/.test(expr)) {
|
||||||
expr = `\\b${expr}`;
|
expr = `\\b${expr}`;
|
||||||
}
|
}
|
||||||
|
@ -62,27 +62,15 @@ const regexFromFilters = filters => {
|
||||||
}).join('|'), 'i');
|
}).join('|'), 'i');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Memoize the filter regexps for each valid server contextType
|
const getFilters = (state, { contextType }) => {
|
||||||
const makeGetFiltersRegex = () => {
|
if (!contextType) return null;
|
||||||
let memo = {};
|
|
||||||
|
|
||||||
return (state, { contextType }) => {
|
const serverSideType = toServerSideType(contextType);
|
||||||
if (!contextType) return ImmutableList();
|
const now = new Date();
|
||||||
|
|
||||||
const serverSideType = toServerSideType(contextType);
|
return state.get('filters').filter((filter) => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || filter.get('expires_at') > now));
|
||||||
const filters = state.get('filters', ImmutableList()).filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date())));
|
|
||||||
|
|
||||||
if (!memo[serverSideType] || !is(memo[serverSideType].filters, filters)) {
|
|
||||||
const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible')));
|
|
||||||
const regex = regexFromFilters(filters);
|
|
||||||
memo[serverSideType] = { filters: filters, results: [dropRegex, regex] };
|
|
||||||
}
|
|
||||||
return memo[serverSideType].results;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFiltersRegex = makeGetFiltersRegex();
|
|
||||||
|
|
||||||
export const makeGetStatus = () => {
|
export const makeGetStatus = () => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
[
|
[
|
||||||
|
@ -90,10 +78,10 @@ export const makeGetStatus = () => {
|
||||||
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
|
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
||||||
getFiltersRegex,
|
getFilters,
|
||||||
],
|
],
|
||||||
|
|
||||||
(statusBase, statusReblog, accountBase, accountReblog, filtersRegex) => {
|
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
|
||||||
if (!statusBase) {
|
if (!statusBase) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -104,14 +92,17 @@ export const makeGetStatus = () => {
|
||||||
statusReblog = null;
|
statusReblog = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropRegex = (accountReblog || accountBase).get('id') !== me && filtersRegex[0];
|
let filtered = false;
|
||||||
if (dropRegex && dropRegex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index'))) {
|
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||||
return null;
|
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||||
|
if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!filterResults.isEmpty()) {
|
||||||
|
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const regex = (accountReblog || accountBase).get('id') !== me && filtersRegex[1];
|
|
||||||
const filtered = regex && regex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index'));
|
|
||||||
|
|
||||||
return statusBase.withMutations(map => {
|
return statusBase.withMutations(map => {
|
||||||
map.set('reblog', statusReblog);
|
map.set('reblog', statusReblog);
|
||||||
map.set('account', accountBase);
|
map.set('account', accountBase);
|
||||||
|
@ -152,14 +143,15 @@ export const getAlerts = createSelector([getAlertsBase], (base) => {
|
||||||
return arr;
|
return arr;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeGetNotification = () => {
|
export const makeGetNotification = () => createSelector([
|
||||||
return createSelector([
|
(_, base) => base,
|
||||||
(_, base) => base,
|
(state, _, accountId) => state.getIn(['accounts', accountId]),
|
||||||
(state, _, accountId) => state.getIn(['accounts', accountId]),
|
], (base, account) => base.set('account', account));
|
||||||
], (base, account) => {
|
|
||||||
return base.set('account', account);
|
export const makeGetReport = () => createSelector([
|
||||||
});
|
(_, base) => base,
|
||||||
};
|
(state, _, targetAccountId) => state.getIn(['accounts', targetAccountId]),
|
||||||
|
], (base, targetAccount) => base.set('target_account', targetAccount));
|
||||||
|
|
||||||
export const getAccountGallery = createSelector([
|
export const getAccountGallery = createSelector([
|
||||||
(state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
|
(state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
|
||||||
|
|
|
@ -3,6 +3,7 @@ import loadPolyfills from '../mastodon/load_polyfills';
|
||||||
import ready from '../mastodon/ready';
|
import ready from '../mastodon/ready';
|
||||||
import { start } from '../mastodon/common';
|
import { start } from '../mastodon/common';
|
||||||
import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions';
|
import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions';
|
||||||
|
import 'cocoon-js-vanilla';
|
||||||
|
|
||||||
start();
|
start();
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,13 @@ $content-width: 840px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo--wordmark {
|
||||||
|
display: inherit;
|
||||||
|
margin: inherit;
|
||||||
|
width: inherit;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $no-columns-breakpoint) {
|
@media screen and (max-width: $no-columns-breakpoint) {
|
||||||
& > a:first-child {
|
& > a:first-child {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -924,7 +931,8 @@ a.name-tag,
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.applications-list__item {
|
.applications-list__item,
|
||||||
|
.filters-list__item {
|
||||||
padding: 15px 0;
|
padding: 15px 0;
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
border: 1px solid lighten($ui-base-color, 4%);
|
border: 1px solid lighten($ui-base-color, 4%);
|
||||||
|
@ -932,7 +940,8 @@ a.name-tag,
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.announcements-list {
|
.announcements-list,
|
||||||
|
.filters-list {
|
||||||
border: 1px solid lighten($ui-base-color, 4%);
|
border: 1px solid lighten($ui-base-color, 4%);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
@ -985,6 +994,33 @@ a.name-tag,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filters-list__item {
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__permissions {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiration {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expired {
|
||||||
|
.expiration {
|
||||||
|
color: lighten($error-red, 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-list__item__icon {
|
||||||
|
color: $dark-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard__counters.admin-account-counters {
|
.dashboard__counters.admin-account-counters {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -959,6 +959,21 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
clear: both;
|
clear: both;
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
display: inline;
|
||||||
|
color: lighten($ui-highlight-color, 8%);
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__prepend-icon-wrapper {
|
.status__prepend-icon-wrapper {
|
||||||
|
@ -1355,6 +1370,8 @@ a .account__avatar {
|
||||||
.account__avatar-overlay {
|
.account__avatar-overlay {
|
||||||
@include avatar-size(48px);
|
@include avatar-size(48px);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&-base {
|
&-base {
|
||||||
@include avatar-radius;
|
@include avatar-radius;
|
||||||
@include avatar-size(36px);
|
@include avatar-size(36px);
|
||||||
|
@ -1620,6 +1637,33 @@ a.account__display-name {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification__report {
|
||||||
|
padding: 8px 10px;
|
||||||
|
padding-left: 68px;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
min-height: 54px;
|
||||||
|
|
||||||
|
&__details {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: $darker-text-color;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__avatar {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.notification__message {
|
.notification__message {
|
||||||
margin: 0 10px 0 68px;
|
margin: 0 10px 0 68px;
|
||||||
padding: 8px 0 0;
|
padding: 8px 0 0;
|
||||||
|
@ -2360,6 +2404,16 @@ a.account__display-name {
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification__report {
|
||||||
|
padding: 15px 15px 15px (48px + 15px * 2);
|
||||||
|
min-height: 48px + 2px;
|
||||||
|
|
||||||
|
&__avatar {
|
||||||
|
left: 15px;
|
||||||
|
top: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
padding: 15px 15px 15px (48px + 15px * 2);
|
padding: 15px 15px 15px (48px + 15px * 2);
|
||||||
min-height: 48px + 2px;
|
min-height: 48px + 2px;
|
||||||
|
|
|
@ -1070,3 +1070,34 @@ code {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keywords-table {
|
||||||
|
thead {
|
||||||
|
th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:first-child {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tfoot {
|
||||||
|
td {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input.string {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label_input__wrapper {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-action-link {
|
||||||
|
margin-top: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ class ActivityPub::Parser::MediaAttachmentParser
|
||||||
components = begin
|
components = begin
|
||||||
blurhash = @json['blurhash']
|
blurhash = @json['blurhash']
|
||||||
|
|
||||||
if blurhash.present? && /^[\w#$%*+-.:;=?@\[\]^{|}~]+$/.match?(blurhash)
|
if blurhash.present? && /^[\w#$%*+,-.:;=?@\[\]^{|}~]+$/.match?(blurhash)
|
||||||
Blurhash.components(blurhash)
|
Blurhash.components(blurhash)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -401,7 +401,6 @@ class FeedManager
|
||||||
def filter_from_home?(status, receiver_id, crutches)
|
def filter_from_home?(status, receiver_id, crutches)
|
||||||
return false if receiver_id == status.account_id
|
return false if receiver_id == status.account_id
|
||||||
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
||||||
return true if phrase_filtered?(status, receiver_id, :home)
|
|
||||||
|
|
||||||
check_for_blocks = crutches[:active_mentions][status.id] || []
|
check_for_blocks = crutches[:active_mentions][status.id] || []
|
||||||
check_for_blocks.concat([status.account_id])
|
check_for_blocks.concat([status.account_id])
|
||||||
|
@ -437,7 +436,6 @@ class FeedManager
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def filter_from_mentions?(status, receiver_id)
|
def filter_from_mentions?(status, receiver_id)
|
||||||
return true if receiver_id == status.account_id
|
return true if receiver_id == status.account_id
|
||||||
return true if phrase_filtered?(status, receiver_id, :notifications)
|
|
||||||
|
|
||||||
# This filter is called from NotifyService, but already after the sender of
|
# This filter is called from NotifyService, but already after the sender of
|
||||||
# the notification has been checked for mute/block. Therefore, it's not
|
# the notification has been checked for mute/block. Therefore, it's not
|
||||||
|
@ -476,34 +474,6 @@ class FeedManager
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
# Check if the status hits a phrase filter
|
|
||||||
# @param [Status] status
|
|
||||||
# @param [Integer] receiver_id
|
|
||||||
# @param [Symbol] context
|
|
||||||
# @return [Boolean]
|
|
||||||
def phrase_filtered?(status, receiver_id, context)
|
|
||||||
active_filters = Rails.cache.fetch("filters:#{receiver_id}") { CustomFilter.where(account_id: receiver_id).active_irreversible.to_a }.to_a
|
|
||||||
|
|
||||||
active_filters.select! { |filter| filter.context.include?(context.to_s) && !filter.expired? }
|
|
||||||
|
|
||||||
active_filters.map! do |filter|
|
|
||||||
if filter.whole_word
|
|
||||||
sb = /\A[[:word:]]/.match?(filter.phrase) ? '\b' : ''
|
|
||||||
eb = /[[:word:]]\z/.match?(filter.phrase) ? '\b' : ''
|
|
||||||
|
|
||||||
/(?mix:#{sb}#{Regexp.escape(filter.phrase)}#{eb})/
|
|
||||||
else
|
|
||||||
/#{Regexp.escape(filter.phrase)}/i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return false if active_filters.empty?
|
|
||||||
|
|
||||||
combined_regex = Regexp.union(active_filters)
|
|
||||||
|
|
||||||
combined_regex.match?(status.proper.searchable_text)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adds a status to an account's feed, returning true if a status was
|
# Adds a status to an account's feed, returning true if a status was
|
||||||
# added, and false if it was not added to the feed. Note that this is
|
# added, and false if it was not added to the feed. Note that this is
|
||||||
# an internal helper: callers must call trim or push updates if
|
# an internal helper: callers must call trim or push updates if
|
||||||
|
|
|
@ -247,6 +247,19 @@ module AccountInteractions
|
||||||
account_pins.where(target_account: account).exists?
|
account_pins.where(target_account: account).exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status_matches_filters(status)
|
||||||
|
active_filters = CustomFilter.cached_filters_for(id)
|
||||||
|
|
||||||
|
filter_matches = active_filters.filter_map do |filter, rules|
|
||||||
|
next if rules[:keywords].blank?
|
||||||
|
|
||||||
|
match = rules[:keywords].match(status.proper.searchable_text)
|
||||||
|
FilterResultPresenter.new(filter: filter, keyword_matches: [match.to_s]) unless match.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
filter_matches
|
||||||
|
end
|
||||||
|
|
||||||
def followers_for_local_distribution
|
def followers_for_local_distribution
|
||||||
followers.local
|
followers.local
|
||||||
.joins(:user)
|
.joins(:user)
|
||||||
|
|
|
@ -3,18 +3,22 @@
|
||||||
#
|
#
|
||||||
# Table name: custom_filters
|
# Table name: custom_filters
|
||||||
#
|
#
|
||||||
# id :bigint(8) not null, primary key
|
# id :bigint not null, primary key
|
||||||
# account_id :bigint(8)
|
# account_id :bigint
|
||||||
# expires_at :datetime
|
# expires_at :datetime
|
||||||
# phrase :text default(""), not null
|
# phrase :text default(""), not null
|
||||||
# context :string default([]), not null, is an Array
|
# context :string default([]), not null, is an Array
|
||||||
# whole_word :boolean default(TRUE), not null
|
# created_at :datetime not null
|
||||||
# irreversible :boolean default(FALSE), not null
|
# updated_at :datetime not null
|
||||||
# created_at :datetime not null
|
# action :integer default(0), not null
|
||||||
# updated_at :datetime not null
|
|
||||||
#
|
#
|
||||||
|
|
||||||
class CustomFilter < ApplicationRecord
|
class CustomFilter < ApplicationRecord
|
||||||
|
self.ignored_columns = %w(whole_word irreversible)
|
||||||
|
|
||||||
|
alias_attribute :title, :phrase
|
||||||
|
alias_attribute :filter_action, :action
|
||||||
|
|
||||||
VALID_CONTEXTS = %w(
|
VALID_CONTEXTS = %w(
|
||||||
home
|
home
|
||||||
notifications
|
notifications
|
||||||
|
@ -26,16 +30,20 @@ class CustomFilter < ApplicationRecord
|
||||||
include Expireable
|
include Expireable
|
||||||
include Redisable
|
include Redisable
|
||||||
|
|
||||||
|
enum action: [:warn, :hide], _suffix: :action
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
has_many :keywords, class_name: 'CustomFilterKeyword', foreign_key: :custom_filter_id, inverse_of: :custom_filter, dependent: :destroy
|
||||||
|
accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true
|
||||||
|
|
||||||
validates :phrase, :context, presence: true
|
validates :title, :context, presence: true
|
||||||
validate :context_must_be_valid
|
validate :context_must_be_valid
|
||||||
validate :irreversible_must_be_within_context
|
|
||||||
|
|
||||||
scope :active_irreversible, -> { where(irreversible: true).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()')) }
|
|
||||||
|
|
||||||
before_validation :clean_up_contexts
|
before_validation :clean_up_contexts
|
||||||
after_commit :remove_cache
|
|
||||||
|
before_save :prepare_cache_invalidation!
|
||||||
|
before_destroy :prepare_cache_invalidation!
|
||||||
|
after_commit :invalidate_cache!
|
||||||
|
|
||||||
def expires_in
|
def expires_in
|
||||||
return @expires_in if defined?(@expires_in)
|
return @expires_in if defined?(@expires_in)
|
||||||
|
@ -44,22 +52,55 @@ class CustomFilter < ApplicationRecord
|
||||||
[30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].find { |expires_in| expires_in.from_now >= expires_at }
|
[30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].find { |expires_in| expires_in.from_now >= expires_at }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def irreversible=(value)
|
||||||
|
self.action = value ? :hide : :warn
|
||||||
|
end
|
||||||
|
|
||||||
|
def irreversible?
|
||||||
|
hide_action?
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.cached_filters_for(account_id)
|
||||||
|
active_filters = Rails.cache.fetch("filters:v3:#{account_id}") do
|
||||||
|
scope = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account_id: account_id }).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()'))
|
||||||
|
scope.to_a.group_by(&:custom_filter).map do |filter, keywords|
|
||||||
|
keywords.map! do |keyword|
|
||||||
|
if keyword.whole_word
|
||||||
|
sb = /\A[[:word:]]/.match?(keyword.keyword) ? '\b' : ''
|
||||||
|
eb = /[[:word:]]\z/.match?(keyword.keyword) ? '\b' : ''
|
||||||
|
|
||||||
|
/(?mix:#{sb}#{Regexp.escape(keyword.keyword)}#{eb})/
|
||||||
|
else
|
||||||
|
/#{Regexp.escape(keyword.keyword)}/i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
[filter, { keywords: Regexp.union(keywords) }]
|
||||||
|
end
|
||||||
|
end.to_a
|
||||||
|
|
||||||
|
active_filters.select { |custom_filter, _| !custom_filter.expired? }
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_cache_invalidation!
|
||||||
|
@should_invalidate_cache = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def invalidate_cache!
|
||||||
|
return unless @should_invalidate_cache
|
||||||
|
@should_invalidate_cache = false
|
||||||
|
|
||||||
|
Rails.cache.delete("filters:v3:#{account_id}")
|
||||||
|
redis.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed))
|
||||||
|
redis.publish("timeline:system:#{account_id}", Oj.dump(event: :filters_changed))
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def clean_up_contexts
|
def clean_up_contexts
|
||||||
self.context = Array(context).map(&:strip).filter_map(&:presence)
|
self.context = Array(context).map(&:strip).filter_map(&:presence)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_cache
|
|
||||||
Rails.cache.delete("filters:#{account_id}")
|
|
||||||
redis.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed))
|
|
||||||
end
|
|
||||||
|
|
||||||
def context_must_be_valid
|
def context_must_be_valid
|
||||||
errors.add(:context, I18n.t('filters.errors.invalid_context')) if context.empty? || context.any? { |c| !VALID_CONTEXTS.include?(c) }
|
errors.add(:context, I18n.t('filters.errors.invalid_context')) if context.empty? || context.any? { |c| !VALID_CONTEXTS.include?(c) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def irreversible_must_be_within_context
|
|
||||||
errors.add(:irreversible, I18n.t('filters.errors.invalid_irreversible')) if irreversible? && !context.include?('home') && !context.include?('notifications')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: custom_filter_keywords
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# custom_filter_id :bigint not null
|
||||||
|
# keyword :text default(""), not null
|
||||||
|
# whole_word :boolean default(TRUE), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class CustomFilterKeyword < ApplicationRecord
|
||||||
|
belongs_to :custom_filter
|
||||||
|
|
||||||
|
validates :keyword, presence: true
|
||||||
|
|
||||||
|
alias_attribute :phrase, :keyword
|
||||||
|
|
||||||
|
before_save :prepare_cache_invalidation!
|
||||||
|
before_destroy :prepare_cache_invalidation!
|
||||||
|
after_commit :invalidate_cache!
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def prepare_cache_invalidation!
|
||||||
|
custom_filter.prepare_cache_invalidation!
|
||||||
|
end
|
||||||
|
|
||||||
|
def invalidate_cache!
|
||||||
|
custom_filter.invalidate_cache!
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,6 +11,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class DomainAllow < ApplicationRecord
|
class DomainAllow < ApplicationRecord
|
||||||
|
include Paginable
|
||||||
include DomainNormalizable
|
include DomainNormalizable
|
||||||
include DomainMaterializable
|
include DomainMaterializable
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ class Notification < ApplicationRecord
|
||||||
poll
|
poll
|
||||||
update
|
update
|
||||||
admin.sign_up
|
admin.sign_up
|
||||||
|
admin.report
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
TARGET_STATUS_INCLUDES_BY_TYPE = {
|
TARGET_STATUS_INCLUDES_BY_TYPE = {
|
||||||
|
@ -46,6 +47,7 @@ class Notification < ApplicationRecord
|
||||||
favourite: [favourite: :status],
|
favourite: [favourite: :status],
|
||||||
poll: [poll: :status],
|
poll: [poll: :status],
|
||||||
update: :status,
|
update: :status,
|
||||||
|
'admin.report': [report: :target_account],
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
belongs_to :account, optional: true
|
belongs_to :account, optional: true
|
||||||
|
@ -58,6 +60,7 @@ class Notification < ApplicationRecord
|
||||||
belongs_to :follow_request, foreign_key: 'activity_id', optional: true
|
belongs_to :follow_request, foreign_key: 'activity_id', optional: true
|
||||||
belongs_to :favourite, foreign_key: 'activity_id', optional: true
|
belongs_to :favourite, foreign_key: 'activity_id', optional: true
|
||||||
belongs_to :poll, foreign_key: 'activity_id', optional: true
|
belongs_to :poll, foreign_key: 'activity_id', optional: true
|
||||||
|
belongs_to :report, foreign_key: 'activity_id', optional: true
|
||||||
|
|
||||||
validates :type, inclusion: { in: TYPES }
|
validates :type, inclusion: { in: TYPES }
|
||||||
|
|
||||||
|
@ -146,7 +149,7 @@ class Notification < ApplicationRecord
|
||||||
return unless new_record?
|
return unless new_record?
|
||||||
|
|
||||||
case activity_type
|
case activity_type
|
||||||
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll'
|
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report'
|
||||||
self.from_account_id = activity&.account_id
|
self.from_account_id = activity&.account_id
|
||||||
when 'Mention'
|
when 'Mention'
|
||||||
self.from_account_id = activity&.status&.account_id
|
self.from_account_id = activity&.status&.account_id
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class DomainAllowPolicy < ApplicationPolicy
|
class DomainAllowPolicy < ApplicationPolicy
|
||||||
|
def index?
|
||||||
|
admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def show?
|
||||||
|
admin?
|
||||||
|
end
|
||||||
|
|
||||||
def create?
|
def create?
|
||||||
admin?
|
admin?
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class FilterResultPresenter < ActiveModelSerializers::Model
|
||||||
|
attributes :filter, :keyword_matches
|
||||||
|
end
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class StatusRelationshipsPresenter
|
class StatusRelationshipsPresenter
|
||||||
attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map,
|
attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map,
|
||||||
:bookmarks_map
|
:bookmarks_map, :filters_map
|
||||||
|
|
||||||
def initialize(statuses, current_account_id = nil, **options)
|
def initialize(statuses, current_account_id = nil, **options)
|
||||||
if current_account_id.nil?
|
if current_account_id.nil?
|
||||||
|
@ -11,12 +11,14 @@ class StatusRelationshipsPresenter
|
||||||
@bookmarks_map = {}
|
@bookmarks_map = {}
|
||||||
@mutes_map = {}
|
@mutes_map = {}
|
||||||
@pins_map = {}
|
@pins_map = {}
|
||||||
|
@filters_map = {}
|
||||||
else
|
else
|
||||||
statuses = statuses.compact
|
statuses = statuses.compact
|
||||||
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact
|
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact
|
||||||
conversation_ids = statuses.filter_map(&:conversation_id).uniq
|
conversation_ids = statuses.filter_map(&:conversation_id).uniq
|
||||||
pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && %w(public unlisted private).include?(s.visibility) }
|
pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && %w(public unlisted private).include?(s.visibility) }
|
||||||
|
|
||||||
|
@filters_map = build_filters_map(statuses, current_account_id).merge(options[:filters_map] || {})
|
||||||
@reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {})
|
@reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {})
|
||||||
@favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {})
|
@favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {})
|
||||||
@bookmarks_map = Status.bookmarks_map(status_ids, current_account_id).merge(options[:bookmarks_map] || {})
|
@bookmarks_map = Status.bookmarks_map(status_ids, current_account_id).merge(options[:bookmarks_map] || {})
|
||||||
|
@ -24,4 +26,24 @@ class StatusRelationshipsPresenter
|
||||||
@pins_map = Status.pins_map(pinnable_status_ids, current_account_id).merge(options[:pins_map] || {})
|
@pins_map = Status.pins_map(pinnable_status_ids, current_account_id).merge(options[:pins_map] || {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_filters_map(statuses, current_account_id)
|
||||||
|
active_filters = CustomFilter.cached_filters_for(current_account_id)
|
||||||
|
|
||||||
|
@filters_map = statuses.each_with_object({}) do |status, h|
|
||||||
|
filter_matches = active_filters.filter_map do |filter, rules|
|
||||||
|
next if rules[:keywords].blank?
|
||||||
|
|
||||||
|
match = rules[:keywords].match(status.proper.searchable_text)
|
||||||
|
FilterResultPresenter.new(filter: filter, keyword_matches: [match.to_s]) unless match.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
unless filter_matches.empty?
|
||||||
|
h[status.id] = filter_matches
|
||||||
|
h[status.reblog_of_id] = filter_matches if status.reblog?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::Admin::DomainAllowSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :domain, :created_at
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class REST::Admin::ReportSerializer < ActiveModel::Serializer
|
class REST::Admin::ReportSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :action_taken, :action_taken_at, :category, :comment,
|
attributes :id, :action_taken, :action_taken_at, :category, :comment,
|
||||||
:created_at, :updated_at
|
:forwarded, :created_at, :updated_at
|
||||||
|
|
||||||
has_one :account, serializer: REST::Admin::AccountSerializer
|
has_one :account, serializer: REST::Admin::AccountSerializer
|
||||||
has_one :target_account, serializer: REST::Admin::AccountSerializer
|
has_one :target_account, serializer: REST::Admin::AccountSerializer
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::FilterKeywordSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :keyword, :whole_word
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,6 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::FilterResultSerializer < ActiveModel::Serializer
|
||||||
|
belongs_to :filter, serializer: REST::FilterSerializer
|
||||||
|
has_many :keyword_matches
|
||||||
|
end
|
|
@ -1,10 +1,14 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::FilterSerializer < ActiveModel::Serializer
|
class REST::FilterSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :phrase, :context, :whole_word, :expires_at,
|
attributes :id, :title, :context, :expires_at, :filter_action
|
||||||
:irreversible
|
has_many :keywords, serializer: REST::FilterKeywordSerializer, if: :rules_requested?
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def rules_requested?
|
||||||
|
instance_options[:rules_requested]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
||||||
|
|
||||||
belongs_to :from_account, key: :account, serializer: REST::AccountSerializer
|
belongs_to :from_account, key: :account, serializer: REST::AccountSerializer
|
||||||
belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer
|
belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer
|
||||||
|
belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
|
@ -13,4 +14,8 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
||||||
def status_type?
|
def status_type?
|
||||||
[:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type)
|
[:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def report_type?
|
||||||
|
object.type == :'admin.report'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::ReportSerializer < ActiveModel::Serializer
|
class REST::ReportSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :action_taken
|
attributes :id, :action_taken, :action_taken_at, :category, :comment,
|
||||||
|
:forwarded, :created_at, :status_ids, :rule_ids
|
||||||
|
|
||||||
|
has_one :target_account, serializer: REST::AccountSerializer
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
|
|
|
@ -14,6 +14,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
attribute :bookmarked, if: :current_user?
|
attribute :bookmarked, if: :current_user?
|
||||||
attribute :pinned, if: :pinnable?
|
attribute :pinned, if: :pinnable?
|
||||||
attribute :local_only if :local?
|
attribute :local_only if :local?
|
||||||
|
has_many :filtered, serializer: REST::FilterResultSerializer, if: :current_user?
|
||||||
|
|
||||||
attribute :content, unless: :source_requested?
|
attribute :content, unless: :source_requested?
|
||||||
attribute :text, if: :source_requested?
|
attribute :text, if: :source_requested?
|
||||||
|
@ -122,6 +123,14 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filtered
|
||||||
|
if instance_options && instance_options[:relationships]
|
||||||
|
instance_options[:relationships].filters_map[object.id] || []
|
||||||
|
else
|
||||||
|
current_user.account.status_matches_filters(object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def pinnable?
|
def pinnable?
|
||||||
current_user? &&
|
current_user? &&
|
||||||
current_user.account_id == object.account_id &&
|
current_user.account_id == object.account_id &&
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::V1::FilterSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :phrase, :context, :whole_word, :expires_at,
|
||||||
|
:irreversible
|
||||||
|
|
||||||
|
delegate :context, :expires_at, to: :custom_filter
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def phrase
|
||||||
|
object.keyword
|
||||||
|
end
|
||||||
|
|
||||||
|
def irreversible
|
||||||
|
custom_filter.irreversible?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def custom_filter
|
||||||
|
object.custom_filter
|
||||||
|
end
|
||||||
|
end
|
|
@ -39,8 +39,8 @@ class ReportService < BaseService
|
||||||
return if @report.unresolved_siblings?
|
return if @report.unresolved_siblings?
|
||||||
|
|
||||||
User.staff.includes(:account).each do |u|
|
User.staff.includes(:account).each do |u|
|
||||||
next unless u.allows_report_emails?
|
LocalNotificationWorker.perform_async(u.account_id, @report.id, 'Report', 'admin.report')
|
||||||
AdminMailer.new_report(u.account, @report).deliver_later
|
AdminMailer.new_report(u.account, @report).deliver_later if u.allows_report_emails?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
.fields-row
|
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
|
||||||
= f.input :phrase, as: :string, wrapper: :with_label, hint: false
|
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
|
||||||
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, include_blank: I18n.t('invites.expires_in_prompt')
|
|
||||||
|
|
||||||
.fields-group
|
|
||||||
= f.input :context, wrapper: :with_block_label, collection: CustomFilter::VALID_CONTEXTS, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: lambda { |context| I18n.t("filters.contexts.#{context}") }, include_blank: false
|
|
||||||
|
|
||||||
%hr.spacer/
|
|
||||||
|
|
||||||
.fields-group
|
|
||||||
= f.input :irreversible, wrapper: :with_label
|
|
||||||
|
|
||||||
.fields-group
|
|
||||||
= f.input :whole_word, wrapper: :with_label
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
.filters-list__item{ class: [filter.expired? && 'expired'] }
|
||||||
|
= link_to edit_filter_path(filter), class: 'filters-list__item__title' do
|
||||||
|
= filter.title
|
||||||
|
|
||||||
|
- if filter.expires?
|
||||||
|
.expiration{ title: t('filters.index.expires_on', date: l(filter.expires_at)) }
|
||||||
|
- if filter.expired?
|
||||||
|
= t('invites.expired')
|
||||||
|
- else
|
||||||
|
= t('filters.index.expires_in', distance: distance_of_time_in_words_to_now(filter.expires_at))
|
||||||
|
|
||||||
|
.filters-list__item__permissions
|
||||||
|
%ul.permissions-list
|
||||||
|
- unless filter.keywords.empty?
|
||||||
|
%li.permissions-list__item
|
||||||
|
.permissions-list__item__icon
|
||||||
|
= fa_icon('paragraph')
|
||||||
|
.permissions-list__item__text
|
||||||
|
.permissions-list__item__text__title
|
||||||
|
= t('filters.index.keywords', count: filter.keywords.size)
|
||||||
|
.permissions-list__item__text__type
|
||||||
|
- keywords = filter.keywords.map(&:keyword)
|
||||||
|
- keywords = keywords.take(5) + ['…'] if keywords.size > 5 # TODO
|
||||||
|
= keywords.join(', ')
|
||||||
|
|
||||||
|
.announcements-list__item__action-bar
|
||||||
|
.announcements-list__item__meta
|
||||||
|
= t('filters.index.contexts', contexts: filter.context.map { |context| I18n.t("filters.contexts.#{context}") }.join(', '))
|
||||||
|
|
||||||
|
%div
|
||||||
|
= table_link_to 'pencil', t('filters.edit.title'), edit_filter_path(filter)
|
||||||
|
= table_link_to 'times', t('filters.index.delete'), filter_path(filter), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
|
@ -0,0 +1,33 @@
|
||||||
|
.fields-row
|
||||||
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
|
= f.input :title, as: :string, wrapper: :with_label, hint: false
|
||||||
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
|
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, include_blank: I18n.t('invites.expires_in_prompt')
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= f.input :context, wrapper: :with_block_label, collection: CustomFilter::VALID_CONTEXTS, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: lambda { |context| I18n.t("filters.contexts.#{context}") }, include_blank: false
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= f.input :filter_action, as: :radio_buttons, collection: %i(warn hide), include_blank: false, wrapper: :with_block_label, label_method: ->(action) { safe_join([t("simple_form.labels.filters.actions.#{action}"), content_tag(:span, t("simple_form.hints.filters.actions.#{action}"), class: 'hint')]) }, hint: t('simple_form.hints.filters.action'), required: true
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
%h4= t('filters.edit.keywords')
|
||||||
|
|
||||||
|
.table-wrapper
|
||||||
|
%table.table.keywords-table
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th= t('simple_form.labels.defaults.phrase')
|
||||||
|
%th= t('simple_form.labels.defaults.whole_word')
|
||||||
|
%th
|
||||||
|
%tbody
|
||||||
|
= f.simple_fields_for :keywords do |keyword|
|
||||||
|
= render 'keyword_fields', f: keyword
|
||||||
|
%tfoot
|
||||||
|
%tr
|
||||||
|
%td{ colspan: 3}
|
||||||
|
= link_to_add_association f, :keywords, class: 'table-action-link', partial: 'keyword_fields', 'data-association-insertion-node': '.keywords-table tbody', 'data-association-insertion-method': 'append' do
|
||||||
|
= safe_join([fa_icon('plus'), t('filters.edit.add_keyword')])
|
|
@ -0,0 +1,8 @@
|
||||||
|
%tr.nested-fields
|
||||||
|
%td= f.input :keyword, as: :string
|
||||||
|
%td
|
||||||
|
.label_input__wrapper= f.input_field :whole_word
|
||||||
|
%td
|
||||||
|
= f.hidden_field :id if f.object&.persisted? # Required so Rails doesn't put the field outside of the <tr/>
|
||||||
|
= link_to_remove_association(f, class: 'table-action-link') do
|
||||||
|
= safe_join([fa_icon('times'), t('filters.index.delete')])
|
|
@ -2,7 +2,7 @@
|
||||||
= t('filters.edit.title')
|
= t('filters.edit.title')
|
||||||
|
|
||||||
= simple_form_for @filter, url: filter_path(@filter), method: :put do |f|
|
= simple_form_for @filter, url: filter_path(@filter), method: :put do |f|
|
||||||
= render 'fields', f: f
|
= render 'filter_fields', f: f
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('generic.save_changes'), type: :submit
|
= f.button :button, t('generic.save_changes'), type: :submit
|
||||||
|
|
|
@ -7,18 +7,5 @@
|
||||||
- if @filters.empty?
|
- if @filters.empty?
|
||||||
%div.muted-hint.center-text= t 'filters.index.empty'
|
%div.muted-hint.center-text= t 'filters.index.empty'
|
||||||
- else
|
- else
|
||||||
.table-wrapper
|
.applications-list
|
||||||
%table.table
|
= render partial: 'filter', collection: @filters
|
||||||
%thead
|
|
||||||
%tr
|
|
||||||
%th= t('simple_form.labels.defaults.phrase')
|
|
||||||
%th= t('simple_form.labels.defaults.context')
|
|
||||||
%th
|
|
||||||
%tbody
|
|
||||||
- @filters.each do |filter|
|
|
||||||
%tr
|
|
||||||
%td= filter.phrase
|
|
||||||
%td= filter.context.map { |context| I18n.t("filters.contexts.#{context}") }.join(', ')
|
|
||||||
%td
|
|
||||||
= table_link_to 'pencil', t('filters.edit.title'), edit_filter_path(filter)
|
|
||||||
= table_link_to 'times', t('filters.index.delete'), filter_path(filter), method: :delete
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
= t('filters.new.title')
|
= t('filters.new.title')
|
||||||
|
|
||||||
= simple_form_for @filter, url: filters_path do |f|
|
= simple_form_for @filter, url: filters_path do |f|
|
||||||
= render 'fields', f: f
|
= render 'filter_fields', f: f
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('filters.new.title'), type: :submit
|
= f.button :button, t('filters.new.save'), type: :submit
|
||||||
|
|
|
@ -21,6 +21,14 @@ id:
|
||||||
username:
|
username:
|
||||||
invalid: hanya boleh berisi huruf, angka, dan garis bawah
|
invalid: hanya boleh berisi huruf, angka, dan garis bawah
|
||||||
reserved: sudah dipesan
|
reserved: sudah dipesan
|
||||||
|
admin/webhook:
|
||||||
|
attributes:
|
||||||
|
url:
|
||||||
|
invalid: bukan URL valid
|
||||||
|
doorkeeper/application:
|
||||||
|
attributes:
|
||||||
|
website:
|
||||||
|
invalid: bukan URL valid
|
||||||
status:
|
status:
|
||||||
attributes:
|
attributes:
|
||||||
reblog:
|
reblog:
|
||||||
|
|
|
@ -21,6 +21,14 @@ is:
|
||||||
username:
|
username:
|
||||||
invalid: má aðeins innihalda bókstafi, tölur og undirstrik
|
invalid: má aðeins innihalda bókstafi, tölur og undirstrik
|
||||||
reserved: er frátekið
|
reserved: er frátekið
|
||||||
|
admin/webhook:
|
||||||
|
attributes:
|
||||||
|
url:
|
||||||
|
invalid: er ekki gild vefslóð
|
||||||
|
doorkeeper/application:
|
||||||
|
attributes:
|
||||||
|
website:
|
||||||
|
invalid: er ekki gild vefslóð
|
||||||
status:
|
status:
|
||||||
attributes:
|
attributes:
|
||||||
reblog:
|
reblog:
|
||||||
|
|
|
@ -21,6 +21,14 @@ ko:
|
||||||
username:
|
username:
|
||||||
invalid: 영문자, 숫자, _만 사용 가능
|
invalid: 영문자, 숫자, _만 사용 가능
|
||||||
reserved: 이미 예약되어 있습니다
|
reserved: 이미 예약되어 있습니다
|
||||||
|
admin/webhook:
|
||||||
|
attributes:
|
||||||
|
url:
|
||||||
|
invalid: 올바른 URL이 아닙니다
|
||||||
|
doorkeeper/application:
|
||||||
|
attributes:
|
||||||
|
website:
|
||||||
|
invalid: 올바른 URL이 아닙니다
|
||||||
status:
|
status:
|
||||||
attributes:
|
attributes:
|
||||||
reblog:
|
reblog:
|
||||||
|
|
|
@ -21,6 +21,14 @@ nl:
|
||||||
username:
|
username:
|
||||||
invalid: alleen letters, nummers en underscores
|
invalid: alleen letters, nummers en underscores
|
||||||
reserved: gereserveerd
|
reserved: gereserveerd
|
||||||
|
admin/webhook:
|
||||||
|
attributes:
|
||||||
|
url:
|
||||||
|
invalid: is een ongeldige URL
|
||||||
|
doorkeeper/application:
|
||||||
|
attributes:
|
||||||
|
website:
|
||||||
|
invalid: is een ongeldige URL
|
||||||
status:
|
status:
|
||||||
attributes:
|
attributes:
|
||||||
reblog:
|
reblog:
|
||||||
|
|
|
@ -21,6 +21,14 @@ th:
|
||||||
username:
|
username:
|
||||||
invalid: ต้องมีเฉพาะตัวอักษร, ตัวเลข และขีดล่างเท่านั้น
|
invalid: ต้องมีเฉพาะตัวอักษร, ตัวเลข และขีดล่างเท่านั้น
|
||||||
reserved: ถูกสงวนไว้
|
reserved: ถูกสงวนไว้
|
||||||
|
admin/webhook:
|
||||||
|
attributes:
|
||||||
|
url:
|
||||||
|
invalid: ไม่ใช่ URL ที่ถูกต้อง
|
||||||
|
doorkeeper/application:
|
||||||
|
attributes:
|
||||||
|
website:
|
||||||
|
invalid: ไม่ใช่ URL ที่ถูกต้อง
|
||||||
status:
|
status:
|
||||||
attributes:
|
attributes:
|
||||||
reblog:
|
reblog:
|
||||||
|
|
|
@ -1124,15 +1124,24 @@ en:
|
||||||
public: Public timelines
|
public: Public timelines
|
||||||
thread: Conversations
|
thread: Conversations
|
||||||
edit:
|
edit:
|
||||||
|
add_keyword: Add keyword
|
||||||
|
keywords: Keywords
|
||||||
title: Edit filter
|
title: Edit filter
|
||||||
errors:
|
errors:
|
||||||
|
deprecated_api_multiple_keywords: These parameters cannot be changed from this application because they apply to more than one filter keyword. Use a more recent application or the web interface.
|
||||||
invalid_context: None or invalid context supplied
|
invalid_context: None or invalid context supplied
|
||||||
invalid_irreversible: Irreversible filtering only works with home or notifications context
|
|
||||||
index:
|
index:
|
||||||
|
contexts: Filters in %{contexts}
|
||||||
delete: Delete
|
delete: Delete
|
||||||
empty: You have no filters.
|
empty: You have no filters.
|
||||||
|
expires_in: Expires in %{distance}
|
||||||
|
expires_on: Expires on %{date}
|
||||||
|
keywords:
|
||||||
|
one: "%{count} keyword"
|
||||||
|
other: "%{count} keywords"
|
||||||
title: Filters
|
title: Filters
|
||||||
new:
|
new:
|
||||||
|
save: Save new filter
|
||||||
title: Add new filter
|
title: Add new filter
|
||||||
footer:
|
footer:
|
||||||
developers: Developers
|
developers: Developers
|
||||||
|
@ -1251,6 +1260,8 @@ en:
|
||||||
copy_account_note_text: 'This user moved from %{acct}, here were your previous notes about them:'
|
copy_account_note_text: 'This user moved from %{acct}, here were your previous notes about them:'
|
||||||
notification_mailer:
|
notification_mailer:
|
||||||
admin:
|
admin:
|
||||||
|
report:
|
||||||
|
subject: "%{name} submitted a report"
|
||||||
sign_up:
|
sign_up:
|
||||||
subject: "%{name} signed up"
|
subject: "%{name} signed up"
|
||||||
digest:
|
digest:
|
||||||
|
|
|
@ -22,9 +22,9 @@ eo:
|
||||||
federation_hint_html: Per konto ĉe %{instance}, vi povos sekvi homojn ĉe iu ajn Mastodon nodo kaj preter.
|
federation_hint_html: Per konto ĉe %{instance}, vi povos sekvi homojn ĉe iu ajn Mastodon nodo kaj preter.
|
||||||
get_apps: Provu telefonan aplikaĵon
|
get_apps: Provu telefonan aplikaĵon
|
||||||
hosted_on: "%{domain} estas nodo de Mastodon"
|
hosted_on: "%{domain} estas nodo de Mastodon"
|
||||||
instance_actor_flash: |
|
instance_actor_flash: 'Ĉi tiu konto estas virtuala aganto uzata por reprezenti la servilon mem kaj neniun individuan uzanton. Ĝi estas uzata por celoj de la federaĵo kaj devas ne esti brokita se vi ne volas bloki la tutan servilon, tiuokaze vi devas uzi blokadon de domajno.
|
||||||
Ĉi tiu konto estas virtuala ulo uzata por reprezenti la servilon mem kaj ne iun apartan uzanton.
|
|
||||||
Ĝi estas uzata por frataraj celoj kaj ĝi ne devus esti blokita krom se vi volas bloki la tutan servilon, tiuokaze vi devus uzi domajnan blokadon.
|
'
|
||||||
learn_more: Lerni pli
|
learn_more: Lerni pli
|
||||||
logout_before_registering: Vi jam salutis.
|
logout_before_registering: Vi jam salutis.
|
||||||
privacy_policy: Privateca politiko
|
privacy_policy: Privateca politiko
|
||||||
|
@ -179,8 +179,8 @@ eo:
|
||||||
sensitized: markita tikla
|
sensitized: markita tikla
|
||||||
shared_inbox_url: URL de kunhavigita leterkesto
|
shared_inbox_url: URL de kunhavigita leterkesto
|
||||||
show:
|
show:
|
||||||
created_reports: Kreitaj signaloj
|
created_reports: Kreitaj raportoj
|
||||||
targeted_reports: Signalitaj de aliaj
|
targeted_reports: Raporitaj de alia
|
||||||
silence: Kaŝi
|
silence: Kaŝi
|
||||||
silenced: Silentigita
|
silenced: Silentigita
|
||||||
statuses: Mesaĝoj
|
statuses: Mesaĝoj
|
||||||
|
@ -1164,10 +1164,10 @@ eo:
|
||||||
one: "%{count} voĉdono"
|
one: "%{count} voĉdono"
|
||||||
other: "%{count} voĉdonoj"
|
other: "%{count} voĉdonoj"
|
||||||
vote: Voĉdoni
|
vote: Voĉdoni
|
||||||
show_more: Malfoldi
|
show_more: Montri pli
|
||||||
show_newer: Montri pli novajn
|
show_newer: Montri pli novajn
|
||||||
show_older: Montri pli malnovajn
|
show_older: Montri pli malnovajn
|
||||||
show_thread: Montri la fadenon
|
show_thread: Montri la mesaĝaron
|
||||||
sign_in_to_participate: Ensaluti por partopreni en la konversacio
|
sign_in_to_participate: Ensaluti por partopreni en la konversacio
|
||||||
title: "%{name}: “%{quote}”"
|
title: "%{name}: “%{quote}”"
|
||||||
visibilities:
|
visibilities:
|
||||||
|
@ -1255,7 +1255,7 @@ eo:
|
||||||
review_preferences_action: Ŝanĝi preferojn
|
review_preferences_action: Ŝanĝi preferojn
|
||||||
review_preferences_step: Estu certa ke vi agordis viajn preferojn, kiel kiujn retmesaĝojn vi ŝatus ricevi, aŭ kiun dekomencan privatecan nivelon vi ŝatus ke viaj mesaĝoj havu. Se tio ne ĝenas vin, vi povas ebligi aŭtomatan ekigon de GIF-oj.
|
review_preferences_step: Estu certa ke vi agordis viajn preferojn, kiel kiujn retmesaĝojn vi ŝatus ricevi, aŭ kiun dekomencan privatecan nivelon vi ŝatus ke viaj mesaĝoj havu. Se tio ne ĝenas vin, vi povas ebligi aŭtomatan ekigon de GIF-oj.
|
||||||
subject: Bonvenon en Mastodon
|
subject: Bonvenon en Mastodon
|
||||||
tip_federated_timeline: La fratara templinio estas antaŭvido de la reto de Mastodon. Sed ĝi enhavas nur homojn, kiuj estas sekvataj de aliaj homoj de via nodo, do ĝi ne estas kompleta.
|
tip_federated_timeline: La federata templinio estas rekta vido de la reto de Mastodon. Sed ĝi inkluzivas nur personojn kiujn via najbaroj abonas, do ĝi ne estas kompleta.
|
||||||
tip_following: Vi dekomence sekvas la administrantojn de via servilo. Por trovi pli da interesaj homoj, rigardu la lokan kaj frataran templiniojn.
|
tip_following: Vi dekomence sekvas la administrantojn de via servilo. Por trovi pli da interesaj homoj, rigardu la lokan kaj frataran templiniojn.
|
||||||
tip_local_timeline: La loka templinio estas antaŭvido de la homoj en %{instance}. Ĉi tiuj estas viaj apudaj najbaroj!
|
tip_local_timeline: La loka templinio estas antaŭvido de la homoj en %{instance}. Ĉi tiuj estas viaj apudaj najbaroj!
|
||||||
tip_mobile_webapp: Se via telefona retumilo proponas al vi aldoni Mastodon al via hejma ekrano, vi povas ricevi puŝsciigojn. Tio multmaniere funkcias kiel operaciuma aplikaĵo!
|
tip_mobile_webapp: Se via telefona retumilo proponas al vi aldoni Mastodon al via hejma ekrano, vi povas ricevi puŝsciigojn. Tio multmaniere funkcias kiel operaciuma aplikaĵo!
|
||||||
|
|
|
@ -941,7 +941,7 @@ gl:
|
||||||
warning: Ten moito tino con estos datos. Non os compartas nunca con ninguén!
|
warning: Ten moito tino con estos datos. Non os compartas nunca con ninguén!
|
||||||
your_token: O seu testemuño de acceso
|
your_token: O seu testemuño de acceso
|
||||||
auth:
|
auth:
|
||||||
apply_for_account: Solicite un convite
|
apply_for_account: Solicita un convite
|
||||||
change_password: Contrasinal
|
change_password: Contrasinal
|
||||||
checkbox_agreement_html: Acepto as <a href="%{rules_path}" target="_blank">regras do servidor</a> e os <a href="%{terms_path}" target="_blank">termos do servizo</a>
|
checkbox_agreement_html: Acepto as <a href="%{rules_path}" target="_blank">regras do servidor</a> e os <a href="%{terms_path}" target="_blank">termos do servizo</a>
|
||||||
checkbox_agreement_without_rules_html: Acepto os <a href="%{terms_path}" target="_blank">termos do servizo</a>
|
checkbox_agreement_without_rules_html: Acepto os <a href="%{terms_path}" target="_blank">termos do servizo</a>
|
||||||
|
@ -954,7 +954,7 @@ gl:
|
||||||
didnt_get_confirmation: Non recibiches as instruccións de confirmación?
|
didnt_get_confirmation: Non recibiches as instruccións de confirmación?
|
||||||
dont_have_your_security_key: "¿Non tes a túa chave de seguridade?"
|
dont_have_your_security_key: "¿Non tes a túa chave de seguridade?"
|
||||||
forgot_password: Non lembras o contrasinal?
|
forgot_password: Non lembras o contrasinal?
|
||||||
invalid_reset_password_token: O testemuño para restablecer o contrasinal non é válido ou caducou. Por favor solicite un novo.
|
invalid_reset_password_token: O token para restablecer o contrasinal non é válido ou caducou. Por favor solicita un novo.
|
||||||
link_to_otp: Escribe o código do segundo factor do móbil ou un código de recuperación
|
link_to_otp: Escribe o código do segundo factor do móbil ou un código de recuperación
|
||||||
link_to_webauth: Usa o teu dispositivo de chave de seguridade
|
link_to_webauth: Usa o teu dispositivo de chave de seguridade
|
||||||
log_in_with: Accede con
|
log_in_with: Accede con
|
||||||
|
|
|
@ -853,13 +853,25 @@ is:
|
||||||
empty: Þú hefur ekki enn skilgreint neinar aðvaranaforstillingar.
|
empty: Þú hefur ekki enn skilgreint neinar aðvaranaforstillingar.
|
||||||
title: Sýsla með forstilltar aðvaranir
|
title: Sýsla með forstilltar aðvaranir
|
||||||
webhooks:
|
webhooks:
|
||||||
|
add_new: Bæta við endapunkti
|
||||||
delete: Eyða
|
delete: Eyða
|
||||||
|
description_html: "<strong>webhook-vefkrækja</strong> gerir Mastodon kleift að ýta <strong>rauntíma-tilkynningum</strong> um valda atburði til þinna eigin forrita, þannig að þau forrit getir <strong>sett sjálfvirk viðbrögð í gang</strong>."
|
||||||
disable: Gera óvirkt
|
disable: Gera óvirkt
|
||||||
disabled: Óvirkt
|
disabled: Óvirkt
|
||||||
|
edit: Breyta endapunkti
|
||||||
|
empty: Þú ert ekki enn búin/n að stilla neina endapunkta á webhook-vefkrækjum.
|
||||||
enable: Virkja
|
enable: Virkja
|
||||||
enabled: Virkt
|
enabled: Virkt
|
||||||
|
enabled_events:
|
||||||
|
one: 1 virkjaður atburður
|
||||||
|
other: "%{count} virkjaðir atburðir"
|
||||||
events: Atburðir
|
events: Atburðir
|
||||||
|
new: Ný webhook-vefkrækja
|
||||||
|
rotate_secret: Skipta um leyniteikn
|
||||||
|
secret: Leyniteikn undirritunar
|
||||||
status: Staða
|
status: Staða
|
||||||
|
title: Webhook-vefkrækjur
|
||||||
|
webhook: Webhook-vefkrækja
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
|
|
|
@ -840,11 +840,22 @@ ko:
|
||||||
webhooks:
|
webhooks:
|
||||||
add_new: 엔드포인트 추가
|
add_new: 엔드포인트 추가
|
||||||
delete: 삭제
|
delete: 삭제
|
||||||
|
description_html: "<strong>웹훅</strong>은 선택한 이벤트에 대해 마스토돈이 <strong>실시간 알림</strong>을 각자의 응용프로그램에게 보냄으로서, 당신의 응용프로그램이 <strong>자동으로 반응</strong>을 할 수 있도록 만듧니다."
|
||||||
disable: 비활성화
|
disable: 비활성화
|
||||||
disabled: 비활성화됨
|
disabled: 비활성화됨
|
||||||
edit: 엔드포인트 수정
|
edit: 엔드포인트 수정
|
||||||
|
empty: 아직 설정한 웹훅 엔드포인트가 없습니다.
|
||||||
enable: 활성화
|
enable: 활성화
|
||||||
enabled: 활성화됨
|
enabled: 활성화됨
|
||||||
|
enabled_events:
|
||||||
|
other: "%{count}개의 이벤트가 활성화되어 있습니다"
|
||||||
|
events: 이벤트
|
||||||
|
new: 새 웹훅
|
||||||
|
rotate_secret: 비밀키 회전
|
||||||
|
secret: 비밀키 서명
|
||||||
|
status: 상태
|
||||||
|
title: 웹훅
|
||||||
|
webhook: 웹훅
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
|
|
|
@ -36,7 +36,7 @@ nl:
|
||||||
one: toot
|
one: toot
|
||||||
other: berichten
|
other: berichten
|
||||||
status_count_before: Zij schreven
|
status_count_before: Zij schreven
|
||||||
tagline: Gedecentraliseerd sociaal netwerk
|
tagline: Decentraal sociaal netwerk
|
||||||
terms: Gebruiksvoorwaarden
|
terms: Gebruiksvoorwaarden
|
||||||
unavailable_content: Gemodereerde servers
|
unavailable_content: Gemodereerde servers
|
||||||
unavailable_content_description:
|
unavailable_content_description:
|
||||||
|
@ -1306,6 +1306,9 @@ nl:
|
||||||
subject: Jouw archief staat klaar om te worden gedownload
|
subject: Jouw archief staat klaar om te worden gedownload
|
||||||
title: Archief ophalen
|
title: Archief ophalen
|
||||||
warning:
|
warning:
|
||||||
|
explanation:
|
||||||
|
mark_statuses_as_sensitive: Sommige van jouw berichten zijn als gevoelig gemarkeerd door de moderatoren van %{instance}. Dit betekent dat mensen op de media in de berichten moeten klikken/tikken om deze weer te geven. Je kunt media in de toekomst ook zelf als gevoelig markeren.
|
||||||
|
sensitive: Vanaf nu worden al jouw geüploade media als gevoelig gemarkeerd en verborgen achter een waarschuwing.
|
||||||
subject:
|
subject:
|
||||||
disable: Jouw account %{acct} is bevroren
|
disable: Jouw account %{acct} is bevroren
|
||||||
none: Waarschuwing voor %{acct}
|
none: Waarschuwing voor %{acct}
|
||||||
|
|
|
@ -68,6 +68,11 @@ en:
|
||||||
with_dns_records: An attempt to resolve the given domain's DNS records will be made and the results will also be blocked
|
with_dns_records: An attempt to resolve the given domain's DNS records will be made and the results will also be blocked
|
||||||
featured_tag:
|
featured_tag:
|
||||||
name: 'You might want to use one of these:'
|
name: 'You might want to use one of these:'
|
||||||
|
filters:
|
||||||
|
action: Chose which action to perform when a post matches the filter
|
||||||
|
actions:
|
||||||
|
hide: Completely hide the filtered content, behaving as if it did not exist
|
||||||
|
warn: Hide the filtered content behind a warning mentioning the filter's title
|
||||||
form_challenge:
|
form_challenge:
|
||||||
current_password: You are entering a secure area
|
current_password: You are entering a secure area
|
||||||
imports:
|
imports:
|
||||||
|
@ -181,6 +186,7 @@ en:
|
||||||
setting_use_pending_items: Slow mode
|
setting_use_pending_items: Slow mode
|
||||||
severity: Severity
|
severity: Severity
|
||||||
sign_in_token_attempt: Security code
|
sign_in_token_attempt: Security code
|
||||||
|
title: Title
|
||||||
type: Import type
|
type: Import type
|
||||||
username: Username
|
username: Username
|
||||||
username_or_email: Username or Email
|
username_or_email: Username or Email
|
||||||
|
@ -189,6 +195,10 @@ en:
|
||||||
with_dns_records: Include MX records and IPs of the domain
|
with_dns_records: Include MX records and IPs of the domain
|
||||||
featured_tag:
|
featured_tag:
|
||||||
name: Hashtag
|
name: Hashtag
|
||||||
|
filters:
|
||||||
|
actions:
|
||||||
|
hide: Hide completely
|
||||||
|
warn: Hide with a warning
|
||||||
interactions:
|
interactions:
|
||||||
must_be_follower: Block notifications from non-followers
|
must_be_follower: Block notifications from non-followers
|
||||||
must_be_following: Block notifications from people you don't follow
|
must_be_following: Block notifications from people you don't follow
|
||||||
|
|
|
@ -57,7 +57,7 @@ gl:
|
||||||
setting_hide_network: Non se mostrará no teu perfil quen te segue e a quen estás a seguir
|
setting_hide_network: Non se mostrará no teu perfil quen te segue e a quen estás a seguir
|
||||||
setting_noindex: Afecta ao teu perfil público e páxinas de publicación
|
setting_noindex: Afecta ao teu perfil público e páxinas de publicación
|
||||||
setting_show_application: A aplicación que estás a utilizar para enviar publicacións mostrarase na vista detallada da publicación
|
setting_show_application: A aplicación que estás a utilizar para enviar publicacións mostrarase na vista detallada da publicación
|
||||||
setting_use_blurhash: Os gradientes toman as cores da imaxe oculta pero esborranchando todos os detalles
|
setting_use_blurhash: Os gradientes toman as cores da imaxe oculta pero esvaecendo tódolos detalles
|
||||||
setting_use_pending_items: Agochar actualizacións da cronoloxía tras un click no lugar de desprazar automáticamente os comentarios
|
setting_use_pending_items: Agochar actualizacións da cronoloxía tras un click no lugar de desprazar automáticamente os comentarios
|
||||||
username: O teu nome de usuaria será único en %{domain}
|
username: O teu nome de usuaria será único en %{domain}
|
||||||
whole_word: Se a chave ou frase de paso é só alfanumérica, só se aplicará se concorda a palabra completa
|
whole_word: Se a chave ou frase de paso é só alfanumérica, só se aplicará se concorda a palabra completa
|
||||||
|
@ -177,7 +177,7 @@ gl:
|
||||||
setting_theme: Decorado da instancia
|
setting_theme: Decorado da instancia
|
||||||
setting_trends: Mostrar as tendencias de hoxe
|
setting_trends: Mostrar as tendencias de hoxe
|
||||||
setting_unfollow_modal: Solicitar confirmación antes de deixar de seguir alguén
|
setting_unfollow_modal: Solicitar confirmación antes de deixar de seguir alguén
|
||||||
setting_use_blurhash: Mostrar gradientes coloridos para medios ocultos
|
setting_use_blurhash: Mostrar gradientes coloridos para multimedia oculto
|
||||||
setting_use_pending_items: Modo lento
|
setting_use_pending_items: Modo lento
|
||||||
severity: Severidade
|
severity: Severidade
|
||||||
sign_in_token_attempt: Código de seguridade
|
sign_in_token_attempt: Código de seguridade
|
||||||
|
|
|
@ -91,6 +91,9 @@ is:
|
||||||
name: Þú getur aðeins breytt stafstöði mill há-/lágstafa, til gæmis til að gera þetta læsilegra
|
name: Þú getur aðeins breytt stafstöði mill há-/lágstafa, til gæmis til að gera þetta læsilegra
|
||||||
user:
|
user:
|
||||||
chosen_languages: Þegar merkt er við þetta, birtast einungis færslur á völdum tungumálum á opinberum tímalínum
|
chosen_languages: Þegar merkt er við þetta, birtast einungis færslur á völdum tungumálum á opinberum tímalínum
|
||||||
|
webhook:
|
||||||
|
events: Veldu atburði sem á að senda
|
||||||
|
url: Hvert atburðir verða sendir
|
||||||
labels:
|
labels:
|
||||||
account:
|
account:
|
||||||
fields:
|
fields:
|
||||||
|
@ -219,6 +222,9 @@ is:
|
||||||
name: Myllumerki
|
name: Myllumerki
|
||||||
trendable: Leyfa þessu myllumerki að birtast undir tilhneigingum
|
trendable: Leyfa þessu myllumerki að birtast undir tilhneigingum
|
||||||
usable: Leyfa færslum að nota þetta myllumerki
|
usable: Leyfa færslum að nota þetta myllumerki
|
||||||
|
webhook:
|
||||||
|
events: Virkjaðir atburðir
|
||||||
|
url: Slóð á endapunkt
|
||||||
'no': Nei
|
'no': Nei
|
||||||
recommended: Mælt með
|
recommended: Mælt með
|
||||||
required:
|
required:
|
||||||
|
|
|
@ -91,6 +91,9 @@ ko:
|
||||||
name: 읽기 쉽게하기 위한 글자의 대소문자만 변경할 수 있습니다.
|
name: 읽기 쉽게하기 위한 글자의 대소문자만 변경할 수 있습니다.
|
||||||
user:
|
user:
|
||||||
chosen_languages: 체크하면, 선택 된 언어로 작성된 게시물들만 공개 타임라인에 보여집니다
|
chosen_languages: 체크하면, 선택 된 언어로 작성된 게시물들만 공개 타임라인에 보여집니다
|
||||||
|
webhook:
|
||||||
|
events: 전송할 이벤트를 선택하세요
|
||||||
|
url: 이벤트가 어디로 전송될 지
|
||||||
labels:
|
labels:
|
||||||
account:
|
account:
|
||||||
fields:
|
fields:
|
||||||
|
@ -219,6 +222,9 @@ ko:
|
||||||
name: 해시태그
|
name: 해시태그
|
||||||
trendable: 이 해시태그가 유행에 보여지도록 허용
|
trendable: 이 해시태그가 유행에 보여지도록 허용
|
||||||
usable: 이 해시태그를 게시물에 사용 가능하도록 허용
|
usable: 이 해시태그를 게시물에 사용 가능하도록 허용
|
||||||
|
webhook:
|
||||||
|
events: 활성화된 이벤트
|
||||||
|
url: 엔드포인트 URL
|
||||||
'no': 아니오
|
'no': 아니오
|
||||||
recommended: 추천함
|
recommended: 추천함
|
||||||
required:
|
required:
|
||||||
|
|
|
@ -201,6 +201,8 @@ nl:
|
||||||
mention: Wanneer iemand jou heeft vermeld
|
mention: Wanneer iemand jou heeft vermeld
|
||||||
pending_account: Wanneer een nieuw account moet worden beoordeeld
|
pending_account: Wanneer een nieuw account moet worden beoordeeld
|
||||||
reblog: Wanneer iemand jouw bericht heeft geboost
|
reblog: Wanneer iemand jouw bericht heeft geboost
|
||||||
|
report: Nieuwe rapportage is ingediend
|
||||||
|
trending_tag: Nieuwe trend vereist beoordeling
|
||||||
rule:
|
rule:
|
||||||
text: Regel
|
text: Regel
|
||||||
tag:
|
tag:
|
||||||
|
@ -208,6 +210,8 @@ nl:
|
||||||
name: Hashtag
|
name: Hashtag
|
||||||
trendable: Toestaan dat deze hashtag onder trends te zien valt
|
trendable: Toestaan dat deze hashtag onder trends te zien valt
|
||||||
usable: Toestaan dat deze hashtag in berichten gebruikt mag worden
|
usable: Toestaan dat deze hashtag in berichten gebruikt mag worden
|
||||||
|
webhook:
|
||||||
|
url: Eindpunt URL
|
||||||
'no': Nee
|
'no': Nee
|
||||||
recommended: Aanbevolen
|
recommended: Aanbevolen
|
||||||
required:
|
required:
|
||||||
|
|
|
@ -88,6 +88,8 @@ th:
|
||||||
name: คุณสามารถเปลี่ยนได้เฉพาะตัวพิมพ์ใหญ่เล็กของตัวอักษรเท่านั้น ตัวอย่างเช่น เพื่อทำให้ตัวอักษรอ่านได้ง่ายขึ้น
|
name: คุณสามารถเปลี่ยนได้เฉพาะตัวพิมพ์ใหญ่เล็กของตัวอักษรเท่านั้น ตัวอย่างเช่น เพื่อทำให้ตัวอักษรอ่านได้ง่ายขึ้น
|
||||||
user:
|
user:
|
||||||
chosen_languages: เมื่อกาเครื่องหมาย จะแสดงเฉพาะโพสต์ในภาษาที่เลือกในเส้นเวลาสาธารณะเท่านั้น
|
chosen_languages: เมื่อกาเครื่องหมาย จะแสดงเฉพาะโพสต์ในภาษาที่เลือกในเส้นเวลาสาธารณะเท่านั้น
|
||||||
|
webhook:
|
||||||
|
events: เลือกเหตุการณ์ที่จะส่ง
|
||||||
labels:
|
labels:
|
||||||
account:
|
account:
|
||||||
fields:
|
fields:
|
||||||
|
@ -214,6 +216,8 @@ th:
|
||||||
name: แฮชแท็ก
|
name: แฮชแท็ก
|
||||||
trendable: อนุญาตให้แฮชแท็กนี้ปรากฏภายใต้แนวโน้ม
|
trendable: อนุญาตให้แฮชแท็กนี้ปรากฏภายใต้แนวโน้ม
|
||||||
usable: อนุญาตให้โพสต์ใช้แฮชแท็กนี้
|
usable: อนุญาตให้โพสต์ใช้แฮชแท็กนี้
|
||||||
|
webhook:
|
||||||
|
url: URL ปลายทาง
|
||||||
'no': ไม่
|
'no': ไม่
|
||||||
recommended: แนะนำ
|
recommended: แนะนำ
|
||||||
required:
|
required:
|
||||||
|
|
|
@ -34,6 +34,7 @@ th:
|
||||||
status_count_after:
|
status_count_after:
|
||||||
other: โพสต์
|
other: โพสต์
|
||||||
status_count_before: ผู้เผยแพร่
|
status_count_before: ผู้เผยแพร่
|
||||||
|
tagline: เครือข่ายสังคมแบบกระจายศูนย์
|
||||||
terms: เงื่อนไขการให้บริการ
|
terms: เงื่อนไขการให้บริการ
|
||||||
unavailable_content: เซิร์ฟเวอร์ที่มีการควบคุม
|
unavailable_content: เซิร์ฟเวอร์ที่มีการควบคุม
|
||||||
unavailable_content_description:
|
unavailable_content_description:
|
||||||
|
@ -782,6 +783,16 @@ th:
|
||||||
edit_preset: แก้ไขคำเตือนที่ตั้งไว้ล่วงหน้า
|
edit_preset: แก้ไขคำเตือนที่ตั้งไว้ล่วงหน้า
|
||||||
empty: คุณยังไม่ได้กำหนดคำเตือนที่ตั้งไว้ล่วงหน้าใด ๆ
|
empty: คุณยังไม่ได้กำหนดคำเตือนที่ตั้งไว้ล่วงหน้าใด ๆ
|
||||||
title: จัดการคำเตือนที่ตั้งไว้ล่วงหน้า
|
title: จัดการคำเตือนที่ตั้งไว้ล่วงหน้า
|
||||||
|
webhooks:
|
||||||
|
add_new: เพิ่มปลายทาง
|
||||||
|
delete: ลบ
|
||||||
|
disable: ปิดใช้งาน
|
||||||
|
disabled: ปิดใช้งานอยู่
|
||||||
|
edit: แก้ไขปลายทาง
|
||||||
|
enable: เปิดใช้งาน
|
||||||
|
enabled: ใช้งานอยู่
|
||||||
|
events: เหตุการณ์
|
||||||
|
status: สถานะ
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
|
@ -1248,6 +1259,10 @@ th:
|
||||||
reports:
|
reports:
|
||||||
errors:
|
errors:
|
||||||
invalid_rules: ไม่ได้อ้างอิงกฎที่ถูกต้อง
|
invalid_rules: ไม่ได้อ้างอิงกฎที่ถูกต้อง
|
||||||
|
rss:
|
||||||
|
content_warning: 'คำเตือนเนื้อหา:'
|
||||||
|
descriptions:
|
||||||
|
account: โพสต์สาธารณะจาก @%{acct}
|
||||||
scheduled_statuses:
|
scheduled_statuses:
|
||||||
too_soon: วันที่ตามกำหนดการต้องอยู่ในอนาคต
|
too_soon: วันที่ตามกำหนดการต้องอยู่ในอนาคต
|
||||||
sessions:
|
sessions:
|
||||||
|
|
|
@ -862,6 +862,9 @@ tr:
|
||||||
empty: Henüz yapılandırılmış bir web kancanız yok.
|
empty: Henüz yapılandırılmış bir web kancanız yok.
|
||||||
enable: Etkinleştir
|
enable: Etkinleştir
|
||||||
enabled: Etkin
|
enabled: Etkin
|
||||||
|
enabled_events:
|
||||||
|
one: 1 aktif etkinlik
|
||||||
|
other: "%{count} aktif etkinlik"
|
||||||
events: Olaylar
|
events: Olaylar
|
||||||
new: Yeni web kancası
|
new: Yeni web kancası
|
||||||
rotate_secret: Gizi döndür
|
rotate_secret: Gizi döndür
|
||||||
|
|
|
@ -646,6 +646,7 @@ uk:
|
||||||
placeholder: Опишіть, які дії були виконані, або інші зміни, що стосуються справи...
|
placeholder: Опишіть, які дії були виконані, або інші зміни, що стосуються справи...
|
||||||
title: Примітки
|
title: Примітки
|
||||||
notes_description_html: Переглядайте та залишайте примітки для інших модераторів та для себе на майбутнє
|
notes_description_html: Переглядайте та залишайте примітки для інших модераторів та для себе на майбутнє
|
||||||
|
quick_actions_description_html: 'Виберіть швидку дію або гортайте вниз, щоб побачити матеріал, на який надійшла скарга:'
|
||||||
remote_user_placeholder: віддалений користувач із %{instance}
|
remote_user_placeholder: віддалений користувач із %{instance}
|
||||||
reopen: Перевідкрити скаргу
|
reopen: Перевідкрити скаргу
|
||||||
report: 'Скарга #%{id}'
|
report: 'Скарга #%{id}'
|
||||||
|
@ -896,6 +897,7 @@ uk:
|
||||||
sensitive: щоб позначати їхній обліковий запис делікатним
|
sensitive: щоб позначати їхній обліковий запис делікатним
|
||||||
silence: щоб обмежити їхній обліковий запис
|
silence: щоб обмежити їхній обліковий запис
|
||||||
suspend: щоб призупинити їхній обліковий запис
|
suspend: щоб призупинити їхній обліковий запис
|
||||||
|
body: "%{target} оскаржує модерацію %{action_taken_by} від %{date}, яка була %{type}. Вони написали:"
|
||||||
next_steps: Ви можете схвалити апеляцію, щоб скасувати рішення про модерацію або проігнорувати її.
|
next_steps: Ви можете схвалити апеляцію, щоб скасувати рішення про модерацію або проігнорувати її.
|
||||||
subject: "%{username} апелює до рішення про модерацію на %{instance}"
|
subject: "%{username} апелює до рішення про модерацію на %{instance}"
|
||||||
new_pending_account:
|
new_pending_account:
|
||||||
|
@ -991,6 +993,7 @@ uk:
|
||||||
functional: Ваш обліковий запис повністю робочий.
|
functional: Ваш обліковий запис повністю робочий.
|
||||||
pending: Ваша заява очікує на розгляд нашим персоналом. Це може зайняти деякий час. Ви отримаєте електронний лист, якщо ваша заява буде схвалена.
|
pending: Ваша заява очікує на розгляд нашим персоналом. Це може зайняти деякий час. Ви отримаєте електронний лист, якщо ваша заява буде схвалена.
|
||||||
redirecting_to: Ваш обліковий запис наразі неактивний, тому що він перенаправлений до %{acct}.
|
redirecting_to: Ваш обліковий запис наразі неактивний, тому що він перенаправлений до %{acct}.
|
||||||
|
view_strikes: Переглянути попередні попередження вашому обліковому запису
|
||||||
too_fast: Форму подано занадто швидко, спробуйте ще раз.
|
too_fast: Форму подано занадто швидко, спробуйте ще раз.
|
||||||
trouble_logging_in: Проблема під час входу?
|
trouble_logging_in: Проблема під час входу?
|
||||||
use_security_key: Використовувати ключ безпеки
|
use_security_key: Використовувати ключ безпеки
|
||||||
|
@ -1058,6 +1061,7 @@ uk:
|
||||||
strikes:
|
strikes:
|
||||||
action_taken: Дію виконано
|
action_taken: Дію виконано
|
||||||
appeal: Апеляція
|
appeal: Апеляція
|
||||||
|
appeal_approved: Це попередження було успішно оскаржене і більше не дійсне
|
||||||
appeal_rejected: Апеляцію було відхилено
|
appeal_rejected: Апеляцію було відхилено
|
||||||
appeal_submitted_at: Апеляцію надіслано
|
appeal_submitted_at: Апеляцію надіслано
|
||||||
appealed_msg: Вашу апеляцію було надіслано. Якщо її погодять, вам буде повідомлено про це.
|
appealed_msg: Вашу апеляцію було надіслано. Якщо її погодять, вам буде повідомлено про це.
|
||||||
|
@ -1392,6 +1396,9 @@ uk:
|
||||||
invalid_rules: не посилається на чинні правила
|
invalid_rules: не посилається на чинні правила
|
||||||
rss:
|
rss:
|
||||||
content_warning: 'Попередження про матеріали:'
|
content_warning: 'Попередження про матеріали:'
|
||||||
|
descriptions:
|
||||||
|
account: Загальнодоступні дописи від @%{acct}
|
||||||
|
tag: 'Загальнодоступні дописи позначені #%{hashtag}'
|
||||||
scheduled_statuses:
|
scheduled_statuses:
|
||||||
over_daily_limit: Ви перевищили ліміт в %{limit} запланованих дмухів на сьогодні
|
over_daily_limit: Ви перевищили ліміт в %{limit} запланованих дмухів на сьогодні
|
||||||
over_total_limit: Ви перевищили ліміт в %{limit} запланованих дмухів
|
over_total_limit: Ви перевищили ліміт в %{limit} запланованих дмухів
|
||||||
|
@ -1562,6 +1569,9 @@ uk:
|
||||||
pinned: Закріплений пост
|
pinned: Закріплений пост
|
||||||
reblogged: передмухнув(-ла)
|
reblogged: передмухнув(-ла)
|
||||||
sensitive_content: Дражливий зміст
|
sensitive_content: Дражливий зміст
|
||||||
|
strikes:
|
||||||
|
errors:
|
||||||
|
too_late: Запізно оскаржувати це попередження
|
||||||
tags:
|
tags:
|
||||||
does_not_match_previous_name: не збігається з попереднім ім'ям
|
does_not_match_previous_name: не збігається з попереднім ім'ям
|
||||||
terms:
|
terms:
|
||||||
|
@ -1593,9 +1603,11 @@ uk:
|
||||||
user_mailer:
|
user_mailer:
|
||||||
appeal_approved:
|
appeal_approved:
|
||||||
action: Перейти у ваш обліковий запис
|
action: Перейти у ваш обліковий запис
|
||||||
|
explanation: Оскарження попередження вашому обліковому запису %{strike_date}, яке ви надіслали %{appeal_date} було схвалено. Ваш обліковий запис знову вважається добропорядним.
|
||||||
subject: Вашу апеляцію від %{date} було схвалено
|
subject: Вашу апеляцію від %{date} було схвалено
|
||||||
title: Апеляцію схвалено
|
title: Апеляцію схвалено
|
||||||
appeal_rejected:
|
appeal_rejected:
|
||||||
|
explanation: Оскарження попередження вашому обліковому запису %{strike_date}, яке ви надіслали %{appeal_date} було відхилено.
|
||||||
subject: Вашу апеляцію від %{date} було відхилено
|
subject: Вашу апеляцію від %{date} було відхилено
|
||||||
title: Апеляцію відхилено
|
title: Апеляцію відхилено
|
||||||
backup_ready:
|
backup_ready:
|
||||||
|
@ -1611,12 +1623,14 @@ uk:
|
||||||
title: Новий вхід
|
title: Новий вхід
|
||||||
warning:
|
warning:
|
||||||
appeal: Подати апеляцію
|
appeal: Подати апеляцію
|
||||||
|
appeal_description: Якщо ви вважаєте, що це помилка, ви можете надіслати оскаржити дії персоналу %{instance}.
|
||||||
categories:
|
categories:
|
||||||
spam: Спам
|
spam: Спам
|
||||||
violation: Вміст порушує такі правила спільноти
|
violation: Вміст порушує такі правила спільноти
|
||||||
explanation:
|
explanation:
|
||||||
delete_statuses: Деякі з ваших дописів порушили одне або кілька правил спільноти, і модератори %{instance} видалили їх.
|
delete_statuses: Деякі з ваших дописів порушили одне або кілька правил спільноти, і модератори %{instance} видалили їх.
|
||||||
disable: Ви можете більше не використовувати свій обліковий запис, але ваш профіль та інші дані залишаються недоторканими. Ви можете надіслати запит на створення резервної копії ваших даних, змінити налаштування облікового запису або видалити свій обліковий запис.
|
disable: Ви можете більше не використовувати свій обліковий запис, але ваш профіль та інші дані залишаються недоторканими. Ви можете надіслати запит на створення резервної копії ваших даних, змінити налаштування облікового запису або видалити свій обліковий запис.
|
||||||
|
mark_statuses_as_sensitive: Деякі з ваших дописів модератори %{instance} позначили делікатними. Це означає, що людям потрібно буде торкнутися медіа у дописах перед тим, як буде показано попередній перегляд. Ви можете самостійно позначити медіа делікатним, коли розміщуватимете його в майбутньому.
|
||||||
sensitive: Відтепер усі ваші завантажені медіафайли будуть позначені делікатними й приховані за попередженням.
|
sensitive: Відтепер усі ваші завантажені медіафайли будуть позначені делікатними й приховані за попередженням.
|
||||||
silence: Ви й надалі можете користуватися своїм обліковим записом, але ваші дописи на цьому сервері бачитимуть лише ті люди, які вже стежать за вами, а вас може бути виключено з різних можливостей виявлення. Проте, інші можуть почати стежити за вами вручну.
|
silence: Ви й надалі можете користуватися своїм обліковим записом, але ваші дописи на цьому сервері бачитимуть лише ті люди, які вже стежать за вами, а вас може бути виключено з різних можливостей виявлення. Проте, інші можуть почати стежити за вами вручну.
|
||||||
suspend: Ви більше не можете користуватися своїм обліковим записом, а ваші інші дані більше недоступні. Ви досі можете увійти, щоб надіслати запит на отримання резервної копії своїх даних до повного видалення впродовж приблизно 30 днів, але ми збережемо деякі основні дані, щоб унеможливити ухилення вами від призупинення.
|
suspend: Ви більше не можете користуватися своїм обліковим записом, а ваші інші дані більше недоступні. Ви досі можете увійти, щоб надіслати запит на отримання резервної копії своїх даних до повного видалення впродовж приблизно 30 днів, але ми збережемо деякі основні дані, щоб унеможливити ухилення вами від призупинення.
|
||||||
|
@ -1625,7 +1639,9 @@ uk:
|
||||||
subject:
|
subject:
|
||||||
delete_statuses: Ваші дописи на %{acct} були вилучені
|
delete_statuses: Ваші дописи на %{acct} були вилучені
|
||||||
disable: Ваш обліковий запис %{acct} було заморожено
|
disable: Ваш обліковий запис %{acct} було заморожено
|
||||||
|
mark_statuses_as_sensitive: Ваші дописи на %{acct} позначені делікатними
|
||||||
none: Попередження для %{acct}
|
none: Попередження для %{acct}
|
||||||
|
sensitive: Ваші дописи на %{acct} відтепер будуть позначені делікатними
|
||||||
silence: Ваш обліковий запис %{acct} було обмежено
|
silence: Ваш обліковий запис %{acct} було обмежено
|
||||||
suspend: Ваш обліковий запис %{acct} було призупинено
|
suspend: Ваш обліковий запис %{acct} було призупинено
|
||||||
title:
|
title:
|
||||||
|
|
|
@ -473,10 +473,16 @@ Rails.application.routes.draw do
|
||||||
resources :bookmarks, only: [:index]
|
resources :bookmarks, only: [:index]
|
||||||
resources :reports, only: [:create]
|
resources :reports, only: [:create]
|
||||||
resources :trends, only: [:index], controller: 'trends/tags'
|
resources :trends, only: [:index], controller: 'trends/tags'
|
||||||
resources :filters, only: [:index, :create, :show, :update, :destroy]
|
resources :filters, only: [:index, :create, :show, :update, :destroy] do
|
||||||
|
resources :keywords, only: [:index, :create], controller: 'filters/keywords'
|
||||||
|
end
|
||||||
resources :endorsements, only: [:index]
|
resources :endorsements, only: [:index]
|
||||||
resources :markers, only: [:index, :create]
|
resources :markers, only: [:index, :create]
|
||||||
|
|
||||||
|
namespace :filters do
|
||||||
|
resources :keywords, only: [:show, :update, :destroy]
|
||||||
|
end
|
||||||
|
|
||||||
namespace :apps do
|
namespace :apps do
|
||||||
get :verify_credentials, to: 'credentials#show'
|
get :verify_credentials, to: 'credentials#show'
|
||||||
end
|
end
|
||||||
|
@ -594,6 +600,7 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :domain_allows, only: [:index, :show, :create, :destroy]
|
||||||
resources :domain_blocks, only: [:index, :show, :update, :create, :destroy]
|
resources :domain_blocks, only: [:index, :show, :update, :create, :destroy]
|
||||||
|
|
||||||
namespace :trends do
|
namespace :trends do
|
||||||
|
@ -612,6 +619,7 @@ Rails.application.routes.draw do
|
||||||
resources :media, only: [:create]
|
resources :media, only: [:create]
|
||||||
get '/search', to: 'search#index', as: :search
|
get '/search', to: 'search#index', as: :search
|
||||||
resources :suggestions, only: [:index]
|
resources :suggestions, only: [:index]
|
||||||
|
resources :filters, only: [:index, :create, :show, :update, :destroy]
|
||||||
|
|
||||||
namespace :admin do
|
namespace :admin do
|
||||||
resources :accounts, only: [:index]
|
resources :accounts, only: [:index]
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateCustomFilterKeywords < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :custom_filter_keywords do |t|
|
||||||
|
t.belongs_to :custom_filter, foreign_key: { on_delete: :cascade }, null: false
|
||||||
|
t.text :keyword, null: false, default: ''
|
||||||
|
t.boolean :whole_word, null: false, default: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MigrateCustomFilters < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
# Preserve IDs as much as possible to not confuse existing clients.
|
||||||
|
# As long as this migration is irreversible, we do not have to deal with conflicts.
|
||||||
|
safety_assured do
|
||||||
|
execute <<-SQL.squish
|
||||||
|
INSERT INTO custom_filter_keywords (id, custom_filter_id, keyword, whole_word, created_at, updated_at)
|
||||||
|
SELECT id, id, phrase, whole_word, created_at, updated_at
|
||||||
|
FROM custom_filters
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# Copy back changes from custom filters guaranteed to be from the old API
|
||||||
|
safety_assured do
|
||||||
|
execute <<-SQL.squish
|
||||||
|
UPDATE custom_filters
|
||||||
|
SET phrase = custom_filter_keywords.keyword, whole_word = custom_filter_keywords.whole_word
|
||||||
|
FROM custom_filter_keywords
|
||||||
|
WHERE custom_filters.id = custom_filter_keywords.id AND custom_filters.id = custom_filter_keywords.custom_filter_id
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
# Drop every keyword as we can't safely provide a 1:1 mapping
|
||||||
|
safety_assured do
|
||||||
|
execute <<-SQL.squish
|
||||||
|
TRUNCATE custom_filter_keywords RESTART IDENTITY
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
|
class AddActionToCustomFilters < ActiveRecord::Migration[6.1]
|
||||||
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
safety_assured do
|
||||||
|
add_column_with_default :custom_filters, :action, :integer, allow_null: false, default: 0
|
||||||
|
execute 'UPDATE custom_filters SET action = 1 WHERE irreversible IS TRUE'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
execute 'UPDATE custom_filters SET irreversible = (action = 1)'
|
||||||
|
remove_column :custom_filters, :action
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
|
class RemoveWholeWordFromCustomFilters < ActiveRecord::Migration[6.1]
|
||||||
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
safety_assured do
|
||||||
|
remove_column :custom_filters, :whole_word
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
safety_assured do
|
||||||
|
add_column_with_default :custom_filters, :whole_word, :boolean, default: true, allow_null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
|
class RemoveIrreversibleFromCustomFilters < ActiveRecord::Migration[6.1]
|
||||||
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
safety_assured do
|
||||||
|
remove_column :custom_filters, :irreversible
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
safety_assured do
|
||||||
|
add_column_with_default :custom_filters, :irreversible, :boolean, allow_null: false, default: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
db/schema.rb
15
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2022_06_06_044941) do
|
ActiveRecord::Schema.define(version: 2022_06_13_110903) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -339,15 +339,23 @@ ActiveRecord::Schema.define(version: 2022_06_06_044941) do
|
||||||
t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true
|
t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "custom_filter_keywords", force: :cascade do |t|
|
||||||
|
t.bigint "custom_filter_id", null: false
|
||||||
|
t.text "keyword", default: "", null: false
|
||||||
|
t.boolean "whole_word", default: true, null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["custom_filter_id"], name: "index_custom_filter_keywords_on_custom_filter_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "custom_filters", force: :cascade do |t|
|
create_table "custom_filters", force: :cascade do |t|
|
||||||
t.bigint "account_id"
|
t.bigint "account_id"
|
||||||
t.datetime "expires_at"
|
t.datetime "expires_at"
|
||||||
t.text "phrase", default: "", null: false
|
t.text "phrase", default: "", null: false
|
||||||
t.string "context", default: [], null: false, array: true
|
t.string "context", default: [], null: false, array: true
|
||||||
t.boolean "irreversible", default: false, null: false
|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.boolean "whole_word", default: true, null: false
|
t.integer "action", default: 0, null: false
|
||||||
t.index ["account_id"], name: "index_custom_filters_on_account_id"
|
t.index ["account_id"], name: "index_custom_filters_on_account_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1085,6 +1093,7 @@ ActiveRecord::Schema.define(version: 2022_06_06_044941) do
|
||||||
add_foreign_key "canonical_email_blocks", "accounts", column: "reference_account_id", on_delete: :cascade
|
add_foreign_key "canonical_email_blocks", "accounts", column: "reference_account_id", on_delete: :cascade
|
||||||
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
|
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
|
||||||
add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
|
add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
|
||||||
|
add_foreign_key "custom_filter_keywords", "custom_filters", on_delete: :cascade
|
||||||
add_foreign_key "custom_filters", "accounts", on_delete: :cascade
|
add_foreign_key "custom_filters", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "devices", "accounts", on_delete: :cascade
|
add_foreign_key "devices", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "devices", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade
|
add_foreign_key "devices", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade
|
||||||
|
|
|
@ -38,10 +38,26 @@ namespace :tests do
|
||||||
puts 'Instance actor does not have a private key'
|
puts 'Instance actor does not have a private key'
|
||||||
exit(1)
|
exit(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
unless Account.find_by(username: 'user', domain: nil).custom_filters.map { |filter| filter.keywords.pluck(:keyword) } == [['test'], ['take']]
|
||||||
|
puts 'CustomFilterKeyword records not created as expected'
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Populate the database with test data for 2.4.3'
|
||||||
|
task populate_v2_4_3: :environment do # rubocop:disable Naming/VariableNumber
|
||||||
|
ActiveRecord::Base.connection.execute(<<~SQL)
|
||||||
|
INSERT INTO "custom_filters"
|
||||||
|
(id, account_id, phrase, context, whole_word, irreversible, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, 2, 'test', '{ "home", "public" }', true, true, now(), now()),
|
||||||
|
(2, 2, 'take', '{ "home" }', false, false, now(), now());
|
||||||
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Populate the database with test data for 2.4.0'
|
desc 'Populate the database with test data for 2.4.0'
|
||||||
task populate_v2_4: :environment do
|
task populate_v2_4: :environment do # rubocop:disable Naming/VariableNumber
|
||||||
ActiveRecord::Base.connection.execute(<<~SQL)
|
ActiveRecord::Base.connection.execute(<<~SQL)
|
||||||
INSERT INTO "settings"
|
INSERT INTO "settings"
|
||||||
(id, thing_type, thing_id, var, value, created_at, updated_at)
|
(id, thing_type, thing_id, var, value, created_at, updated_at)
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
"blurhash": "^1.1.5",
|
"blurhash": "^1.1.5",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
"cocoon-js-vanilla": "^1.2.0",
|
||||||
"color-blend": "^3.0.1",
|
"color-blend": "^3.0.1",
|
||||||
"compression-webpack-plugin": "^6.1.1",
|
"compression-webpack-plugin": "^6.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
@ -73,6 +74,7 @@
|
||||||
"intl-relativeformat": "^6.4.3",
|
"intl-relativeformat": "^6.4.3",
|
||||||
"is-nan": "^1.3.2",
|
"is-nan": "^1.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
"jsdom": "^20.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mark-loader": "^0.1.6",
|
"mark-loader": "^0.1.6",
|
||||||
"marky": "^1.2.4",
|
"marky": "^1.2.4",
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Api::V1::Admin::DomainAllowsController, type: :controller do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:role) { 'admin' }
|
||||||
|
let(:user) { Fabricate(:user, role: role) }
|
||||||
|
let(:scopes) { 'admin:read admin:write' }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'forbidden for wrong scope' do |wrong_scope|
|
||||||
|
let(:scopes) { wrong_scope }
|
||||||
|
|
||||||
|
it 'returns http forbidden' do
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'forbidden for wrong role' do |wrong_role|
|
||||||
|
let(:role) { wrong_role }
|
||||||
|
|
||||||
|
it 'returns http forbidden' do
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
|
let!(:domain_allow) { Fabricate(:domain_allow) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get :index
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
|
||||||
|
it_behaves_like 'forbidden for wrong role', 'user'
|
||||||
|
it_behaves_like 'forbidden for wrong role', 'moderator'
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the expected domain allows' do
|
||||||
|
json = body_as_json
|
||||||
|
expect(json.length).to eq 1
|
||||||
|
expect(json[0][:id].to_i).to eq domain_allow.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #show' do
|
||||||
|
let!(:domain_allow) { Fabricate(:domain_allow) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get :show, params: { id: domain_allow.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
|
||||||
|
it_behaves_like 'forbidden for wrong role', 'user'
|
||||||
|
it_behaves_like 'forbidden for wrong role', 'moderator'
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns expected domain name' do
|
||||||
|
json = body_as_json
|
||||||
|
expect(json[:domain]).to eq domain_allow.domain
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE #destroy' do
|
||||||
|
let!(:domain_allow) { Fabricate(:domain_allow) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
delete :destroy, params: { id: domain_allow.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
|
||||||
|
it_behaves_like 'forbidden for wrong role', 'user'
|
||||||
|
it_behaves_like 'forbidden for wrong role', 'moderator'
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes the block' do
|
||||||
|
expect(DomainAllow.find_by(id: domain_allow.id)).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #create' do
|
||||||
|
let!(:domain_allow) { Fabricate(:domain_allow, domain: 'example.com') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
post :create, params: { domain: 'foo.bar.com' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
|
||||||
|
it_behaves_like 'forbidden for wrong role', 'user'
|
||||||
|
it_behaves_like 'forbidden for wrong role', 'moderator'
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns expected domain name' do
|
||||||
|
json = body_as_json
|
||||||
|
expect(json[:domain]).to eq 'foo.bar.com'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a domain block' do
|
||||||
|
expect(DomainAllow.find_by(domain: 'foo.bar.com')).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,142 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Api::V1::Filters::KeywordsController, type: :controller do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
let(:filter) { Fabricate(:custom_filter, account: user.account) }
|
||||||
|
let(:other_user) { Fabricate(:user) }
|
||||||
|
let(:other_filter) { Fabricate(:custom_filter, account: other_user.account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
|
let(:scopes) { 'read:filters' }
|
||||||
|
let!(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) }
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
get :index, params: { filter_id: filter.id }
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when trying to access another's user filters" do
|
||||||
|
it 'returns http not found' do
|
||||||
|
get :index, params: { filter_id: other_filter.id }
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #create' do
|
||||||
|
let(:scopes) { 'write:filters' }
|
||||||
|
let(:filter_id) { filter.id }
|
||||||
|
|
||||||
|
before do
|
||||||
|
post :create, params: { filter_id: filter_id, keyword: 'magic', whole_word: false }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a keyword' do
|
||||||
|
json = body_as_json
|
||||||
|
expect(json[:keyword]).to eq 'magic'
|
||||||
|
expect(json[:whole_word]).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a keyword' do
|
||||||
|
filter = user.account.custom_filters.first
|
||||||
|
expect(filter).to_not be_nil
|
||||||
|
expect(filter.keywords.pluck(:keyword)).to eq ['magic']
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when trying to add to another another's user filters" do
|
||||||
|
let(:filter_id) { other_filter.id }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #show' do
|
||||||
|
let(:scopes) { 'read:filters' }
|
||||||
|
let(:keyword) { Fabricate(:custom_filter_keyword, keyword: 'foo', whole_word: false, custom_filter: filter) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get :show, params: { id: keyword.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns expected data' do
|
||||||
|
json = body_as_json
|
||||||
|
expect(json[:keyword]).to eq 'foo'
|
||||||
|
expect(json[:whole_word]).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when trying to access another user's filter keyword" do
|
||||||
|
let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: other_filter) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT #update' do
|
||||||
|
let(:scopes) { 'write:filters' }
|
||||||
|
let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get :update, params: { id: keyword.id, keyword: 'updated' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the keyword' do
|
||||||
|
expect(keyword.reload.keyword).to eq 'updated'
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when trying to update another user's filter keyword" do
|
||||||
|
let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: other_filter) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE #destroy' do
|
||||||
|
let(:scopes) { 'write:filters' }
|
||||||
|
let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
delete :destroy, params: { id: keyword.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the filter' do
|
||||||
|
expect { keyword.reload }.to raise_error ActiveRecord::RecordNotFound
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when trying to update another user's filter keyword" do
|
||||||
|
let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: other_filter) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -34,7 +34,7 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
|
||||||
it 'creates a filter' do
|
it 'creates a filter' do
|
||||||
filter = user.account.custom_filters.first
|
filter = user.account.custom_filters.first
|
||||||
expect(filter).to_not be_nil
|
expect(filter).to_not be_nil
|
||||||
expect(filter.phrase).to eq 'magic'
|
expect(filter.keywords.pluck(:keyword)).to eq ['magic']
|
||||||
expect(filter.context).to eq %w(home)
|
expect(filter.context).to eq %w(home)
|
||||||
expect(filter.irreversible?).to be true
|
expect(filter.irreversible?).to be true
|
||||||
expect(filter.expires_at).to be_nil
|
expect(filter.expires_at).to be_nil
|
||||||
|
@ -42,21 +42,23 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #show' do
|
describe 'GET #show' do
|
||||||
let(:scopes) { 'read:filters' }
|
let(:scopes) { 'read:filters' }
|
||||||
let(:filter) { Fabricate(:custom_filter, account: user.account) }
|
let(:filter) { Fabricate(:custom_filter, account: user.account) }
|
||||||
|
let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
get :show, params: { id: filter.id }
|
get :show, params: { id: keyword.id }
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT #update' do
|
describe 'PUT #update' do
|
||||||
let(:scopes) { 'write:filters' }
|
let(:scopes) { 'write:filters' }
|
||||||
let(:filter) { Fabricate(:custom_filter, account: user.account) }
|
let(:filter) { Fabricate(:custom_filter, account: user.account) }
|
||||||
|
let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
put :update, params: { id: filter.id, phrase: 'updated' }
|
put :update, params: { id: keyword.id, phrase: 'updated' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
|
@ -64,16 +66,17 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates the filter' do
|
it 'updates the filter' do
|
||||||
expect(filter.reload.phrase).to eq 'updated'
|
expect(keyword.reload.phrase).to eq 'updated'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'DELETE #destroy' do
|
describe 'DELETE #destroy' do
|
||||||
let(:scopes) { 'write:filters' }
|
let(:scopes) { 'write:filters' }
|
||||||
let(:filter) { Fabricate(:custom_filter, account: user.account) }
|
let(:filter) { Fabricate(:custom_filter, account: user.account) }
|
||||||
|
let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
delete :destroy, params: { id: filter.id }
|
delete :destroy, params: { id: keyword.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
|
@ -81,7 +84,7 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes the filter' do
|
it 'removes the filter' do
|
||||||
expect { filter.reload }.to raise_error ActiveRecord::RecordNotFound
|
expect { keyword.reload }.to raise_error ActiveRecord::RecordNotFound
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,6 +20,58 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
|
||||||
get :show, params: { id: status.id }
|
get :show, params: { id: status.id }
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when post includes filtered terms' do
|
||||||
|
let(:status) { Fabricate(:status, text: 'this toot is about that banned word') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
get :show, params: { id: status.id }
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns filter information' do
|
||||||
|
get :show, params: { id: status.id }
|
||||||
|
json = body_as_json
|
||||||
|
expect(json[:filtered][0]).to include({
|
||||||
|
filter: a_hash_including({
|
||||||
|
id: user.account.custom_filters.first.id.to_s,
|
||||||
|
title: 'filter1',
|
||||||
|
filter_action: 'hide',
|
||||||
|
}),
|
||||||
|
keyword_matches: ['banned'],
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when reblog includes filtered terms' do
|
||||||
|
let(:status) { Fabricate(:status, reblog: Fabricate(:status, text: 'this toot is about that banned word')) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
get :show, params: { id: status.id }
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns filter information' do
|
||||||
|
get :show, params: { id: status.id }
|
||||||
|
json = body_as_json
|
||||||
|
expect(json[:reblog][:filtered][0]).to include({
|
||||||
|
filter: a_hash_including({
|
||||||
|
id: user.account.custom_filters.first.id.to_s,
|
||||||
|
title: 'filter1',
|
||||||
|
filter_action: 'hide',
|
||||||
|
}),
|
||||||
|
keyword_matches: ['banned'],
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #context' do
|
describe 'GET #context' do
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Api::V2::FiltersController, type: :controller do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
|
let(:scopes) { 'read:filters' }
|
||||||
|
let!(:filter) { Fabricate(:custom_filter, account: user.account) }
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
get :index
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #create' do
|
||||||
|
let(:scopes) { 'write:filters' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
post :create, params: { title: 'magic', context: %w(home), filter_action: 'hide', keywords_attributes: [keyword: 'magic'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a filter with keywords' do
|
||||||
|
json = body_as_json
|
||||||
|
expect(json[:title]).to eq 'magic'
|
||||||
|
expect(json[:filter_action]).to eq 'hide'
|
||||||
|
expect(json[:context]).to eq ['home']
|
||||||
|
expect(json[:keywords].map { |keyword| keyword.slice(:keyword, :whole_word) }).to eq [{ keyword: 'magic', whole_word: true }]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a filter' do
|
||||||
|
filter = user.account.custom_filters.first
|
||||||
|
expect(filter).to_not be_nil
|
||||||
|
expect(filter.keywords.pluck(:keyword)).to eq ['magic']
|
||||||
|
expect(filter.context).to eq %w(home)
|
||||||
|
expect(filter.irreversible?).to be true
|
||||||
|
expect(filter.expires_at).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #show' do
|
||||||
|
let(:scopes) { 'read:filters' }
|
||||||
|
let(:filter) { Fabricate(:custom_filter, account: user.account) }
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
get :show, params: { id: filter.id }
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT #update' do
|
||||||
|
let(:scopes) { 'write:filters' }
|
||||||
|
let!(:filter) { Fabricate(:custom_filter, account: user.account) }
|
||||||
|
let!(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) }
|
||||||
|
|
||||||
|
context 'updating filter parameters' do
|
||||||
|
before do
|
||||||
|
put :update, params: { id: filter.id, title: 'updated', context: %w(home public) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the filter title' do
|
||||||
|
expect(filter.reload.title).to eq 'updated'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the filter context' do
|
||||||
|
expect(filter.reload.context).to eq %w(home public)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'updating keywords in bulk' do
|
||||||
|
before do
|
||||||
|
allow(redis).to receive_messages(publish: nil)
|
||||||
|
put :update, params: { id: filter.id, keywords_attributes: [{ id: keyword.id, keyword: 'updated' }] }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the keyword' do
|
||||||
|
expect(keyword.reload.keyword).to eq 'updated'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends exactly one filters_changed event' do
|
||||||
|
expect(redis).to have_received(:publish).with("timeline:#{user.account.id}", Oj.dump(event: :filters_changed)).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE #destroy' do
|
||||||
|
let(:scopes) { 'write:filters' }
|
||||||
|
let(:filter) { Fabricate(:custom_filter, account: user.account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
delete :destroy, params: { id: filter.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the filter' do
|
||||||
|
expect { filter.reload }.to raise_error ActiveRecord::RecordNotFound
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue