Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
91a78e0652
144 changed files with 1634 additions and 815 deletions
|
@ -24,4 +24,4 @@ RAILS_ENV=development ./bin/rails db:setup
|
||||||
RAILS_ENV=development ./bin/rails assets:precompile
|
RAILS_ENV=development ./bin/rails assets:precompile
|
||||||
|
|
||||||
# Precompile assets for test
|
# Precompile assets for test
|
||||||
RAILS_ENV=test NODE_ENV=tests ./bin/rails assets:precompile
|
RAILS_ENV=test ./bin/rails assets:precompile
|
||||||
|
|
22
.eslintrc.js
22
.eslintrc.js
|
@ -1,4 +1,7 @@
|
||||||
module.exports = {
|
// @ts-check
|
||||||
|
const { defineConfig } = require('eslint-define-config');
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
root: true,
|
root: true,
|
||||||
|
|
||||||
extends: [
|
extends: [
|
||||||
|
@ -193,6 +196,7 @@ module.exports = {
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
devDependencies: [
|
devDependencies: [
|
||||||
|
'.eslintrc.js',
|
||||||
'config/webpack/**',
|
'config/webpack/**',
|
||||||
'app/javascript/mastodon/performance.js',
|
'app/javascript/mastodon/performance.js',
|
||||||
'app/javascript/mastodon/test_setup.js',
|
'app/javascript/mastodon/test_setup.js',
|
||||||
|
@ -280,7 +284,6 @@ module.exports = {
|
||||||
'formatjs/no-id': 'off', // IDs are used for translation keys
|
'formatjs/no-id': 'off', // IDs are used for translation keys
|
||||||
'formatjs/no-invalid-icu': 'error',
|
'formatjs/no-invalid-icu': 'error',
|
||||||
'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings
|
'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings
|
||||||
'formatjs/no-multiple-plurals': 'off', // Only used by hashtag.jsx
|
|
||||||
'formatjs/no-multiple-whitespaces': 'error',
|
'formatjs/no-multiple-whitespaces': 'error',
|
||||||
'formatjs/no-offset': 'error',
|
'formatjs/no-offset': 'error',
|
||||||
'formatjs/no-useless-message': 'error',
|
'formatjs/no-useless-message': 'error',
|
||||||
|
@ -299,6 +302,7 @@ module.exports = {
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: [
|
files: [
|
||||||
|
'.eslintrc.js',
|
||||||
'*.config.js',
|
'*.config.js',
|
||||||
'.*rc.js',
|
'.*rc.js',
|
||||||
'ide-helper.js',
|
'ide-helper.js',
|
||||||
|
@ -349,7 +353,7 @@ module.exports = {
|
||||||
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
|
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
|
||||||
'@typescript-eslint/consistent-type-exports': 'error',
|
'@typescript-eslint/consistent-type-exports': 'error',
|
||||||
'@typescript-eslint/consistent-type-imports': 'error',
|
'@typescript-eslint/consistent-type-imports': 'error',
|
||||||
"@typescript-eslint/prefer-nullish-coalescing": ['error', {ignorePrimitives: {boolean: true}}],
|
"@typescript-eslint/prefer-nullish-coalescing": ['error', { ignorePrimitives: { boolean: true } }],
|
||||||
|
|
||||||
'jsdoc/require-jsdoc': 'off',
|
'jsdoc/require-jsdoc': 'off',
|
||||||
|
|
||||||
|
@ -372,14 +376,6 @@ module.exports = {
|
||||||
env: {
|
env: {
|
||||||
jest: true,
|
jest: true,
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'streaming/**/*',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'import/no-commonjs': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
});
|
||||||
|
|
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
|
@ -22,6 +22,7 @@
|
||||||
'react-hotkeys', // Requires code changes
|
'react-hotkeys', // Requires code changes
|
||||||
|
|
||||||
// Requires Webpacker upgrade or replacement
|
// Requires Webpacker upgrade or replacement
|
||||||
|
'@svgr/webpack',
|
||||||
'@types/webpack',
|
'@types/webpack',
|
||||||
'babel-loader',
|
'babel-loader',
|
||||||
'compression-webpack-plugin',
|
'compression-webpack-plugin',
|
||||||
|
|
3
.github/workflows/build-container-image.yml
vendored
3
.github/workflows/build-container-image.yml
vendored
|
@ -21,6 +21,8 @@ on:
|
||||||
type: string
|
type: string
|
||||||
labels:
|
labels:
|
||||||
type: string
|
type: string
|
||||||
|
file_to_build:
|
||||||
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-image:
|
build-image:
|
||||||
|
@ -86,6 +88,7 @@ jobs:
|
||||||
- uses: docker/build-push-action@v5
|
- uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
file: ${{ inputs.file_to_build }}
|
||||||
build-args: |
|
build-args: |
|
||||||
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
|
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
|
||||||
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
|
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
|
||||||
|
|
23
.github/workflows/build-nightly.yml
vendored
23
.github/workflows/build-nightly.yml
vendored
|
@ -25,6 +25,7 @@ jobs:
|
||||||
needs: compute-suffix
|
needs: compute-suffix
|
||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
|
file_to_build: Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
use_native_arm64_builder: true
|
use_native_arm64_builder: true
|
||||||
cache: false
|
cache: false
|
||||||
|
@ -41,3 +42,25 @@ jobs:
|
||||||
type=raw,value=nightly
|
type=raw,value=nightly
|
||||||
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
|
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
build-image-streaming:
|
||||||
|
needs: compute-suffix
|
||||||
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
|
with:
|
||||||
|
file_to_build: streaming/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
use_native_arm64_builder: true
|
||||||
|
cache: false
|
||||||
|
push_to_images: |
|
||||||
|
tootsuite/mastodon-streaming
|
||||||
|
ghcr.io/mastodon/mastodon-streaming
|
||||||
|
version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.description=Nightly build image used for testing purposes
|
||||||
|
flavor: |
|
||||||
|
latest=auto
|
||||||
|
tags: |
|
||||||
|
type=raw,value=edge
|
||||||
|
type=raw,value=nightly
|
||||||
|
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
|
||||||
|
secrets: inherit
|
||||||
|
|
17
.github/workflows/build-push-pr.yml
vendored
17
.github/workflows/build-push-pr.yml
vendored
|
@ -29,6 +29,7 @@ jobs:
|
||||||
needs: compute-suffix
|
needs: compute-suffix
|
||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
|
file_to_build: Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
use_native_arm64_builder: true
|
use_native_arm64_builder: true
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
|
@ -39,3 +40,19 @@ jobs:
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
build-image-streaming:
|
||||||
|
needs: compute-suffix
|
||||||
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
|
with:
|
||||||
|
file_to_build: streaming/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
use_native_arm64_builder: true
|
||||||
|
push_to_images: |
|
||||||
|
ghcr.io/mastodon/mastodon-streaming
|
||||||
|
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
|
||||||
|
flavor: |
|
||||||
|
latest=auto
|
||||||
|
tags: |
|
||||||
|
type=ref,event=pr
|
||||||
|
secrets: inherit
|
||||||
|
|
22
.github/workflows/build-releases.yml
vendored
22
.github/workflows/build-releases.yml
vendored
|
@ -12,6 +12,7 @@ jobs:
|
||||||
build-image:
|
build-image:
|
||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
|
file_to_build: Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
use_native_arm64_builder: true
|
use_native_arm64_builder: true
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
|
@ -27,3 +28,24 @@ jobs:
|
||||||
type=pep440,pattern={{raw}}
|
type=pep440,pattern={{raw}}
|
||||||
type=pep440,pattern=v{{major}}.{{minor}}
|
type=pep440,pattern=v{{major}}.{{minor}}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
build-image-streaming:
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v4.3.')
|
||||||
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
|
with:
|
||||||
|
file_to_build: streaming/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
use_native_arm64_builder: true
|
||||||
|
push_to_images: |
|
||||||
|
tootsuite/mastodon-streaming
|
||||||
|
ghcr.io/mastodon/mastodon-streaming
|
||||||
|
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
|
||||||
|
cache: false
|
||||||
|
# Only tag with latest when ran against the latest stable branch
|
||||||
|
# This needs to be updated after each minor version release
|
||||||
|
flavor: |
|
||||||
|
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
|
||||||
|
tags: |
|
||||||
|
type=pep440,pattern={{raw}}
|
||||||
|
type=pep440,pattern=v{{major}}.{{minor}}
|
||||||
|
secrets: inherit
|
||||||
|
|
14
.github/workflows/test-image-build.yml
vendored
14
.github/workflows/test-image-build.yml
vendored
|
@ -7,6 +7,7 @@ on:
|
||||||
- .github/workflows/build-releases.yml
|
- .github/workflows/build-releases.yml
|
||||||
- .github/workflows/test-image-build.yml
|
- .github/workflows/test-image-build.yml
|
||||||
- Dockerfile
|
- Dockerfile
|
||||||
|
- streaming/Dockerfile
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
@ -18,4 +19,17 @@ jobs:
|
||||||
|
|
||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
|
file_to_build: Dockerfile
|
||||||
platforms: linux/amd64 # Testing only on native platform so it is performant
|
platforms: linux/amd64 # Testing only on native platform so it is performant
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
build-image-streaming:
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-streaming
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
|
with:
|
||||||
|
file_to_build: streaming/Dockerfile
|
||||||
|
platforms: linux/amd64 # Testing only on native platform so it is performant
|
||||||
|
cache: true
|
||||||
|
|
|
@ -307,7 +307,7 @@ Style/FetchEnvVar:
|
||||||
- 'config/initializers/devise.rb'
|
- 'config/initializers/devise.rb'
|
||||||
- 'config/initializers/paperclip.rb'
|
- 'config/initializers/paperclip.rb'
|
||||||
- 'config/initializers/vapid.rb'
|
- 'config/initializers/vapid.rb'
|
||||||
- 'lib/mastodon/premailer_webpack_strategy.rb'
|
- 'lib/premailer_webpack_strategy.rb'
|
||||||
- 'lib/mastodon/redis_config.rb'
|
- 'lib/mastodon/redis_config.rb'
|
||||||
- 'lib/tasks/repo.rake'
|
- 'lib/tasks/repo.rake'
|
||||||
- 'spec/features/profile_spec.rb'
|
- 'spec/features/profile_spec.rb'
|
||||||
|
@ -357,8 +357,8 @@ Style/GuardClause:
|
||||||
- 'config/initializers/devise.rb'
|
- 'config/initializers/devise.rb'
|
||||||
- 'db/migrate/20170901141119_truncate_preview_cards.rb'
|
- 'db/migrate/20170901141119_truncate_preview_cards.rb'
|
||||||
- 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb'
|
- 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb'
|
||||||
- 'lib/devise/two_factor_ldap_authenticatable.rb'
|
- 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
|
||||||
- 'lib/devise/two_factor_pam_authenticatable.rb'
|
- 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
|
||||||
- 'lib/mastodon/cli/accounts.rb'
|
- 'lib/mastodon/cli/accounts.rb'
|
||||||
- 'lib/mastodon/cli/maintenance.rb'
|
- 'lib/mastodon/cli/maintenance.rb'
|
||||||
- 'lib/mastodon/cli/media.rb'
|
- 'lib/mastodon/cli/media.rb'
|
||||||
|
@ -493,8 +493,8 @@ Style/SafeNavigation:
|
||||||
# SupportedStyles: only_raise, only_fail, semantic
|
# SupportedStyles: only_raise, only_fail, semantic
|
||||||
Style/SignalException:
|
Style/SignalException:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/devise/two_factor_ldap_authenticatable.rb'
|
- 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
|
||||||
- 'lib/devise/two_factor_pam_authenticatable.rb'
|
- 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
|
||||||
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
Style/SingleArgumentDig:
|
Style/SingleArgumentDig:
|
||||||
|
|
319
Dockerfile
319
Dockerfile
|
@ -1,112 +1,257 @@
|
||||||
# syntax=docker/dockerfile:1.4
|
# syntax=docker/dockerfile:1.4
|
||||||
# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim
|
|
||||||
ARG NODE_VERSION="20.9-bookworm-slim"
|
|
||||||
|
|
||||||
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby
|
# Please see https://docs.docker.com/engine/reference/builder for information about
|
||||||
FROM node:${NODE_VERSION} as build
|
# the extended buildx capabilities used in this file.
|
||||||
|
# Make sure multiarch TARGETPLATFORM is available for interpolation
|
||||||
|
# See: https://docs.docker.com/build/building/multi-platform/
|
||||||
|
ARG TARGETPLATFORM=${TARGETPLATFORM}
|
||||||
|
ARG BUILDPLATFORM=${BUILDPLATFORM}
|
||||||
|
|
||||||
COPY --link --from=ruby /opt/ruby /opt/ruby
|
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.2"]
|
||||||
|
ARG RUBY_VERSION="3.2.2"
|
||||||
|
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||||
|
ARG NODE_MAJOR_VERSION="20"
|
||||||
|
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
||||||
|
ARG DEBIAN_VERSION="bookworm"
|
||||||
|
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
||||||
|
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
|
||||||
|
# Ruby image to use for base image based on combined variables (ex: 3.2.2-slim-bookworm)
|
||||||
|
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND="noninteractive" \
|
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
||||||
PATH="${PATH}:/opt/ruby/bin"
|
# Example: v4.2.0-nightly.2023.11.09+something
|
||||||
|
# Overwrite existance of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
|
||||||
|
ARG MASTODON_VERSION_PRERELEASE="bark"
|
||||||
|
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"]
|
||||||
|
ARG MASTODON_VERSION_METADATA="dev"
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
# Allow Ruby on Rails to serve static files
|
||||||
|
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
|
||||||
|
ARG RAILS_SERVE_STATIC_FILES="true"
|
||||||
|
# Allow to use YJIT compiler
|
||||||
|
# See: https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md
|
||||||
|
ARG RUBY_YJIT_ENABLE="1"
|
||||||
|
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
|
||||||
|
ARG TZ="Etc/UTC"
|
||||||
|
# Linux UID (user id) for the mastodon user, change with [--build-arg UID=1234]
|
||||||
|
ARG UID="991"
|
||||||
|
# Linux GID (group id) for the mastodon user, change with [--build-arg GID=1234]
|
||||||
|
ARG GID="991"
|
||||||
|
|
||||||
|
# Apply Mastodon build options based on options above
|
||||||
|
ENV \
|
||||||
|
# Apply Mastodon version information
|
||||||
|
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
|
||||||
|
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \
|
||||||
|
# Apply Mastodon static files and YJIT options
|
||||||
|
RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \
|
||||||
|
RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \
|
||||||
|
# Apply timezone
|
||||||
|
TZ=${TZ}
|
||||||
|
|
||||||
|
ENV \
|
||||||
|
# Configure the IP to bind Mastodon to when serving traffic
|
||||||
|
BIND="0.0.0.0" \
|
||||||
|
# Use production settings for Yarn, Node and related nodejs based tools
|
||||||
|
NODE_ENV="production" \
|
||||||
|
# Use production settings for Ruby on Rails
|
||||||
|
RAILS_ENV="production" \
|
||||||
|
# Add Ruby and Mastodon installation to the PATH
|
||||||
|
DEBIAN_FRONTEND="noninteractive" \
|
||||||
|
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \
|
||||||
|
# Optimize jemalloc 5.x performance
|
||||||
|
MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0"
|
||||||
|
|
||||||
|
# Set default shell used for running commands
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"]
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
RUN echo "Target platform is $TARGETPLATFORM"
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
# Remove automatic apt cache Docker cleanup scripts
|
||||||
|
rm -f /etc/apt/apt.conf.d/docker-clean; \
|
||||||
|
# Sets timezone
|
||||||
|
echo "${TZ}" > /etc/localtime; \
|
||||||
|
# Creates mastodon user/group and sets home directory
|
||||||
|
groupadd -g "${GID}" mastodon; \
|
||||||
|
useradd -l -u "${UID}" -g "${GID}" -m -d /opt/mastodon mastodon; \
|
||||||
|
# Creates /mastodon symlink to /opt/mastodon
|
||||||
|
ln -s /opt/mastodon /mastodon;
|
||||||
|
|
||||||
|
# Set /opt/mastodon as working directory
|
||||||
WORKDIR /opt/mastodon
|
WORKDIR /opt/mastodon
|
||||||
|
|
||||||
# hadolint ignore=DL3008
|
# hadolint ignore=DL3008,DL3005
|
||||||
RUN apt-get update && \
|
RUN \
|
||||||
apt-get -yq dist-upgrade && \
|
# Mount Apt cache and lib directories from Docker buildx caches
|
||||||
apt-get install -y --no-install-recommends build-essential \
|
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||||
git \
|
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||||
libicu-dev \
|
# Apt update & upgrade to check for security updates to Debian image
|
||||||
libidn-dev \
|
apt-get update; \
|
||||||
libpq-dev \
|
apt-get dist-upgrade -yq; \
|
||||||
libjemalloc-dev \
|
# Install jemalloc, curl and other necessary components
|
||||||
zlib1g-dev \
|
apt-get install -y --no-install-recommends \
|
||||||
libgdbm-dev \
|
ca-certificates \
|
||||||
libgmp-dev \
|
curl \
|
||||||
libssl-dev \
|
ffmpeg \
|
||||||
libyaml-dev \
|
file \
|
||||||
ca-certificates \
|
imagemagick \
|
||||||
libreadline8 \
|
libjemalloc2 \
|
||||||
python3 \
|
patchelf \
|
||||||
shared-mime-info && \
|
procps \
|
||||||
bundle config set --local deployment 'true' && \
|
tini \
|
||||||
bundle config set --local without 'development test' && \
|
tzdata \
|
||||||
bundle config set silence_root_warning true && \
|
; \
|
||||||
corepack enable
|
# Patch Ruby to use jemalloc
|
||||||
|
patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \
|
||||||
|
# Discard patchelf after use
|
||||||
|
apt-get purge -y \
|
||||||
|
patchelf \
|
||||||
|
;
|
||||||
|
|
||||||
COPY Gemfile* package.json yarn.lock .yarnrc.yml /opt/mastodon/
|
# Create temporary build layer from base image
|
||||||
|
FROM ruby as build
|
||||||
|
|
||||||
|
# Copy Node package configuration files into working directory
|
||||||
|
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
|
||||||
|
COPY .yarn /opt/mastodon/.yarn
|
||||||
|
|
||||||
|
COPY --from=node /usr/local/bin /usr/local/bin
|
||||||
|
COPY --from=node /usr/local/lib /usr/local/lib
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# hadolint ignore=DL3008
|
||||||
|
RUN \
|
||||||
|
# Mount Apt cache and lib directories from Docker buildx caches
|
||||||
|
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||||
|
# Install build tools and bundler dependencies from APT
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
g++ \
|
||||||
|
gcc \
|
||||||
|
git \
|
||||||
|
libgdbm-dev \
|
||||||
|
libgmp-dev \
|
||||||
|
libicu-dev \
|
||||||
|
libidn-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libssl-dev \
|
||||||
|
make \
|
||||||
|
shared-mime-info \
|
||||||
|
zlib1g-dev \
|
||||||
|
;
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
# Configure Corepack
|
||||||
|
rm /usr/local/bin/yarn*; \
|
||||||
|
corepack enable; \
|
||||||
|
corepack prepare --activate;
|
||||||
|
|
||||||
|
# Create temporary bundler specific build layer from build layer
|
||||||
|
FROM build as bundler
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# Copy Gemfile config into working directory
|
||||||
|
COPY Gemfile* /opt/mastodon/
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
# Mount Ruby Gem caches
|
||||||
|
--mount=type=cache,id=gem-cache-${TARGETPLATFORM},target=/usr/local/bundle/cache/,sharing=locked \
|
||||||
|
# Configure bundle to prevent changes to Gemfile and Gemfile.lock
|
||||||
|
bundle config set --global frozen "true"; \
|
||||||
|
# Configure bundle to not cache downloaded Gems
|
||||||
|
bundle config set --global cache_all "false"; \
|
||||||
|
# Configure bundle to only process production Gems
|
||||||
|
bundle config set --local without "development test"; \
|
||||||
|
# Configure bundle to not warn about root user
|
||||||
|
bundle config set silence_root_warning "true"; \
|
||||||
|
# Download and install required Gems
|
||||||
|
bundle install -j"$(nproc)";
|
||||||
|
|
||||||
|
# Create temporary node specific build layer from build layer
|
||||||
|
FROM build as yarn
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# Copy Node package configuration files into working directory
|
||||||
|
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
|
||||||
COPY streaming/package.json /opt/mastodon/streaming/
|
COPY streaming/package.json /opt/mastodon/streaming/
|
||||||
COPY .yarn /opt/mastodon/.yarn
|
COPY .yarn /opt/mastodon/.yarn
|
||||||
|
|
||||||
RUN bundle install -j"$(nproc)"
|
# hadolint ignore=DL3008
|
||||||
|
RUN \
|
||||||
|
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
|
||||||
|
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
|
||||||
|
# Install Node packages
|
||||||
|
yarn workspaces focus --production @mastodon/mastodon;
|
||||||
|
|
||||||
RUN yarn workspaces focus --all --production && \
|
# Create temporary assets build layer from build layer
|
||||||
yarn cache clean
|
FROM build as precompiler
|
||||||
|
|
||||||
FROM node:${NODE_VERSION}
|
# Copy Mastodon sources into precompiler layer
|
||||||
|
COPY . /opt/mastodon/
|
||||||
|
|
||||||
# Use those args to specify your own version flags & suffixes
|
# Copy bundler and node packages from build layer to container
|
||||||
ARG MASTODON_VERSION_PRERELEASE="bark"
|
COPY --from=yarn /opt/mastodon /opt/mastodon/
|
||||||
ARG MASTODON_VERSION_METADATA="dev"
|
COPY --from=bundler /opt/mastodon /opt/mastodon/
|
||||||
|
COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
|
||||||
|
|
||||||
ARG UID="991"
|
ARG TARGETPLATFORM
|
||||||
ARG GID="991"
|
|
||||||
|
|
||||||
COPY --link --from=ruby /opt/ruby /opt/ruby
|
RUN \
|
||||||
|
# Use Ruby on Rails to create Mastodon assets
|
||||||
|
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile; \
|
||||||
|
# Cleanup temporary files
|
||||||
|
rm -fr /opt/mastodon/tmp;
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
# Prep final Mastodon Ruby layer
|
||||||
|
FROM ruby as mastodon
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND="noninteractive" \
|
ARG TARGETPLATFORM
|
||||||
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin"
|
|
||||||
|
|
||||||
# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use
|
# hadolint ignore=DL3008
|
||||||
# hadolint ignore=DL3008,DL3009
|
RUN \
|
||||||
RUN apt-get update && \
|
# Mount Apt cache and lib directories from Docker buildx caches
|
||||||
echo "Etc/UTC" > /etc/localtime && \
|
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||||
groupadd -g "${GID}" mastodon && \
|
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||||
useradd -l -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \
|
# Mount Corepack and Yarn caches from Docker buildx caches
|
||||||
apt-get -y --no-install-recommends install whois \
|
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
|
||||||
wget \
|
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
|
||||||
procps \
|
# Apt update install non-dev versions of necessary components
|
||||||
libssl3 \
|
apt-get install -y --no-install-recommends \
|
||||||
libpq5 \
|
libssl3 \
|
||||||
imagemagick \
|
libpq5 \
|
||||||
ffmpeg \
|
libicu72 \
|
||||||
libjemalloc2 \
|
libidn12 \
|
||||||
libicu72 \
|
libreadline8 \
|
||||||
libidn12 \
|
libyaml-0-2 \
|
||||||
libyaml-0-2 \
|
;
|
||||||
file \
|
|
||||||
ca-certificates \
|
|
||||||
tzdata \
|
|
||||||
libreadline8 \
|
|
||||||
tini && \
|
|
||||||
ln -s /opt/mastodon /mastodon && \
|
|
||||||
corepack enable
|
|
||||||
|
|
||||||
# Note: no, cleaning here since Debian does this automatically
|
# Copy Mastodon sources into final layer
|
||||||
# See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem
|
COPY . /opt/mastodon/
|
||||||
|
|
||||||
COPY --chown=mastodon:mastodon . /opt/mastodon
|
# Copy compiled assets to layer
|
||||||
COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon
|
COPY --from=precompiler /opt/mastodon/public/packs /opt/mastodon/public/packs
|
||||||
|
COPY --from=precompiler /opt/mastodon/public/assets /opt/mastodon/public/assets
|
||||||
|
# Copy bundler components to layer
|
||||||
|
COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
|
||||||
|
|
||||||
ENV RAILS_ENV="production" \
|
RUN \
|
||||||
NODE_ENV="production" \
|
# Precompile bootsnap code for faster Rails startup
|
||||||
RAILS_SERVE_STATIC_FILES="true" \
|
bundle exec bootsnap precompile --gemfile app/ lib/;
|
||||||
BIND="0.0.0.0" \
|
|
||||||
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
|
|
||||||
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}"
|
|
||||||
|
|
||||||
# Set the run user
|
RUN \
|
||||||
|
# Pre-create and chown system volume to Mastodon user
|
||||||
|
mkdir -p /opt/mastodon/public/system; \
|
||||||
|
chown mastodon:mastodon /opt/mastodon/public/system;
|
||||||
|
|
||||||
|
# Set the running user for resulting container
|
||||||
USER mastodon
|
USER mastodon
|
||||||
WORKDIR /opt/mastodon
|
# Expose default Puma ports
|
||||||
|
EXPOSE 3000
|
||||||
# Precompile assets
|
# Set container tini as default entry point
|
||||||
RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
|
|
||||||
# Set the work dir and the container entry point
|
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
|
||||||
EXPOSE 3000 4000
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -131,7 +131,7 @@ GEM
|
||||||
attr_required (1.0.1)
|
attr_required (1.0.1)
|
||||||
awrence (1.2.1)
|
awrence (1.2.1)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.855.0)
|
aws-partitions (1.857.0)
|
||||||
aws-sdk-core (3.188.0)
|
aws-sdk-core (3.188.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
|
@ -140,7 +140,7 @@ GEM
|
||||||
aws-sdk-kms (1.73.0)
|
aws-sdk-kms (1.73.0)
|
||||||
aws-sdk-core (~> 3, >= 3.188.0)
|
aws-sdk-core (~> 3, >= 3.188.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.139.0)
|
aws-sdk-s3 (1.140.0)
|
||||||
aws-sdk-core (~> 3, >= 3.188.0)
|
aws-sdk-core (~> 3, >= 3.188.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.6)
|
aws-sigv4 (~> 1.6)
|
||||||
|
@ -156,7 +156,7 @@ GEM
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
nokogiri (~> 1, >= 1.10.8)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcp47_spec (0.2.1)
|
bcp47_spec (0.2.1)
|
||||||
bcrypt (3.1.19)
|
bcrypt (3.1.20)
|
||||||
better_errors (2.10.1)
|
better_errors (2.10.1)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
|
@ -272,7 +272,7 @@ GEM
|
||||||
et-orbi (1.2.7)
|
et-orbi (1.2.7)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.104.0)
|
excon (0.104.0)
|
||||||
fabrication (2.30.0)
|
fabrication (2.31.0)
|
||||||
faker (3.2.2)
|
faker (3.2.2)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.3)
|
faraday (1.10.3)
|
||||||
|
@ -522,7 +522,7 @@ GEM
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.5.4)
|
pg (1.5.4)
|
||||||
pghero (3.3.4)
|
pghero (3.4.0)
|
||||||
activerecord (>= 6)
|
activerecord (>= 6)
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
premailer (1.21.0)
|
premailer (1.21.0)
|
||||||
|
@ -755,7 +755,7 @@ GEM
|
||||||
attr_required (>= 0.0.5)
|
attr_required (>= 0.0.5)
|
||||||
httpclient (>= 2.4)
|
httpclient (>= 2.4)
|
||||||
sysexits (1.2.0)
|
sysexits (1.2.0)
|
||||||
temple (0.10.2)
|
temple (0.10.3)
|
||||||
terminal-table (3.0.2)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
terrapin (0.6.0)
|
terrapin (0.6.0)
|
||||||
|
|
6
Vagrantfile
vendored
6
Vagrantfile
vendored
|
@ -10,7 +10,11 @@ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||||
sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
|
sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
|
||||||
|
|
||||||
# Add repo for NodeJS
|
# Add repo for NodeJS
|
||||||
curl -sL https://deb.nodesource.com/setup_16.x | sudo bash -
|
sudo mkdir -p /etc/apt/keyrings
|
||||||
|
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||||
|
NODE_MAJOR=20
|
||||||
|
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
# Add firewall rule to redirect 80 to PORT and save
|
# Add firewall rule to redirect 80 to PORT and save
|
||||||
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]}
|
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AccountsIndex < Chewy::Index
|
class AccountsIndex < Chewy::Index
|
||||||
|
include DatetimeClampingConcern
|
||||||
|
|
||||||
settings index: index_preset(refresh_interval: '30s'), analysis: {
|
settings index: index_preset(refresh_interval: '30s'), analysis: {
|
||||||
filter: {
|
filter: {
|
||||||
english_stop: {
|
english_stop: {
|
||||||
|
@ -60,7 +62,7 @@ class AccountsIndex < Chewy::Index
|
||||||
field(:following_count, type: 'long')
|
field(:following_count, type: 'long')
|
||||||
field(:followers_count, type: 'long')
|
field(:followers_count, type: 'long')
|
||||||
field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties })
|
field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties })
|
||||||
field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
|
field(:last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) })
|
||||||
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
||||||
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
||||||
field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
|
field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
|
||||||
|
|
14
app/chewy/concerns/datetime_clamping_concern.rb
Normal file
14
app/chewy/concerns/datetime_clamping_concern.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DatetimeClampingConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze
|
||||||
|
MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
def clamp_date(datetime)
|
||||||
|
datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PublicStatusesIndex < Chewy::Index
|
class PublicStatusesIndex < Chewy::Index
|
||||||
|
include DatetimeClampingConcern
|
||||||
|
|
||||||
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
||||||
filter: {
|
filter: {
|
||||||
english_stop: {
|
english_stop: {
|
||||||
|
@ -62,6 +64,6 @@ class PublicStatusesIndex < Chewy::Index
|
||||||
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
|
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
|
||||||
field(:language, type: 'keyword')
|
field(:language, type: 'keyword')
|
||||||
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
|
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
|
||||||
field(:created_at, type: 'date')
|
field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class StatusesIndex < Chewy::Index
|
class StatusesIndex < Chewy::Index
|
||||||
|
include DatetimeClampingConcern
|
||||||
|
|
||||||
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
||||||
filter: {
|
filter: {
|
||||||
english_stop: {
|
english_stop: {
|
||||||
|
@ -60,6 +62,6 @@ class StatusesIndex < Chewy::Index
|
||||||
field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by })
|
field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by })
|
||||||
field(:language, type: 'keyword')
|
field(:language, type: 'keyword')
|
||||||
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
|
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
|
||||||
field(:created_at, type: 'date')
|
field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TagsIndex < Chewy::Index
|
class TagsIndex < Chewy::Index
|
||||||
|
include DatetimeClampingConcern
|
||||||
|
|
||||||
settings index: index_preset(refresh_interval: '30s'), analysis: {
|
settings index: index_preset(refresh_interval: '30s'), analysis: {
|
||||||
analyzer: {
|
analyzer: {
|
||||||
content: {
|
content: {
|
||||||
|
@ -42,6 +44,6 @@ class TagsIndex < Chewy::Index
|
||||||
field(:name, type: 'text', analyzer: 'content', value: :display_name) { field(:edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content') }
|
field(:name, type: 'text', analyzer: 'content', value: :display_name) { field(:edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content') }
|
||||||
field(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? })
|
field(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? })
|
||||||
field(:usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts })
|
field(:usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts })
|
||||||
field(:last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at })
|
field(:last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,8 +18,6 @@ class AccountsController < ApplicationController
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in?
|
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in?
|
||||||
|
|
||||||
@rss_url = rss_url
|
|
||||||
end
|
end
|
||||||
|
|
||||||
format.rss do
|
format.rss do
|
||||||
|
@ -84,29 +82,21 @@ class AccountsController < ApplicationController
|
||||||
short_account_url(@account, format: 'rss')
|
short_account_url(@account, format: 'rss')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
helper_method :rss_url
|
||||||
|
|
||||||
def media_requested?
|
def media_requested?
|
||||||
request.path.split('.').first.end_with?('/media') && !tag_requested?
|
path_without_format.end_with?('/media') && !tag_requested?
|
||||||
end
|
end
|
||||||
|
|
||||||
def replies_requested?
|
def replies_requested?
|
||||||
request.path.split('.').first.end_with?('/with_replies') && !tag_requested?
|
path_without_format.end_with?('/with_replies') && !tag_requested?
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_requested?
|
def tag_requested?
|
||||||
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
path_without_format.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_filtered_status_page
|
def path_without_format
|
||||||
cache_collection_paginated_by_id(
|
request.path.split('.').first
|
||||||
filtered_statuses,
|
|
||||||
Status,
|
|
||||||
PAGE_SIZE,
|
|
||||||
params_slice(:max_id, :min_id, :since_id)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def params_slice(*keys)
|
|
||||||
params.slice(*keys).permit(*keys)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,7 @@ module Admin
|
||||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||||
@warnings = @account.strikes.custom.latest
|
@warnings = @account.strikes.custom.latest
|
||||||
|
|
||||||
render template: 'admin/accounts/show'
|
render 'admin/accounts/show'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ module Admin
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :audit_log, :index?
|
authorize :audit_log, :index?
|
||||||
@auditable_accounts = Account.where(id: Admin::ActionLog.reorder(nil).select('distinct account_id')).select(:id, :username)
|
@auditable_accounts = Account.where(id: Admin::ActionLog.select('distinct account_id')).select(:id, :username)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -24,7 +24,7 @@ module Admin
|
||||||
@relay.enable!
|
@relay.enable!
|
||||||
redirect_to admin_relays_path
|
redirect_to admin_relays_path
|
||||||
else
|
else
|
||||||
render action: :new
|
render :new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ module Admin
|
||||||
@form = Admin::StatusBatchAction.new
|
@form = Admin::StatusBatchAction.new
|
||||||
@statuses = @report.statuses.with_includes
|
@statuses = @report.statuses.with_includes
|
||||||
|
|
||||||
render template: 'admin/reports/show'
|
render 'admin/reports/show'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ module ChallengableConcern
|
||||||
|
|
||||||
def render_challenge
|
def render_challenge
|
||||||
@body_classes = 'lighter'
|
@body_classes = 'lighter'
|
||||||
render template: 'auth/challenges/new', layout: 'auth'
|
render 'auth/challenges/new', layout: 'auth'
|
||||||
end
|
end
|
||||||
|
|
||||||
def challenge_passed?
|
def challenge_passed?
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Disputes::AppealsController < Disputes::BaseController
|
||||||
redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg')
|
redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg')
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
@appeal = e.record
|
@appeal = e.record
|
||||||
render template: 'disputes/strikes/show'
|
render 'disputes/strikes/show'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -25,7 +25,7 @@ class FiltersController < ApplicationController
|
||||||
if @filter.save
|
if @filter.save
|
||||||
redirect_to filters_path
|
redirect_to filters_path
|
||||||
else
|
else
|
||||||
render action: :new
|
render :new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class FiltersController < ApplicationController
|
||||||
if @filter.update(resource_params)
|
if @filter.update(resource_params)
|
||||||
redirect_to filters_path
|
redirect_to filters_path
|
||||||
else
|
else
|
||||||
render action: :edit
|
render :edit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ class StatusesCleanupController < ApplicationController
|
||||||
if @policy.update(resource_params)
|
if @policy.update(resource_params)
|
||||||
redirect_to statuses_cleanup_path, notice: I18n.t('generic.changes_saved_msg')
|
redirect_to statuses_cleanup_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
else
|
else
|
||||||
render action: :show
|
render :show
|
||||||
end
|
end
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
# Do nothing
|
# Do nothing
|
||||||
|
|
12
app/helpers/admin/account_actions_helper.rb
Normal file
12
app/helpers/admin/account_actions_helper.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin::AccountActionsHelper
|
||||||
|
def account_action_type_label(type)
|
||||||
|
safe_join(
|
||||||
|
[
|
||||||
|
I18n.t("simple_form.labels.admin_account_action.types.#{type}"),
|
||||||
|
content_tag(:span, I18n.t("simple_form.hints.admin_account_action.types.#{type}"), class: 'hint'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
19
app/helpers/admin/accounts_helper.rb
Normal file
19
app/helpers/admin/accounts_helper.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin::AccountsHelper
|
||||||
|
def admin_accounts_moderation_options
|
||||||
|
[
|
||||||
|
[t('admin.accounts.moderation.active'), 'active'],
|
||||||
|
[t('admin.accounts.moderation.silenced'), 'silenced'],
|
||||||
|
[t('admin.accounts.moderation.disabled'), 'disabled'],
|
||||||
|
[t('admin.accounts.moderation.suspended'), 'suspended'],
|
||||||
|
[safe_join([t('admin.accounts.moderation.pending'), "(#{pending_user_count_label})"], ' '), 'pending'],
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def pending_user_count_label
|
||||||
|
number_with_delimiter User.pending.count
|
||||||
|
end
|
||||||
|
end
|
12
app/helpers/admin/ip_blocks_helper.rb
Normal file
12
app/helpers/admin/ip_blocks_helper.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin::IpBlocksHelper
|
||||||
|
def ip_blocks_severity_label(severity)
|
||||||
|
safe_join(
|
||||||
|
[
|
||||||
|
I18n.t("simple_form.labels.ip_block.severities.#{severity}"),
|
||||||
|
content_tag(:span, I18n.t("simple_form.hints.ip_block.severities.#{severity}"), class: 'hint'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
24
app/helpers/admin/roles_helper.rb
Normal file
24
app/helpers/admin/roles_helper.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
module RolesHelper
|
||||||
|
def privilege_label(privilege)
|
||||||
|
safe_join(
|
||||||
|
[
|
||||||
|
t("admin.roles.privileges.#{privilege}"),
|
||||||
|
content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable_permissions?(permissions)
|
||||||
|
permissions.filter { |privilege| role_flag_value(privilege).zero? }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def role_flag_value(privilege)
|
||||||
|
UserRole::FLAGS[privilege] & current_user.role.computed_permissions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
app/helpers/admin/settings/discovery_helper.rb
Normal file
15
app/helpers/admin/settings/discovery_helper.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin::Settings::DiscoveryHelper
|
||||||
|
def discovery_warning_hint_text
|
||||||
|
authorized_fetch_overridden? ? t('admin.settings.security.authorized_fetch_overridden_hint') : nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def discovery_hint_text
|
||||||
|
t('admin.settings.security.authorized_fetch_hint')
|
||||||
|
end
|
||||||
|
|
||||||
|
def discovery_recommended_value
|
||||||
|
authorized_fetch_overridden? ? :overridden : nil
|
||||||
|
end
|
||||||
|
end
|
12
app/helpers/filters_helper.rb
Normal file
12
app/helpers/filters_helper.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module FiltersHelper
|
||||||
|
def filter_action_label(action)
|
||||||
|
safe_join(
|
||||||
|
[
|
||||||
|
t("simple_form.labels.filters.actions.#{action}"),
|
||||||
|
content_tag(:span, t("simple_form.hints.filters.actions.#{action}"), class: 'hint'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import api from 'mastodon/api';
|
import api from 'mastodon/api';
|
||||||
import Hashtag from 'mastodon/components/hashtag';
|
import { Hashtag } from 'mastodon/components/hashtag';
|
||||||
|
|
||||||
export default class Trends extends PureComponent {
|
export default class Trends extends PureComponent {
|
||||||
|
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Component } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
|
|
||||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
|
||||||
|
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
|
||||||
import { Skeleton } from 'mastodon/components/skeleton';
|
|
||||||
|
|
||||||
class SilentErrorBoundary extends Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
error: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidCatch() {
|
|
||||||
this.setState({ error: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to render counter of how much people are talking about hashtag
|
|
||||||
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
|
||||||
*/
|
|
||||||
export const accountsCountRenderer = (displayNumber, pluralReady) => (
|
|
||||||
<FormattedMessage
|
|
||||||
id='trends.counter_by_accounts'
|
|
||||||
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}'
|
|
||||||
values={{
|
|
||||||
count: pluralReady,
|
|
||||||
counter: <strong>{displayNumber}</strong>,
|
|
||||||
days: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
export const ImmutableHashtag = ({ hashtag }) => (
|
|
||||||
<Hashtag
|
|
||||||
name={hashtag.get('name')}
|
|
||||||
to={`/tags/${hashtag.get('name')}`}
|
|
||||||
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
|
|
||||||
// @ts-expect-error
|
|
||||||
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
ImmutableHashtag.propTypes = {
|
|
||||||
hashtag: ImmutablePropTypes.map.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => (
|
|
||||||
<div className={classNames('trends__item', className)}>
|
|
||||||
<div className='trends__item__name'>
|
|
||||||
<Link to={to}>
|
|
||||||
{name ? <>#<span>{name}</span></> : <Skeleton width={50} />}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{description ? (
|
|
||||||
<span>{description}</span>
|
|
||||||
) : (
|
|
||||||
typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{typeof uses !== 'undefined' && (
|
|
||||||
<div className='trends__item__current'>
|
|
||||||
<ShortNumber value={uses} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{withGraph && (
|
|
||||||
<div className='trends__item__sparkline'>
|
|
||||||
<SilentErrorBoundary>
|
|
||||||
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
|
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
|
||||||
</Sparklines>
|
|
||||||
</SilentErrorBoundary>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
Hashtag.propTypes = {
|
|
||||||
name: PropTypes.string,
|
|
||||||
to: PropTypes.string,
|
|
||||||
people: PropTypes.number,
|
|
||||||
description: PropTypes.node,
|
|
||||||
uses: PropTypes.number,
|
|
||||||
history: PropTypes.arrayOf(PropTypes.number),
|
|
||||||
className: PropTypes.string,
|
|
||||||
withGraph: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
Hashtag.defaultProps = {
|
|
||||||
withGraph: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Hashtag;
|
|
145
app/javascript/mastodon/components/hashtag.tsx
Normal file
145
app/javascript/mastodon/components/hashtag.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import type { JSX } from 'react';
|
||||||
|
import { Component } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type Immutable from 'immutable';
|
||||||
|
|
||||||
|
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||||
|
|
||||||
|
import { ShortNumber } from 'mastodon/components/short_number';
|
||||||
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
|
|
||||||
|
interface SilentErrorBoundaryProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SilentErrorBoundary extends Component<SilentErrorBoundaryProps> {
|
||||||
|
state = {
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidCatch() {
|
||||||
|
this.setState({ error: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to render counter of how much people are talking about hashtag
|
||||||
|
* @param displayNumber Counter number to display
|
||||||
|
* @param pluralReady Whether the count is plural
|
||||||
|
* @returns Formatted counter of how much people are talking about hashtag
|
||||||
|
*/
|
||||||
|
export const accountsCountRenderer = (
|
||||||
|
displayNumber: JSX.Element,
|
||||||
|
pluralReady: number,
|
||||||
|
) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='trends.counter_by_accounts'
|
||||||
|
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}'
|
||||||
|
values={{
|
||||||
|
count: pluralReady,
|
||||||
|
counter: <strong>{displayNumber}</strong>,
|
||||||
|
days: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ImmutableHashtagProps {
|
||||||
|
hashtag: Immutable.Map<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImmutableHashtag = ({ hashtag }: ImmutableHashtagProps) => (
|
||||||
|
<Hashtag
|
||||||
|
name={hashtag.get('name') as string}
|
||||||
|
to={`/tags/${hashtag.get('name') as string}`}
|
||||||
|
people={
|
||||||
|
(hashtag.getIn(['history', 0, 'accounts']) as number) * 1 +
|
||||||
|
(hashtag.getIn(['history', 1, 'accounts']) as number) * 1
|
||||||
|
}
|
||||||
|
history={(
|
||||||
|
hashtag.get('history') as Immutable.Collection.Indexed<
|
||||||
|
Immutable.Map<string, number>
|
||||||
|
>
|
||||||
|
)
|
||||||
|
.reverse()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
.map((day) => day.get('uses')!)
|
||||||
|
.toArray()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface HashtagProps {
|
||||||
|
className?: string;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
history?: number[];
|
||||||
|
name: string;
|
||||||
|
people: number;
|
||||||
|
to: string;
|
||||||
|
uses?: number;
|
||||||
|
withGraph?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Hashtag: React.FC<HashtagProps> = ({
|
||||||
|
name,
|
||||||
|
to,
|
||||||
|
people,
|
||||||
|
uses,
|
||||||
|
history,
|
||||||
|
className,
|
||||||
|
description,
|
||||||
|
withGraph = true,
|
||||||
|
}) => (
|
||||||
|
<div className={classNames('trends__item', className)}>
|
||||||
|
<div className='trends__item__name'>
|
||||||
|
<Link to={to}>
|
||||||
|
{name ? (
|
||||||
|
<>
|
||||||
|
#<span>{name}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Skeleton width={50} />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{description ? (
|
||||||
|
<span>{description}</span>
|
||||||
|
) : typeof people !== 'undefined' ? (
|
||||||
|
<ShortNumber value={people} renderer={accountsCountRenderer} />
|
||||||
|
) : (
|
||||||
|
<Skeleton width={100} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{typeof uses !== 'undefined' && (
|
||||||
|
<div className='trends__item__current'>
|
||||||
|
<ShortNumber value={uses} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{withGraph && (
|
||||||
|
<div className='trends__item__sparkline'>
|
||||||
|
<SilentErrorBoundary>
|
||||||
|
<Sparklines
|
||||||
|
width={50}
|
||||||
|
height={28}
|
||||||
|
data={history ? history : Array.from(Array(7)).map(() => 0)}
|
||||||
|
>
|
||||||
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
|
</Sparklines>
|
||||||
|
</SilentErrorBoundary>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -5,7 +5,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import Hashtag from 'mastodon/components/hashtag';
|
import { Hashtag } from 'mastodon/components/hashtag';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' },
|
lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' },
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags';
|
import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags';
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
import Hashtag from 'mastodon/components/hashtag';
|
import { Hashtag } from 'mastodon/components/hashtag';
|
||||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
import Column from 'mastodon/features/ui/components/column';
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
|
|
||||||
|
|
|
@ -250,6 +250,9 @@
|
||||||
"notifications.column_settings.unread_notifications.highlight": "Lig ongelese kennisgewings uit",
|
"notifications.column_settings.unread_notifications.highlight": "Lig ongelese kennisgewings uit",
|
||||||
"notifications.filter.boosts": "Aangestuurde plasings",
|
"notifications.filter.boosts": "Aangestuurde plasings",
|
||||||
"notifications.group": "{count} kennisgewings",
|
"notifications.group": "{count} kennisgewings",
|
||||||
|
"notifications.permission_denied_alert": "Lessenaarkennisgewings kan nie geaktiveer word nie omdat 'n webblaaier toegewing voorheen geweier was",
|
||||||
|
"notifications_permission_banner.enable": "Aktiveer lessenaarkennissgewings",
|
||||||
|
"notifications_permission_banner.how_to_control": "Om kennisgewings te ontvang wanner Mastodon nie oop is nie, aktiveer lessenaarkennisgewings. Jy kan beheer watter spesifieke tipe interaksies lessenaarkennisgewings genereer deur die {icon} knoppie hier bo sodra hulle geaktiveer is.",
|
||||||
"onboarding.actions.go_to_explore": "See what's trending",
|
"onboarding.actions.go_to_explore": "See what's trending",
|
||||||
"onboarding.actions.go_to_home": "Go to your home feed",
|
"onboarding.actions.go_to_home": "Go to your home feed",
|
||||||
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
|
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Супадзенне паведамленняў {x}",
|
"search.quick_action.status_search": "Супадзенне паведамленняў {x}",
|
||||||
"search.search_or_paste": "Пошук",
|
"search.search_or_paste": "Пошук",
|
||||||
"search_popout.full_text_search_disabled_message": "Недаступна на {domain}.",
|
"search_popout.full_text_search_disabled_message": "Недаступна на {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Даступна толькі пры ўваходзе ў сістэму.",
|
||||||
"search_popout.language_code": "ISO код мовы",
|
"search_popout.language_code": "ISO код мовы",
|
||||||
"search_popout.options": "Параметры пошуку",
|
"search_popout.options": "Параметры пошуку",
|
||||||
"search_popout.quick_actions": "Хуткія дзеянні",
|
"search_popout.quick_actions": "Хуткія дзеянні",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Съвпадение на публикации {x}",
|
"search.quick_action.status_search": "Съвпадение на публикации {x}",
|
||||||
"search.search_or_paste": "Търсене или поставяне на URL адрес",
|
"search.search_or_paste": "Търсене или поставяне на URL адрес",
|
||||||
"search_popout.full_text_search_disabled_message": "Не е достъпно на {domain}.",
|
"search_popout.full_text_search_disabled_message": "Не е достъпно на {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Достъпно само при влизане в системата.",
|
||||||
"search_popout.language_code": "Код на езика по ISO",
|
"search_popout.language_code": "Код на езика по ISO",
|
||||||
"search_popout.options": "Възможности при търсене",
|
"search_popout.options": "Възможности при търсене",
|
||||||
"search_popout.quick_actions": "Бързи действия",
|
"search_popout.quick_actions": "Бързи действия",
|
||||||
|
|
|
@ -605,6 +605,7 @@
|
||||||
"search.quick_action.status_search": "Tuts coincidint amb {x}",
|
"search.quick_action.status_search": "Tuts coincidint amb {x}",
|
||||||
"search.search_or_paste": "Cerca o escriu l'URL",
|
"search.search_or_paste": "Cerca o escriu l'URL",
|
||||||
"search_popout.full_text_search_disabled_message": "No disponible a {domain}.",
|
"search_popout.full_text_search_disabled_message": "No disponible a {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Només disponible en iniciar la sessió.",
|
||||||
"search_popout.language_code": "Codi de llengua ISO",
|
"search_popout.language_code": "Codi de llengua ISO",
|
||||||
"search_popout.options": "Opcions de cerca",
|
"search_popout.options": "Opcions de cerca",
|
||||||
"search_popout.quick_actions": "Accions ràpides",
|
"search_popout.quick_actions": "Accions ràpides",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Indlæg matchende {x}",
|
"search.quick_action.status_search": "Indlæg matchende {x}",
|
||||||
"search.search_or_paste": "Søg efter eller angiv URL",
|
"search.search_or_paste": "Søg efter eller angiv URL",
|
||||||
"search_popout.full_text_search_disabled_message": "Utilgængelig på {domain}.",
|
"search_popout.full_text_search_disabled_message": "Utilgængelig på {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Kun tilgængelig, når logget ind.",
|
||||||
"search_popout.language_code": "ISO-sprogkode",
|
"search_popout.language_code": "ISO-sprogkode",
|
||||||
"search_popout.options": "Søgevalg",
|
"search_popout.options": "Søgevalg",
|
||||||
"search_popout.quick_actions": "Hurtige handlinger",
|
"search_popout.quick_actions": "Hurtige handlinger",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Beiträge passend zu {x}",
|
"search.quick_action.status_search": "Beiträge passend zu {x}",
|
||||||
"search.search_or_paste": "Suchen oder URL einfügen",
|
"search.search_or_paste": "Suchen oder URL einfügen",
|
||||||
"search_popout.full_text_search_disabled_message": "Auf {domain} nicht verfügbar.",
|
"search_popout.full_text_search_disabled_message": "Auf {domain} nicht verfügbar.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Nur verfügbar, wenn angemeldet.",
|
||||||
"search_popout.language_code": "ISO-Sprachcode",
|
"search_popout.language_code": "ISO-Sprachcode",
|
||||||
"search_popout.options": "Suchoptionen",
|
"search_popout.options": "Suchoptionen",
|
||||||
"search_popout.quick_actions": "Schnellaktionen",
|
"search_popout.quick_actions": "Schnellaktionen",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Mensajes que coinciden con {x}",
|
"search.quick_action.status_search": "Mensajes que coinciden con {x}",
|
||||||
"search.search_or_paste": "Buscar o pegar dirección web",
|
"search.search_or_paste": "Buscar o pegar dirección web",
|
||||||
"search_popout.full_text_search_disabled_message": "No disponible en {domain}.",
|
"search_popout.full_text_search_disabled_message": "No disponible en {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Solo disponible al iniciar sesión.",
|
||||||
"search_popout.language_code": "Código ISO de idioma",
|
"search_popout.language_code": "Código ISO de idioma",
|
||||||
"search_popout.options": "Opciones de búsqueda",
|
"search_popout.options": "Opciones de búsqueda",
|
||||||
"search_popout.quick_actions": "Acciones rápidas",
|
"search_popout.quick_actions": "Acciones rápidas",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Publicaciones que coinciden con {x}",
|
"search.quick_action.status_search": "Publicaciones que coinciden con {x}",
|
||||||
"search.search_or_paste": "Buscar o pegar URL",
|
"search.search_or_paste": "Buscar o pegar URL",
|
||||||
"search_popout.full_text_search_disabled_message": "No disponible en {domain}.",
|
"search_popout.full_text_search_disabled_message": "No disponible en {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Solo disponible si inicias sesión.",
|
||||||
"search_popout.language_code": "Código de idioma ISO",
|
"search_popout.language_code": "Código de idioma ISO",
|
||||||
"search_popout.options": "Opciones de búsqueda",
|
"search_popout.options": "Opciones de búsqueda",
|
||||||
"search_popout.quick_actions": "Acciones rápidas",
|
"search_popout.quick_actions": "Acciones rápidas",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Publicaciones que coinciden con {x}",
|
"search.quick_action.status_search": "Publicaciones que coinciden con {x}",
|
||||||
"search.search_or_paste": "Buscar o pegar URL",
|
"search.search_or_paste": "Buscar o pegar URL",
|
||||||
"search_popout.full_text_search_disabled_message": "No disponible en {domain}.",
|
"search_popout.full_text_search_disabled_message": "No disponible en {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Solo disponible si inicias sesión.",
|
||||||
"search_popout.language_code": "Código de idioma ISO",
|
"search_popout.language_code": "Código de idioma ISO",
|
||||||
"search_popout.options": "Opciones de búsqueda",
|
"search_popout.options": "Opciones de búsqueda",
|
||||||
"search_popout.quick_actions": "Acciones rápidas",
|
"search_popout.quick_actions": "Acciones rápidas",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"account.blocked": "Blokeeritud",
|
"account.blocked": "Blokeeritud",
|
||||||
"account.browse_more_on_origin_server": "Vaata rohkem algsel profiilil",
|
"account.browse_more_on_origin_server": "Vaata rohkem algsel profiilil",
|
||||||
"account.cancel_follow_request": "Võta jälgimistaotlus tagasi",
|
"account.cancel_follow_request": "Võta jälgimistaotlus tagasi",
|
||||||
|
"account.copy": "Kopeeri link profiili",
|
||||||
"account.direct": "Maini privaatselt @{name}",
|
"account.direct": "Maini privaatselt @{name}",
|
||||||
"account.disable_notifications": "Peata teavitused @{name} postitustest",
|
"account.disable_notifications": "Peata teavitused @{name} postitustest",
|
||||||
"account.domain_blocked": "Domeen peidetud",
|
"account.domain_blocked": "Domeen peidetud",
|
||||||
|
@ -191,6 +192,7 @@
|
||||||
"conversation.mark_as_read": "Märgi loetuks",
|
"conversation.mark_as_read": "Märgi loetuks",
|
||||||
"conversation.open": "Vaata vestlust",
|
"conversation.open": "Vaata vestlust",
|
||||||
"conversation.with": "Koos {names}",
|
"conversation.with": "Koos {names}",
|
||||||
|
"copy_icon_button.copied": "Kopeeritud vahemällu",
|
||||||
"copypaste.copied": "Kopeeritud",
|
"copypaste.copied": "Kopeeritud",
|
||||||
"copypaste.copy_to_clipboard": "Kopeeri vahemällu",
|
"copypaste.copy_to_clipboard": "Kopeeri vahemällu",
|
||||||
"directory.federated": "Tuntud födiversumist",
|
"directory.federated": "Tuntud födiversumist",
|
||||||
|
@ -390,6 +392,7 @@
|
||||||
"lists.search": "Otsi enda jälgitavate inimeste hulgast",
|
"lists.search": "Otsi enda jälgitavate inimeste hulgast",
|
||||||
"lists.subheading": "Sinu nimekirjad",
|
"lists.subheading": "Sinu nimekirjad",
|
||||||
"load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}",
|
"load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}",
|
||||||
|
"loading_indicator.label": "Laadimine…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Varja pilt} other {Varja pildid}}",
|
"media_gallery.toggle_visible": "{number, plural, one {Varja pilt} other {Varja pildid}}",
|
||||||
"moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
|
"moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
|
||||||
"mute_modal.duration": "Kestus",
|
"mute_modal.duration": "Kestus",
|
||||||
|
@ -478,6 +481,17 @@
|
||||||
"onboarding.follows.empty": "Kahjuks ei saa hetkel tulemusi näidata. Proovi kasutada otsingut või lehitse uurimise lehte, et leida inimesi, keda jälgida, või proovi hiljem uuesti.",
|
"onboarding.follows.empty": "Kahjuks ei saa hetkel tulemusi näidata. Proovi kasutada otsingut või lehitse uurimise lehte, et leida inimesi, keda jälgida, või proovi hiljem uuesti.",
|
||||||
"onboarding.follows.lead": "Haldad ise oma koduvoogu. Mida rohkemaid inimesi jälgid, seda aktiivsem ja huvitavam see on. Need profiilid võiksid olla head alustamiskohad — saad nende jälgimise alati lõpetada!",
|
"onboarding.follows.lead": "Haldad ise oma koduvoogu. Mida rohkemaid inimesi jälgid, seda aktiivsem ja huvitavam see on. Need profiilid võiksid olla head alustamiskohad — saad nende jälgimise alati lõpetada!",
|
||||||
"onboarding.follows.title": "Populaarne Mastodonis",
|
"onboarding.follows.title": "Populaarne Mastodonis",
|
||||||
|
"onboarding.profile.discoverable": "Muuda mu profiil avastatavaks",
|
||||||
|
"onboarding.profile.discoverable_hint": "Kui nõustud enda avastamisega Mastodonis, võivad sinu postitused ilmuda otsingutulemustes ja trendides ning sinu profiili võidakse soovitada sinuga sarnaste huvidega inimestele.",
|
||||||
|
"onboarding.profile.display_name": "Näidatav nimi",
|
||||||
|
"onboarding.profile.display_name_hint": "Su täisnimi või naljanimi…",
|
||||||
|
"onboarding.profile.lead": "Saad selle alati hiljem seadetes lõpuni viia, kus on saadaval veel rohkem kohandamisvalikuid.",
|
||||||
|
"onboarding.profile.note": "Elulugu",
|
||||||
|
"onboarding.profile.note_hint": "Saad @mainida teisi kasutajaid või #sildistada…",
|
||||||
|
"onboarding.profile.save_and_continue": "Salvesta ja jätka",
|
||||||
|
"onboarding.profile.title": "Profiili seadistamine",
|
||||||
|
"onboarding.profile.upload_avatar": "Laadi üles profiilipilt",
|
||||||
|
"onboarding.profile.upload_header": "Laadi üles profiili päis",
|
||||||
"onboarding.share.lead": "Anna inimestele teada, kuidas sind Mastodonist üles leida!",
|
"onboarding.share.lead": "Anna inimestele teada, kuidas sind Mastodonist üles leida!",
|
||||||
"onboarding.share.message": "Ma olen #Mastodon võrgustikus {username}! tule ja jälgi mind aadressil {url}",
|
"onboarding.share.message": "Ma olen #Mastodon võrgustikus {username}! tule ja jälgi mind aadressil {url}",
|
||||||
"onboarding.share.next_steps": "Võimalikud järgmised sammud:",
|
"onboarding.share.next_steps": "Võimalikud järgmised sammud:",
|
||||||
|
@ -521,6 +535,7 @@
|
||||||
"privacy.unlisted.short": "Määramata",
|
"privacy.unlisted.short": "Määramata",
|
||||||
"privacy_policy.last_updated": "Viimati uuendatud {date}",
|
"privacy_policy.last_updated": "Viimati uuendatud {date}",
|
||||||
"privacy_policy.title": "Isikuandmete kaitse",
|
"privacy_policy.title": "Isikuandmete kaitse",
|
||||||
|
"recommended": "Soovitatud",
|
||||||
"refresh": "Värskenda",
|
"refresh": "Värskenda",
|
||||||
"regeneration_indicator.label": "Laeb…",
|
"regeneration_indicator.label": "Laeb…",
|
||||||
"regeneration_indicator.sublabel": "Su koduvoog on ettevalmistamisel!",
|
"regeneration_indicator.sublabel": "Su koduvoog on ettevalmistamisel!",
|
||||||
|
|
|
@ -16,17 +16,17 @@
|
||||||
"account.badges.bot": "Bot-a",
|
"account.badges.bot": "Bot-a",
|
||||||
"account.badges.group": "Taldea",
|
"account.badges.group": "Taldea",
|
||||||
"account.block": "Blokeatu @{name}",
|
"account.block": "Blokeatu @{name}",
|
||||||
"account.block_domain": "Ezkutatu {domain} domeinuko guztia",
|
"account.block_domain": "Blokeatu {domain} domeinua",
|
||||||
"account.block_short": "Blokeatu",
|
"account.block_short": "Blokeatu",
|
||||||
"account.blocked": "Blokeatuta",
|
"account.blocked": "Blokeatuta",
|
||||||
"account.browse_more_on_origin_server": "Arakatu gehiago jatorrizko profilean",
|
"account.browse_more_on_origin_server": "Arakatu gehiago jatorrizko profilean",
|
||||||
"account.cancel_follow_request": "Baztertu jarraitzeko eskaera",
|
"account.cancel_follow_request": "Baztertu jarraitzeko eskaera",
|
||||||
"account.copy": "Kopiatu profilerako esteka",
|
"account.copy": "Kopiatu profilerako esteka",
|
||||||
"account.direct": "Aipatu pribatuki @{name}",
|
"account.direct": "Aipatu pribatuki @{name}",
|
||||||
"account.disable_notifications": "Utzi jakinarazteari @{name} erabiltzailearen bidalketetan",
|
"account.disable_notifications": "Utzi jakinarazteari @{name} erabiltzaileak argitaratzean",
|
||||||
"account.domain_blocked": "Ezkutatutako domeinua",
|
"account.domain_blocked": "Ezkutatutako domeinua",
|
||||||
"account.edit_profile": "Aldatu profila",
|
"account.edit_profile": "Aldatu profila",
|
||||||
"account.enable_notifications": "Jakinarazi @{name} erabiltzaileak bidalketak egitean",
|
"account.enable_notifications": "Jakinarazi @{name} erabiltzaileak argitaratzean",
|
||||||
"account.endorse": "Nabarmendu profilean",
|
"account.endorse": "Nabarmendu profilean",
|
||||||
"account.featured_tags.last_status_at": "Azken bidalketa {date} datan",
|
"account.featured_tags.last_status_at": "Azken bidalketa {date} datan",
|
||||||
"account.featured_tags.last_status_never": "Bidalketarik ez",
|
"account.featured_tags.last_status_never": "Bidalketarik ez",
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
"account.follows.empty": "Erabiltzaile honek ez du inor jarraitzen oraindik.",
|
"account.follows.empty": "Erabiltzaile honek ez du inor jarraitzen oraindik.",
|
||||||
"account.follows_you": "Jarraitzen dizu",
|
"account.follows_you": "Jarraitzen dizu",
|
||||||
"account.go_to_profile": "Joan profilera",
|
"account.go_to_profile": "Joan profilera",
|
||||||
"account.hide_reblogs": "Ezkutatu @{name}(r)en bultzadak",
|
"account.hide_reblogs": "Ezkutatu @{name} erabiltzailearen bultzadak",
|
||||||
"account.in_memoriam": "Oroimenezkoa.",
|
"account.in_memoriam": "Oroimenezkoa.",
|
||||||
"account.joined_short": "Elkartuta",
|
"account.joined_short": "Elkartuta",
|
||||||
"account.languages": "Aldatu harpidetutako hizkuntzak",
|
"account.languages": "Aldatu harpidetutako hizkuntzak",
|
||||||
|
@ -60,8 +60,8 @@
|
||||||
"account.report": "Salatu @{name}",
|
"account.report": "Salatu @{name}",
|
||||||
"account.requested": "Onarpenaren zain. Egin klik jarraipen-eskaera ezeztatzeko",
|
"account.requested": "Onarpenaren zain. Egin klik jarraipen-eskaera ezeztatzeko",
|
||||||
"account.requested_follow": "{name}-(e)k zu jarraitzeko eskaera egin du",
|
"account.requested_follow": "{name}-(e)k zu jarraitzeko eskaera egin du",
|
||||||
"account.share": "@{name}(e)ren profila elkarbanatu",
|
"account.share": "Partekatu @{name} erabiltzailearen profila",
|
||||||
"account.show_reblogs": "Erakutsi @{name}(r)en bultzadak",
|
"account.show_reblogs": "Erakutsi @{name} erabiltzailearen bultzadak",
|
||||||
"account.statuses_counter": "{count, plural, one {Bidalketa {counter}} other {{counter} bidalketa}}",
|
"account.statuses_counter": "{count, plural, one {Bidalketa {counter}} other {{counter} bidalketa}}",
|
||||||
"account.unblock": "Desblokeatu @{name}",
|
"account.unblock": "Desblokeatu @{name}",
|
||||||
"account.unblock_domain": "Berriz erakutsi {domain}",
|
"account.unblock_domain": "Berriz erakutsi {domain}",
|
||||||
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "{x}-(r)ekin bat datozen argitalpenak",
|
"search.quick_action.status_search": "{x}-(r)ekin bat datozen argitalpenak",
|
||||||
"search.search_or_paste": "Bilatu edo itsatsi URLa",
|
"search.search_or_paste": "Bilatu edo itsatsi URLa",
|
||||||
"search_popout.full_text_search_disabled_message": "{domain}-en ez dago eskuragarri.",
|
"search_popout.full_text_search_disabled_message": "{domain}-en ez dago eskuragarri.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Soilik erabilgarri saioa hastean.",
|
||||||
"search_popout.language_code": "ISO hizkuntza-kodea",
|
"search_popout.language_code": "ISO hizkuntza-kodea",
|
||||||
"search_popout.options": "Bilaketaren aukerak",
|
"search_popout.options": "Bilaketaren aukerak",
|
||||||
"search_popout.quick_actions": "Ekintza azkarrak",
|
"search_popout.quick_actions": "Ekintza azkarrak",
|
||||||
|
|
|
@ -482,7 +482,7 @@
|
||||||
"onboarding.follows.lead": "Kokoat oman kotisyötteesi itse. Mitä enemmän ihmisiä seuraat, sitä aktiivisempi ja kiinnostavampi syöte on. Nämä profiilit voivat olla alkuun hyvä lähtökohta — voit aina lopettaa niiden seuraamisen myöhemmin!",
|
"onboarding.follows.lead": "Kokoat oman kotisyötteesi itse. Mitä enemmän ihmisiä seuraat, sitä aktiivisempi ja kiinnostavampi syöte on. Nämä profiilit voivat olla alkuun hyvä lähtökohta — voit aina lopettaa niiden seuraamisen myöhemmin!",
|
||||||
"onboarding.follows.title": "Mukauta kotisyötettäsi",
|
"onboarding.follows.title": "Mukauta kotisyötettäsi",
|
||||||
"onboarding.profile.discoverable": "Aseta profiilini löydettäväksi",
|
"onboarding.profile.discoverable": "Aseta profiilini löydettäväksi",
|
||||||
"onboarding.profile.discoverable_hint": "Kun olet määrittänyt itsesi löydettäväksi Mastodonista, voivat julkaisusi näkyä hakutuloksissa ja suosituissa kohteissa, ja profiiliasi voidaan ehdottaa käyttäjille, jotka ovat kiinnostuneet samoista aiheista kuin sinä.",
|
"onboarding.profile.discoverable_hint": "Kun olet määrittänyt itsesi löydettäväksi Mastodonista, julkaisusi voivat näkyä hakutuloksissa ja suosituissa kohteissa ja profiiliasi voidaan ehdottaa käyttäjille, jotka ovat kiinnostuneet samoista aiheista kuin sinä.",
|
||||||
"onboarding.profile.display_name": "Näyttönimi",
|
"onboarding.profile.display_name": "Näyttönimi",
|
||||||
"onboarding.profile.display_name_hint": "Koko nimesi tai lempinimesi…",
|
"onboarding.profile.display_name_hint": "Koko nimesi tai lempinimesi…",
|
||||||
"onboarding.profile.lead": "Voit viimeistellä tämän milloin tahansa asetuksista, jotka tarjoavat vielä enemmän mukautusvalintoja.",
|
"onboarding.profile.lead": "Voit viimeistellä tämän milloin tahansa asetuksista, jotka tarjoavat vielä enemmän mukautusvalintoja.",
|
||||||
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Julkaisut haulla {x}",
|
"search.quick_action.status_search": "Julkaisut haulla {x}",
|
||||||
"search.search_or_paste": "Hae tai liitä URL-osoite",
|
"search.search_or_paste": "Hae tai liitä URL-osoite",
|
||||||
"search_popout.full_text_search_disabled_message": "Ei saatavilla palvelimella {domain}.",
|
"search_popout.full_text_search_disabled_message": "Ei saatavilla palvelimella {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Saatavilla vain sisäänkirjautuneena.",
|
||||||
"search_popout.language_code": "ISO-kielikoodi",
|
"search_popout.language_code": "ISO-kielikoodi",
|
||||||
"search_popout.options": "Hakuvalinnat",
|
"search_popout.options": "Hakuvalinnat",
|
||||||
"search_popout.quick_actions": "Pikatoiminnot",
|
"search_popout.quick_actions": "Pikatoiminnot",
|
||||||
|
|
|
@ -596,6 +596,7 @@
|
||||||
"search.quick_action.status_search": "Postar, ið samsvara {x}",
|
"search.quick_action.status_search": "Postar, ið samsvara {x}",
|
||||||
"search.search_or_paste": "Leita ella set URL inn",
|
"search.search_or_paste": "Leita ella set URL inn",
|
||||||
"search_popout.full_text_search_disabled_message": "Ikki tøkt á {domain}.",
|
"search_popout.full_text_search_disabled_message": "Ikki tøkt á {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Einans tøkt um innritað er.",
|
||||||
"search_popout.language_code": "ISO málkoda",
|
"search_popout.language_code": "ISO málkoda",
|
||||||
"search_popout.options": "Leitimøguleikar",
|
"search_popout.options": "Leitimøguleikar",
|
||||||
"search_popout.quick_actions": "Skjótar atgerðir",
|
"search_popout.quick_actions": "Skjótar atgerðir",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"account.blocked": "Bloqué·e",
|
"account.blocked": "Bloqué·e",
|
||||||
"account.browse_more_on_origin_server": "Parcourir davantage sur le profil original",
|
"account.browse_more_on_origin_server": "Parcourir davantage sur le profil original",
|
||||||
"account.cancel_follow_request": "Retirer cette demande d'abonnement",
|
"account.cancel_follow_request": "Retirer cette demande d'abonnement",
|
||||||
|
"account.copy": "Copier le lien vers le profil",
|
||||||
"account.direct": "Mention privée @{name}",
|
"account.direct": "Mention privée @{name}",
|
||||||
"account.disable_notifications": "Ne plus me notifier quand @{name} publie",
|
"account.disable_notifications": "Ne plus me notifier quand @{name} publie",
|
||||||
"account.domain_blocked": "Domaine bloqué",
|
"account.domain_blocked": "Domaine bloqué",
|
||||||
|
@ -191,6 +192,7 @@
|
||||||
"conversation.mark_as_read": "Marquer comme lu",
|
"conversation.mark_as_read": "Marquer comme lu",
|
||||||
"conversation.open": "Afficher cette conversation",
|
"conversation.open": "Afficher cette conversation",
|
||||||
"conversation.with": "Avec {names}",
|
"conversation.with": "Avec {names}",
|
||||||
|
"copy_icon_button.copied": "Copié dans le presse-papier",
|
||||||
"copypaste.copied": "Copié",
|
"copypaste.copied": "Copié",
|
||||||
"copypaste.copy_to_clipboard": "Copier dans le presse-papiers",
|
"copypaste.copy_to_clipboard": "Copier dans le presse-papiers",
|
||||||
"directory.federated": "D'un fediverse connu",
|
"directory.federated": "D'un fediverse connu",
|
||||||
|
@ -479,12 +481,17 @@
|
||||||
"onboarding.follows.empty": "Malheureusement, aucun résultat ne peut être affiché pour le moment. Vous pouvez essayer de rechercher ou de parcourir la page \"Explorer\" pour trouver des personnes à suivre, ou réessayer plus tard.",
|
"onboarding.follows.empty": "Malheureusement, aucun résultat ne peut être affiché pour le moment. Vous pouvez essayer de rechercher ou de parcourir la page \"Explorer\" pour trouver des personnes à suivre, ou réessayer plus tard.",
|
||||||
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
|
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
|
||||||
"onboarding.follows.title": "Popular on Mastodon",
|
"onboarding.follows.title": "Popular on Mastodon",
|
||||||
|
"onboarding.profile.discoverable": "Rendre mon profil découvrable",
|
||||||
|
"onboarding.profile.discoverable_hint": "Lorsque vous acceptez d'être découvert sur Mastodon, vos messages peuvent apparaître dans les résultats de recherche et les tendances, et votre profil peut être suggéré à des personnes ayant des intérêts similaires aux vôtres.",
|
||||||
"onboarding.profile.display_name": "Nom affiché",
|
"onboarding.profile.display_name": "Nom affiché",
|
||||||
|
"onboarding.profile.display_name_hint": "Votre nom complet ou votre nom rigolo…",
|
||||||
|
"onboarding.profile.lead": "Vous pouvez toujours compléter cela plus tard dans les paramètres. Vous y trouverez encore plus d'options de personnalisation.",
|
||||||
"onboarding.profile.note": "Biographie",
|
"onboarding.profile.note": "Biographie",
|
||||||
|
"onboarding.profile.note_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…",
|
||||||
"onboarding.profile.save_and_continue": "Enregistrer et continuer",
|
"onboarding.profile.save_and_continue": "Enregistrer et continuer",
|
||||||
"onboarding.profile.title": "Configuration du profil",
|
"onboarding.profile.title": "Configuration du profil",
|
||||||
"onboarding.profile.upload_avatar": "Importer une photo de profil",
|
"onboarding.profile.upload_avatar": "Importer une photo de profil",
|
||||||
"onboarding.profile.upload_header": "Envoyer une image de profil",
|
"onboarding.profile.upload_header": "Importer un entête de profil",
|
||||||
"onboarding.share.lead": "Faites savoir aux gens comment vous trouver sur Mastodon!",
|
"onboarding.share.lead": "Faites savoir aux gens comment vous trouver sur Mastodon!",
|
||||||
"onboarding.share.message": "Je suis {username} sur #Mastodon! Suivez-moi sur {url}",
|
"onboarding.share.message": "Je suis {username} sur #Mastodon! Suivez-moi sur {url}",
|
||||||
"onboarding.share.next_steps": "Étapes suivantes possibles:",
|
"onboarding.share.next_steps": "Étapes suivantes possibles:",
|
||||||
|
@ -528,6 +535,7 @@
|
||||||
"privacy.unlisted.short": "Non listé",
|
"privacy.unlisted.short": "Non listé",
|
||||||
"privacy_policy.last_updated": "Dernière mise à jour {date}",
|
"privacy_policy.last_updated": "Dernière mise à jour {date}",
|
||||||
"privacy_policy.title": "Politique de confidentialité",
|
"privacy_policy.title": "Politique de confidentialité",
|
||||||
|
"recommended": "Recommandé",
|
||||||
"refresh": "Actualiser",
|
"refresh": "Actualiser",
|
||||||
"regeneration_indicator.label": "Chargement…",
|
"regeneration_indicator.label": "Chargement…",
|
||||||
"regeneration_indicator.sublabel": "Votre fil d'accueil est en cours de préparation!",
|
"regeneration_indicator.sublabel": "Votre fil d'accueil est en cours de préparation!",
|
||||||
|
@ -598,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Publications correspondant à {x}",
|
"search.quick_action.status_search": "Publications correspondant à {x}",
|
||||||
"search.search_or_paste": "Rechercher ou saisir un URL",
|
"search.search_or_paste": "Rechercher ou saisir un URL",
|
||||||
"search_popout.full_text_search_disabled_message": "Non disponible sur {domain}.",
|
"search_popout.full_text_search_disabled_message": "Non disponible sur {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Disponible uniquement lorsque vous êtes connecté.",
|
||||||
"search_popout.language_code": "code de langue ISO",
|
"search_popout.language_code": "code de langue ISO",
|
||||||
"search_popout.options": "Options de recherche",
|
"search_popout.options": "Options de recherche",
|
||||||
"search_popout.quick_actions": "Actions rapides",
|
"search_popout.quick_actions": "Actions rapides",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"account.blocked": "Bloqué·e",
|
"account.blocked": "Bloqué·e",
|
||||||
"account.browse_more_on_origin_server": "Parcourir davantage sur le profil original",
|
"account.browse_more_on_origin_server": "Parcourir davantage sur le profil original",
|
||||||
"account.cancel_follow_request": "Annuler le suivi",
|
"account.cancel_follow_request": "Annuler le suivi",
|
||||||
|
"account.copy": "Copier le lien vers le profil",
|
||||||
"account.direct": "Mention privée @{name}",
|
"account.direct": "Mention privée @{name}",
|
||||||
"account.disable_notifications": "Ne plus me notifier quand @{name} publie quelque chose",
|
"account.disable_notifications": "Ne plus me notifier quand @{name} publie quelque chose",
|
||||||
"account.domain_blocked": "Domaine bloqué",
|
"account.domain_blocked": "Domaine bloqué",
|
||||||
|
@ -191,6 +192,7 @@
|
||||||
"conversation.mark_as_read": "Marquer comme lu",
|
"conversation.mark_as_read": "Marquer comme lu",
|
||||||
"conversation.open": "Afficher la conversation",
|
"conversation.open": "Afficher la conversation",
|
||||||
"conversation.with": "Avec {names}",
|
"conversation.with": "Avec {names}",
|
||||||
|
"copy_icon_button.copied": "Copié dans le presse-papier",
|
||||||
"copypaste.copied": "Copié",
|
"copypaste.copied": "Copié",
|
||||||
"copypaste.copy_to_clipboard": "Copier dans le presse-papiers",
|
"copypaste.copy_to_clipboard": "Copier dans le presse-papiers",
|
||||||
"directory.federated": "Du fédiverse connu",
|
"directory.federated": "Du fédiverse connu",
|
||||||
|
@ -479,12 +481,17 @@
|
||||||
"onboarding.follows.empty": "Malheureusement, aucun résultat ne peut être affiché pour le moment. Vous pouvez essayer d'utiliser la recherche ou parcourir la page de découverte pour trouver des personnes à suivre, ou réessayez plus tard.",
|
"onboarding.follows.empty": "Malheureusement, aucun résultat ne peut être affiché pour le moment. Vous pouvez essayer d'utiliser la recherche ou parcourir la page de découverte pour trouver des personnes à suivre, ou réessayez plus tard.",
|
||||||
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
|
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
|
||||||
"onboarding.follows.title": "Personnaliser votre flux principal",
|
"onboarding.follows.title": "Personnaliser votre flux principal",
|
||||||
|
"onboarding.profile.discoverable": "Rendre mon profil découvrable",
|
||||||
|
"onboarding.profile.discoverable_hint": "Lorsque vous acceptez d'être découvert sur Mastodon, vos messages peuvent apparaître dans les résultats de recherche et les tendances, et votre profil peut être suggéré à des personnes ayant des intérêts similaires aux vôtres.",
|
||||||
"onboarding.profile.display_name": "Nom affiché",
|
"onboarding.profile.display_name": "Nom affiché",
|
||||||
|
"onboarding.profile.display_name_hint": "Votre nom complet ou votre nom rigolo…",
|
||||||
|
"onboarding.profile.lead": "Vous pouvez toujours compléter cela plus tard dans les paramètres. Vous y trouverez encore plus d'options de personnalisation.",
|
||||||
"onboarding.profile.note": "Biographie",
|
"onboarding.profile.note": "Biographie",
|
||||||
|
"onboarding.profile.note_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…",
|
||||||
"onboarding.profile.save_and_continue": "Enregistrer et continuer",
|
"onboarding.profile.save_and_continue": "Enregistrer et continuer",
|
||||||
"onboarding.profile.title": "Configuration du profil",
|
"onboarding.profile.title": "Configuration du profil",
|
||||||
"onboarding.profile.upload_avatar": "Importer une photo de profil",
|
"onboarding.profile.upload_avatar": "Importer une photo de profil",
|
||||||
"onboarding.profile.upload_header": "Envoyer une image de profil",
|
"onboarding.profile.upload_header": "Importer un entête de profil",
|
||||||
"onboarding.share.lead": "Faites savoir aux gens comment ils peuvent vous trouver sur Mastodon!",
|
"onboarding.share.lead": "Faites savoir aux gens comment ils peuvent vous trouver sur Mastodon!",
|
||||||
"onboarding.share.message": "Je suis {username} sur #Mastodon ! Suivez-moi sur {url}",
|
"onboarding.share.message": "Je suis {username} sur #Mastodon ! Suivez-moi sur {url}",
|
||||||
"onboarding.share.next_steps": "Étapes suivantes possibles :",
|
"onboarding.share.next_steps": "Étapes suivantes possibles :",
|
||||||
|
@ -528,6 +535,7 @@
|
||||||
"privacy.unlisted.short": "Non listé",
|
"privacy.unlisted.short": "Non listé",
|
||||||
"privacy_policy.last_updated": "Dernière mise à jour {date}",
|
"privacy_policy.last_updated": "Dernière mise à jour {date}",
|
||||||
"privacy_policy.title": "Politique de confidentialité",
|
"privacy_policy.title": "Politique de confidentialité",
|
||||||
|
"recommended": "Recommandé",
|
||||||
"refresh": "Actualiser",
|
"refresh": "Actualiser",
|
||||||
"regeneration_indicator.label": "Chargement…",
|
"regeneration_indicator.label": "Chargement…",
|
||||||
"regeneration_indicator.sublabel": "Votre fil principal est en cours de préparation !",
|
"regeneration_indicator.sublabel": "Votre fil principal est en cours de préparation !",
|
||||||
|
@ -598,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Publications correspondant à {x}",
|
"search.quick_action.status_search": "Publications correspondant à {x}",
|
||||||
"search.search_or_paste": "Rechercher ou saisir une URL",
|
"search.search_or_paste": "Rechercher ou saisir une URL",
|
||||||
"search_popout.full_text_search_disabled_message": "Non disponible sur {domain}.",
|
"search_popout.full_text_search_disabled_message": "Non disponible sur {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Disponible uniquement lorsque vous êtes connecté.",
|
||||||
"search_popout.language_code": "code de langue ISO",
|
"search_popout.language_code": "code de langue ISO",
|
||||||
"search_popout.options": "Options de recherche",
|
"search_popout.options": "Options de recherche",
|
||||||
"search_popout.quick_actions": "Actions rapides",
|
"search_popout.quick_actions": "Actions rapides",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"account.blocked": "Blokkearre",
|
"account.blocked": "Blokkearre",
|
||||||
"account.browse_more_on_origin_server": "Mear op it orizjinele profyl besjen",
|
"account.browse_more_on_origin_server": "Mear op it orizjinele profyl besjen",
|
||||||
"account.cancel_follow_request": "Folchfersyk annulearje",
|
"account.cancel_follow_request": "Folchfersyk annulearje",
|
||||||
|
"account.copy": "Keppeling nei profyl kopiearje",
|
||||||
"account.direct": "Privee fermelde @{name}",
|
"account.direct": "Privee fermelde @{name}",
|
||||||
"account.disable_notifications": "Jou gjin melding mear wannear @{name} in berjocht pleatst",
|
"account.disable_notifications": "Jou gjin melding mear wannear @{name} in berjocht pleatst",
|
||||||
"account.domain_blocked": "Domein blokkearre",
|
"account.domain_blocked": "Domein blokkearre",
|
||||||
|
@ -191,6 +192,7 @@
|
||||||
"conversation.mark_as_read": "As lêzen markearje",
|
"conversation.mark_as_read": "As lêzen markearje",
|
||||||
"conversation.open": "Petear toane",
|
"conversation.open": "Petear toane",
|
||||||
"conversation.with": "Mei {names}",
|
"conversation.with": "Mei {names}",
|
||||||
|
"copy_icon_button.copied": "Nei klamboerd kopiearre",
|
||||||
"copypaste.copied": "Kopiearre",
|
"copypaste.copied": "Kopiearre",
|
||||||
"copypaste.copy_to_clipboard": "Nei klamboerd kopiearje",
|
"copypaste.copy_to_clipboard": "Nei klamboerd kopiearje",
|
||||||
"directory.federated": "Fediverse (wat bekend is)",
|
"directory.federated": "Fediverse (wat bekend is)",
|
||||||
|
@ -390,6 +392,7 @@
|
||||||
"lists.search": "Sykje nei minsken dy’t jo folgje",
|
"lists.search": "Sykje nei minsken dy’t jo folgje",
|
||||||
"lists.subheading": "Jo listen",
|
"lists.subheading": "Jo listen",
|
||||||
"load_pending": "{count, plural, one {# nij item} other {# nije items}}",
|
"load_pending": "{count, plural, one {# nij item} other {# nije items}}",
|
||||||
|
"loading_indicator.label": "Lade…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {ôfbylding ferstopje} other {ôfbyldingen ferstopje}}",
|
"media_gallery.toggle_visible": "{number, plural, one {ôfbylding ferstopje} other {ôfbyldingen ferstopje}}",
|
||||||
"moved_to_account_banner.text": "Omdat jo nei {movedToAccount} ferhuze binne is jo account {disabledAccount} op dit stuit útskeakele.",
|
"moved_to_account_banner.text": "Omdat jo nei {movedToAccount} ferhuze binne is jo account {disabledAccount} op dit stuit útskeakele.",
|
||||||
"mute_modal.duration": "Doer",
|
"mute_modal.duration": "Doer",
|
||||||
|
@ -478,6 +481,17 @@
|
||||||
"onboarding.follows.empty": "Spitigernôch kinne op dit stuit gjin resultaten toand wurde. Jo kinne probearje te sykjen of te blêdzjen troch de ferkenningsside om minsken te finen dy’t jo folgje kinne, of probearje it letter opnij.",
|
"onboarding.follows.empty": "Spitigernôch kinne op dit stuit gjin resultaten toand wurde. Jo kinne probearje te sykjen of te blêdzjen troch de ferkenningsside om minsken te finen dy’t jo folgje kinne, of probearje it letter opnij.",
|
||||||
"onboarding.follows.lead": "Jo beheare jo eigen startside. Hoe mear minsken jo folgje, hoe aktiver en ynteressanter it wêze sil. Dizze profilen kinne in goed startpunt wêze, jo kinne se letter altyd ûntfolgje!",
|
"onboarding.follows.lead": "Jo beheare jo eigen startside. Hoe mear minsken jo folgje, hoe aktiver en ynteressanter it wêze sil. Dizze profilen kinne in goed startpunt wêze, jo kinne se letter altyd ûntfolgje!",
|
||||||
"onboarding.follows.title": "Populêr op Mastodon",
|
"onboarding.follows.title": "Populêr op Mastodon",
|
||||||
|
"onboarding.profile.discoverable": "Meitsje myn profyl te finen",
|
||||||
|
"onboarding.profile.discoverable_hint": "Wannear’t jo akkoard gean mei it te finen wêzen op Mastodon, ferskine jo berjochten yn sykresultaten en kinne se trending wurde, en jo profyl kin oan oare minsken oanrekommandearre wurde wannear’t se fergelykbere ynteressen hawwe.",
|
||||||
|
"onboarding.profile.display_name": "Werjeftenamme",
|
||||||
|
"onboarding.profile.display_name_hint": "Jo folsleine namme of in aardige bynamme…",
|
||||||
|
"onboarding.profile.lead": "Jo kinne dit letter altyd oanfolje yn de ynstellingen, wêr’t noch mear oanpassingsopsjes beskikber binne.",
|
||||||
|
"onboarding.profile.note": "Biografy",
|
||||||
|
"onboarding.profile.note_hint": "Jo kinne oare minsken @fermelde of #hashtags brûke…",
|
||||||
|
"onboarding.profile.save_and_continue": "Bewarje en trochgean",
|
||||||
|
"onboarding.profile.title": "Profyl ynstelle",
|
||||||
|
"onboarding.profile.upload_avatar": "Profylfoto oplade",
|
||||||
|
"onboarding.profile.upload_header": "Omslachfoto foar profyl oplade",
|
||||||
"onboarding.share.lead": "Lit minsken witte hoe’t se jo fine kinne op Mastodon!",
|
"onboarding.share.lead": "Lit minsken witte hoe’t se jo fine kinne op Mastodon!",
|
||||||
"onboarding.share.message": "Ik bin {username} op #Mastodon! Folgje my op {url}",
|
"onboarding.share.message": "Ik bin {username} op #Mastodon! Folgje my op {url}",
|
||||||
"onboarding.share.next_steps": "Mooglike folgjende stappen:",
|
"onboarding.share.next_steps": "Mooglike folgjende stappen:",
|
||||||
|
@ -521,6 +535,7 @@
|
||||||
"privacy.unlisted.short": "Minder iepenbier",
|
"privacy.unlisted.short": "Minder iepenbier",
|
||||||
"privacy_policy.last_updated": "Lêst bywurke op {date}",
|
"privacy_policy.last_updated": "Lêst bywurke op {date}",
|
||||||
"privacy_policy.title": "Privacybelied",
|
"privacy_policy.title": "Privacybelied",
|
||||||
|
"recommended": "Oanrekommandearre",
|
||||||
"refresh": "Ferfarskje",
|
"refresh": "Ferfarskje",
|
||||||
"regeneration_indicator.label": "Lade…",
|
"regeneration_indicator.label": "Lade…",
|
||||||
"regeneration_indicator.sublabel": "Jo starttiidline wurdt oanmakke!",
|
"regeneration_indicator.sublabel": "Jo starttiidline wurdt oanmakke!",
|
||||||
|
@ -591,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Berjochten dy’t oerienkomme mei {x}",
|
"search.quick_action.status_search": "Berjochten dy’t oerienkomme mei {x}",
|
||||||
"search.search_or_paste": "Sykje of fier URL yn",
|
"search.search_or_paste": "Sykje of fier URL yn",
|
||||||
"search_popout.full_text_search_disabled_message": "Net beskikber op {domain}.",
|
"search_popout.full_text_search_disabled_message": "Net beskikber op {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Allinnich beskikber as jo oanmeld binne.",
|
||||||
"search_popout.language_code": "ISO-taalkoade",
|
"search_popout.language_code": "ISO-taalkoade",
|
||||||
"search_popout.options": "Sykopsjes",
|
"search_popout.options": "Sykopsjes",
|
||||||
"search_popout.quick_actions": "Flugge aksjes",
|
"search_popout.quick_actions": "Flugge aksjes",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Publicacións coincidentes {x}",
|
"search.quick_action.status_search": "Publicacións coincidentes {x}",
|
||||||
"search.search_or_paste": "Busca ou insire URL",
|
"search.search_or_paste": "Busca ou insire URL",
|
||||||
"search_popout.full_text_search_disabled_message": "Non está dispoñible en {domain}.",
|
"search_popout.full_text_search_disabled_message": "Non está dispoñible en {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Só dispoñible ao iniciar sesión.",
|
||||||
"search_popout.language_code": "Código ISO do idioma",
|
"search_popout.language_code": "Código ISO do idioma",
|
||||||
"search_popout.options": "Opcións de busca",
|
"search_popout.options": "Opcións de busca",
|
||||||
"search_popout.quick_actions": "Accións rápidas",
|
"search_popout.quick_actions": "Accións rápidas",
|
||||||
|
@ -686,7 +687,7 @@
|
||||||
"status.translated_from_with": "Traducido do {lang} usando {provider}",
|
"status.translated_from_with": "Traducido do {lang} usando {provider}",
|
||||||
"status.uncached_media_warning": "A vista previa non está dispoñíble",
|
"status.uncached_media_warning": "A vista previa non está dispoñíble",
|
||||||
"status.unmute_conversation": "Deixar de silenciar conversa",
|
"status.unmute_conversation": "Deixar de silenciar conversa",
|
||||||
"status.unpin": "Desafixar do perfil",
|
"status.unpin": "Non fixar no perfil",
|
||||||
"subscribed_languages.lead": "Ao facer cambios só as publicacións nos idiomas seleccionados aparecerán nas túas cronoloxías. Non elixas ningún para poder ver publicacións en tódolos idiomas.",
|
"subscribed_languages.lead": "Ao facer cambios só as publicacións nos idiomas seleccionados aparecerán nas túas cronoloxías. Non elixas ningún para poder ver publicacións en tódolos idiomas.",
|
||||||
"subscribed_languages.save": "Gardar cambios",
|
"subscribed_languages.save": "Gardar cambios",
|
||||||
"subscribed_languages.target": "Cambiar a subscrición a idiomas para {target}",
|
"subscribed_languages.target": "Cambiar a subscrición a idiomas para {target}",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "הודעות המכילות {x}",
|
"search.quick_action.status_search": "הודעות המכילות {x}",
|
||||||
"search.search_or_paste": "חפש או הזן קישור",
|
"search.search_or_paste": "חפש או הזן קישור",
|
||||||
"search_popout.full_text_search_disabled_message": "בלתי זמין על {domain}.",
|
"search_popout.full_text_search_disabled_message": "בלתי זמין על {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "זמין רק לאחר כניסה לאתר.",
|
||||||
"search_popout.language_code": "קוד ISO לשפה",
|
"search_popout.language_code": "קוד ISO לשפה",
|
||||||
"search_popout.options": "אפשרויות חיפוש",
|
"search_popout.options": "אפשרויות חיפוש",
|
||||||
"search_popout.quick_actions": "פעולות זריזות",
|
"search_popout.quick_actions": "פעולות זריזות",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Bejegyzések a következő keresésre: {x}",
|
"search.quick_action.status_search": "Bejegyzések a következő keresésre: {x}",
|
||||||
"search.search_or_paste": "Keresés vagy URL beillesztése",
|
"search.search_or_paste": "Keresés vagy URL beillesztése",
|
||||||
"search_popout.full_text_search_disabled_message": "Nem érhető el ezen: {domain}.",
|
"search_popout.full_text_search_disabled_message": "Nem érhető el ezen: {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Csak bejelentkezve érhető el.",
|
||||||
"search_popout.language_code": "ISO nyelvkód",
|
"search_popout.language_code": "ISO nyelvkód",
|
||||||
"search_popout.options": "Keresési beállítások",
|
"search_popout.options": "Keresési beállítások",
|
||||||
"search_popout.quick_actions": "Gyors műveletek",
|
"search_popout.quick_actions": "Gyors műveletek",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Færslur sem samsvara {x}",
|
"search.quick_action.status_search": "Færslur sem samsvara {x}",
|
||||||
"search.search_or_paste": "Leita eða líma slóð",
|
"search.search_or_paste": "Leita eða líma slóð",
|
||||||
"search_popout.full_text_search_disabled_message": "Ekki tiltækt á {domain}.",
|
"search_popout.full_text_search_disabled_message": "Ekki tiltækt á {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Aðeins tiltækt eftir innskráningu.",
|
||||||
"search_popout.language_code": "ISO-kóði tungumáls",
|
"search_popout.language_code": "ISO-kóði tungumáls",
|
||||||
"search_popout.options": "Leitarvalkostir",
|
"search_popout.options": "Leitarvalkostir",
|
||||||
"search_popout.quick_actions": "Flýtiaðgerðir",
|
"search_popout.quick_actions": "Flýtiaðgerðir",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Post corrispondenti a {x}",
|
"search.quick_action.status_search": "Post corrispondenti a {x}",
|
||||||
"search.search_or_paste": "Cerca o incolla URL",
|
"search.search_or_paste": "Cerca o incolla URL",
|
||||||
"search_popout.full_text_search_disabled_message": "Non disponibile in {domain}.",
|
"search_popout.full_text_search_disabled_message": "Non disponibile in {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Disponibile solo dopo aver effettuato l'accesso.",
|
||||||
"search_popout.language_code": "Codice ISO lingua",
|
"search_popout.language_code": "Codice ISO lingua",
|
||||||
"search_popout.options": "Opzioni di ricerca",
|
"search_popout.options": "Opzioni di ricerca",
|
||||||
"search_popout.quick_actions": "Azioni rapide",
|
"search_popout.quick_actions": "Azioni rapide",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "{x}에 맞는 게시물",
|
"search.quick_action.status_search": "{x}에 맞는 게시물",
|
||||||
"search.search_or_paste": "검색하거나 URL 붙여넣기",
|
"search.search_or_paste": "검색하거나 URL 붙여넣기",
|
||||||
"search_popout.full_text_search_disabled_message": "{domain}에서는 이용할 수 없습니다.",
|
"search_popout.full_text_search_disabled_message": "{domain}에서는 이용할 수 없습니다.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "로그인되어 있을 때만 할 수 있습니다.",
|
||||||
"search_popout.language_code": "ISO 언어코드",
|
"search_popout.language_code": "ISO 언어코드",
|
||||||
"search_popout.options": "검색 옵션",
|
"search_popout.options": "검색 옵션",
|
||||||
"search_popout.quick_actions": "빠른 작업",
|
"search_popout.quick_actions": "빠른 작업",
|
||||||
|
|
|
@ -445,6 +445,7 @@
|
||||||
"search.placeholder": "Paieška",
|
"search.placeholder": "Paieška",
|
||||||
"search.search_or_paste": "Ieškok arba įklijuok URL",
|
"search.search_or_paste": "Ieškok arba įklijuok URL",
|
||||||
"search_popout.full_text_search_disabled_message": "Nepasiekima {domain}.",
|
"search_popout.full_text_search_disabled_message": "Nepasiekima {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Pasiekiama tik prisijungus.",
|
||||||
"search_popout.language_code": "ISO kalbos kodas",
|
"search_popout.language_code": "ISO kalbos kodas",
|
||||||
"search_popout.specific_date": "konkreti data",
|
"search_popout.specific_date": "konkreti data",
|
||||||
"search_popout.user": "naudotojas",
|
"search_popout.user": "naudotojas",
|
||||||
|
|
|
@ -143,7 +143,7 @@
|
||||||
"compose_form.encryption_warning": "Berichten op Mastodon worden, net zoals op andere social media, niet end-to-end versleuteld. Deel daarom geen gevoelige informatie via Mastodon.",
|
"compose_form.encryption_warning": "Berichten op Mastodon worden, net zoals op andere social media, niet end-to-end versleuteld. Deel daarom geen gevoelige informatie via Mastodon.",
|
||||||
"compose_form.hashtag_warning": "Dit bericht valt niet onder een hashtag te bekijken, omdat deze niet openbaar is. Alleen openbare berichten kunnen via hashtags gevonden worden.",
|
"compose_form.hashtag_warning": "Dit bericht valt niet onder een hashtag te bekijken, omdat deze niet openbaar is. Alleen openbare berichten kunnen via hashtags gevonden worden.",
|
||||||
"compose_form.lock_disclaimer": "Jouw account is niet {locked}. Iedereen kan jou volgen en kan de berichten zien die je alleen aan jouw volgers hebt gericht.",
|
"compose_form.lock_disclaimer": "Jouw account is niet {locked}. Iedereen kan jou volgen en kan de berichten zien die je alleen aan jouw volgers hebt gericht.",
|
||||||
"compose_form.lock_disclaimer.lock": "besloten",
|
"compose_form.lock_disclaimer.lock": "vergrendeld",
|
||||||
"compose_form.placeholder": "Wat wil je kwijt?",
|
"compose_form.placeholder": "Wat wil je kwijt?",
|
||||||
"compose_form.poll.add_option": "Keuze toevoegen",
|
"compose_form.poll.add_option": "Keuze toevoegen",
|
||||||
"compose_form.poll.duration": "Duur van de peiling",
|
"compose_form.poll.duration": "Duur van de peiling",
|
||||||
|
@ -382,7 +382,7 @@
|
||||||
"lists.delete": "Lijst verwijderen",
|
"lists.delete": "Lijst verwijderen",
|
||||||
"lists.edit": "Lijst bewerken",
|
"lists.edit": "Lijst bewerken",
|
||||||
"lists.edit.submit": "Titel veranderen",
|
"lists.edit.submit": "Titel veranderen",
|
||||||
"lists.exclusive": "Verberg deze berichten op je starttijdlijn",
|
"lists.exclusive": "Verberg lijstleden op je starttijdlijn",
|
||||||
"lists.new.create": "Lijst toevoegen",
|
"lists.new.create": "Lijst toevoegen",
|
||||||
"lists.new.title_placeholder": "Naam nieuwe lijst",
|
"lists.new.title_placeholder": "Naam nieuwe lijst",
|
||||||
"lists.replies_policy.followed": "Elke gevolgde gebruiker",
|
"lists.replies_policy.followed": "Elke gevolgde gebruiker",
|
||||||
|
@ -395,7 +395,7 @@
|
||||||
"loading_indicator.label": "Laden…",
|
"loading_indicator.label": "Laden…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {afbeelding verbergen} other {afbeeldingen verbergen}}",
|
"media_gallery.toggle_visible": "{number, plural, one {afbeelding verbergen} other {afbeeldingen verbergen}}",
|
||||||
"moved_to_account_banner.text": "Omdat je naar {movedToAccount} bent verhuisd is jouw account {disabledAccount} momenteel uitgeschakeld.",
|
"moved_to_account_banner.text": "Omdat je naar {movedToAccount} bent verhuisd is jouw account {disabledAccount} momenteel uitgeschakeld.",
|
||||||
"mute_modal.duration": "Duur",
|
"mute_modal.duration": "Tijdsduur",
|
||||||
"mute_modal.hide_notifications": "Verberg meldingen van deze persoon?",
|
"mute_modal.hide_notifications": "Verberg meldingen van deze persoon?",
|
||||||
"mute_modal.indefinite": "Voor onbepaalde tijd",
|
"mute_modal.indefinite": "Voor onbepaalde tijd",
|
||||||
"navigation_bar.about": "Over",
|
"navigation_bar.about": "Over",
|
||||||
|
@ -481,8 +481,8 @@
|
||||||
"onboarding.follows.empty": "Helaas kunnen op dit moment geen resultaten worden getoond. Je kunt proberen te zoeken of op de verkenningspagina te bladeren om mensen te vinden die je kunt volgen, of probeer het later opnieuw.",
|
"onboarding.follows.empty": "Helaas kunnen op dit moment geen resultaten worden getoond. Je kunt proberen te zoeken of op de verkenningspagina te bladeren om mensen te vinden die je kunt volgen, of probeer het later opnieuw.",
|
||||||
"onboarding.follows.lead": "Jouw starttijdlijn is de belangrijkste manier om Mastodon te ervaren. Hoe meer mensen je volgt, hoe actiever en interessanter het zal zijn. Om te beginnen, zijn hier enkele suggesties:",
|
"onboarding.follows.lead": "Jouw starttijdlijn is de belangrijkste manier om Mastodon te ervaren. Hoe meer mensen je volgt, hoe actiever en interessanter het zal zijn. Om te beginnen, zijn hier enkele suggesties:",
|
||||||
"onboarding.follows.title": "Je starttijdlijn aan jouw wensen aanpassen",
|
"onboarding.follows.title": "Je starttijdlijn aan jouw wensen aanpassen",
|
||||||
"onboarding.profile.discoverable": "Maak mij profiel ontdekbaar",
|
"onboarding.profile.discoverable": "Maak mijn profiel vindbaar",
|
||||||
"onboarding.profile.discoverable_hint": "Wanneer je kiest voor Mastodon kun je berichten weergeven in zoekresultaten en trending, en je profiel kan worden voorgesteld aan mensen met vergelijkbare interesses.",
|
"onboarding.profile.discoverable_hint": "Wanneer je akkoord gaat met het vindbaar zijn op Mastodon, verschijnen je berichten in zoekresultaten en kunnen ze trending worden, en je profiel kan aan andere mensen worden aanbevolen wanneer ze vergelijkbare interesses hebben.",
|
||||||
"onboarding.profile.display_name": "Weergavenaam",
|
"onboarding.profile.display_name": "Weergavenaam",
|
||||||
"onboarding.profile.display_name_hint": "Jouw volledige naam of een leuke bijnaam…",
|
"onboarding.profile.display_name_hint": "Jouw volledige naam of een leuke bijnaam…",
|
||||||
"onboarding.profile.lead": "Je kunt dit later altijd aanvullen in de instellingen, waar nog meer aanpassingsopties beschikbaar zijn.",
|
"onboarding.profile.lead": "Je kunt dit later altijd aanvullen in de instellingen, waar nog meer aanpassingsopties beschikbaar zijn.",
|
||||||
|
@ -491,7 +491,7 @@
|
||||||
"onboarding.profile.save_and_continue": "Opslaan en doorgaan",
|
"onboarding.profile.save_and_continue": "Opslaan en doorgaan",
|
||||||
"onboarding.profile.title": "Profiel instellen",
|
"onboarding.profile.title": "Profiel instellen",
|
||||||
"onboarding.profile.upload_avatar": "Profielfoto uploaden",
|
"onboarding.profile.upload_avatar": "Profielfoto uploaden",
|
||||||
"onboarding.profile.upload_header": "Kop voor het profiel uploaden",
|
"onboarding.profile.upload_header": "Omslagfoto voor het profiel uploaden",
|
||||||
"onboarding.share.lead": "Laat mensen weten hoe ze je kunnen vinden op Mastodon!",
|
"onboarding.share.lead": "Laat mensen weten hoe ze je kunnen vinden op Mastodon!",
|
||||||
"onboarding.share.message": "Ik ben {username} op #Mastodon! Volg mij op {url}",
|
"onboarding.share.message": "Ik ben {username} op #Mastodon! Volg mij op {url}",
|
||||||
"onboarding.share.next_steps": "Mogelijke volgende stappen:",
|
"onboarding.share.next_steps": "Mogelijke volgende stappen:",
|
||||||
|
@ -504,7 +504,7 @@
|
||||||
"onboarding.steps.publish_status.body": "Zeg hallo tegen de wereld met tekst, foto's, video's of peilingen {emoji}",
|
"onboarding.steps.publish_status.body": "Zeg hallo tegen de wereld met tekst, foto's, video's of peilingen {emoji}",
|
||||||
"onboarding.steps.publish_status.title": "Maak je eerste bericht",
|
"onboarding.steps.publish_status.title": "Maak je eerste bericht",
|
||||||
"onboarding.steps.setup_profile.body": "Anderen zullen eerder met je in contact treden als je wat over jezelf vertelt.",
|
"onboarding.steps.setup_profile.body": "Anderen zullen eerder met je in contact treden als je wat over jezelf vertelt.",
|
||||||
"onboarding.steps.setup_profile.title": "Pas je profiel aan",
|
"onboarding.steps.setup_profile.title": "Je profiel aanpassen",
|
||||||
"onboarding.steps.share_profile.body": "Laat je vrienden weten waar je te vinden bent op Mastodon",
|
"onboarding.steps.share_profile.body": "Laat je vrienden weten waar je te vinden bent op Mastodon",
|
||||||
"onboarding.steps.share_profile.title": "Deel je Mastodonprofiel",
|
"onboarding.steps.share_profile.title": "Deel je Mastodonprofiel",
|
||||||
"onboarding.tips.2fa": "<strong>Wist je dat?</strong> Je kunt je account beveiligen door tweestapsverificatie in te stellen in je accountinstellingen. Het werkt met elke TOTP-app naar keuze, geen telefoonnummer nodig!",
|
"onboarding.tips.2fa": "<strong>Wist je dat?</strong> Je kunt je account beveiligen door tweestapsverificatie in te stellen in je accountinstellingen. Het werkt met elke TOTP-app naar keuze, geen telefoonnummer nodig!",
|
||||||
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Berichten die overeenkomen met {x}",
|
"search.quick_action.status_search": "Berichten die overeenkomen met {x}",
|
||||||
"search.search_or_paste": "Zoek of voer een URL in",
|
"search.search_or_paste": "Zoek of voer een URL in",
|
||||||
"search_popout.full_text_search_disabled_message": "Niet beschikbaar op {domain}.",
|
"search_popout.full_text_search_disabled_message": "Niet beschikbaar op {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Alleen beschikbaar als je bent ingelogd.",
|
||||||
"search_popout.language_code": "ISO-taalcode",
|
"search_popout.language_code": "ISO-taalcode",
|
||||||
"search_popout.options": "Zoekopties",
|
"search_popout.options": "Zoekopties",
|
||||||
"search_popout.quick_actions": "Snelle acties",
|
"search_popout.quick_actions": "Snelle acties",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Wpisy pasujące do {x}",
|
"search.quick_action.status_search": "Wpisy pasujące do {x}",
|
||||||
"search.search_or_paste": "Wyszukaj lub wklej adres",
|
"search.search_or_paste": "Wyszukaj lub wklej adres",
|
||||||
"search_popout.full_text_search_disabled_message": "Niedostępne na {domain}.",
|
"search_popout.full_text_search_disabled_message": "Niedostępne na {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Dostępne tylko po zalogowaniu.",
|
||||||
"search_popout.language_code": "Kod języka wg ISO",
|
"search_popout.language_code": "Kod języka wg ISO",
|
||||||
"search_popout.options": "Opcje wyszukiwania",
|
"search_popout.options": "Opcje wyszukiwania",
|
||||||
"search_popout.quick_actions": "Szybkie akcje",
|
"search_popout.quick_actions": "Szybkie akcje",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Publicações correspondentes a {x}",
|
"search.quick_action.status_search": "Publicações correspondentes a {x}",
|
||||||
"search.search_or_paste": "Buscar ou colar URL",
|
"search.search_or_paste": "Buscar ou colar URL",
|
||||||
"search_popout.full_text_search_disabled_message": "Não disponível em {domain}.",
|
"search_popout.full_text_search_disabled_message": "Não disponível em {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Disponível apenas quando conectado.",
|
||||||
"search_popout.language_code": "Código ISO do idioma",
|
"search_popout.language_code": "Código ISO do idioma",
|
||||||
"search_popout.options": "Opções de pesquisa",
|
"search_popout.options": "Opções de pesquisa",
|
||||||
"search_popout.quick_actions": "Ações rápidas",
|
"search_popout.quick_actions": "Ações rápidas",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Publicações com correspondência a {x}",
|
"search.quick_action.status_search": "Publicações com correspondência a {x}",
|
||||||
"search.search_or_paste": "Pesquisar ou introduzir URL",
|
"search.search_or_paste": "Pesquisar ou introduzir URL",
|
||||||
"search_popout.full_text_search_disabled_message": "Não disponível em {domain}.",
|
"search_popout.full_text_search_disabled_message": "Não disponível em {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Apenas disponível quando tem sessão iniciada.",
|
||||||
"search_popout.language_code": "Código ISO do idioma",
|
"search_popout.language_code": "Código ISO do idioma",
|
||||||
"search_popout.options": "Opções de pesquisa",
|
"search_popout.options": "Opções de pesquisa",
|
||||||
"search_popout.quick_actions": "Ações rápidas",
|
"search_popout.quick_actions": "Ações rápidas",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"account.blocked": "Заблокировано",
|
"account.blocked": "Заблокировано",
|
||||||
"account.browse_more_on_origin_server": "Посмотреть в оригинальном профиле",
|
"account.browse_more_on_origin_server": "Посмотреть в оригинальном профиле",
|
||||||
"account.cancel_follow_request": "Отозвать запрос на подписку",
|
"account.cancel_follow_request": "Отозвать запрос на подписку",
|
||||||
|
"account.copy": "Скопировать ссылку на профиль",
|
||||||
"account.direct": "Лично упоминать @{name}",
|
"account.direct": "Лично упоминать @{name}",
|
||||||
"account.disable_notifications": "Не уведомлять о постах от @{name}",
|
"account.disable_notifications": "Не уведомлять о постах от @{name}",
|
||||||
"account.domain_blocked": "Домен заблокирован",
|
"account.domain_blocked": "Домен заблокирован",
|
||||||
|
@ -191,6 +192,7 @@
|
||||||
"conversation.mark_as_read": "Отметить как прочитанное",
|
"conversation.mark_as_read": "Отметить как прочитанное",
|
||||||
"conversation.open": "Просмотр беседы",
|
"conversation.open": "Просмотр беседы",
|
||||||
"conversation.with": "С {names}",
|
"conversation.with": "С {names}",
|
||||||
|
"copy_icon_button.copied": "Скопировано в буфер обмена",
|
||||||
"copypaste.copied": "Скопировано",
|
"copypaste.copied": "Скопировано",
|
||||||
"copypaste.copy_to_clipboard": "Копировать в буфер обмена",
|
"copypaste.copy_to_clipboard": "Копировать в буфер обмена",
|
||||||
"directory.federated": "Со всей федерации",
|
"directory.federated": "Со всей федерации",
|
||||||
|
@ -222,6 +224,7 @@
|
||||||
"emoji_button.search_results": "Результаты поиска",
|
"emoji_button.search_results": "Результаты поиска",
|
||||||
"emoji_button.symbols": "Символы",
|
"emoji_button.symbols": "Символы",
|
||||||
"emoji_button.travel": "Путешествия и места",
|
"emoji_button.travel": "Путешествия и места",
|
||||||
|
"empty_column.account_hides_collections": "Данный пользователь решил не предоставлять эту информацию",
|
||||||
"empty_column.account_suspended": "Учетная запись заблокирована",
|
"empty_column.account_suspended": "Учетная запись заблокирована",
|
||||||
"empty_column.account_timeline": "Здесь нет постов!",
|
"empty_column.account_timeline": "Здесь нет постов!",
|
||||||
"empty_column.account_unavailable": "Профиль недоступен",
|
"empty_column.account_unavailable": "Профиль недоступен",
|
||||||
|
@ -389,6 +392,7 @@
|
||||||
"lists.search": "Искать среди подписок",
|
"lists.search": "Искать среди подписок",
|
||||||
"lists.subheading": "Ваши списки",
|
"lists.subheading": "Ваши списки",
|
||||||
"load_pending": "{count, plural, one {# новый элемент} few {# новых элемента} other {# новых элементов}}",
|
"load_pending": "{count, plural, one {# новый элемент} few {# новых элемента} other {# новых элементов}}",
|
||||||
|
"loading_indicator.label": "Загрузка…",
|
||||||
"media_gallery.toggle_visible": "Показать/скрыть {number, plural, =1 {изображение} other {изображения}}",
|
"media_gallery.toggle_visible": "Показать/скрыть {number, plural, =1 {изображение} other {изображения}}",
|
||||||
"moved_to_account_banner.text": "Ваша учетная запись {disabledAccount} в настоящее время заморожена, потому что вы переехали на {movedToAccount}.",
|
"moved_to_account_banner.text": "Ваша учетная запись {disabledAccount} в настоящее время заморожена, потому что вы переехали на {movedToAccount}.",
|
||||||
"mute_modal.duration": "Продолжительность",
|
"mute_modal.duration": "Продолжительность",
|
||||||
|
@ -477,6 +481,17 @@
|
||||||
"onboarding.follows.empty": "К сожалению, сейчас нет результатов. Вы можете попробовать использовать поиск или просмотреть страницу \"Исследования\", чтобы найти людей, за которыми можно следить, или повторить попытку позже.",
|
"onboarding.follows.empty": "К сожалению, сейчас нет результатов. Вы можете попробовать использовать поиск или просмотреть страницу \"Исследования\", чтобы найти людей, за которыми можно следить, или повторить попытку позже.",
|
||||||
"onboarding.follows.lead": "Вы сами формируете свою домашнюю ленту. Чем больше людей, за которыми вы следите, тем активнее и интереснее она будет. Эти профили могут быть хорошей отправной точкой - вы всегда можете от них отказаться!",
|
"onboarding.follows.lead": "Вы сами формируете свою домашнюю ленту. Чем больше людей, за которыми вы следите, тем активнее и интереснее она будет. Эти профили могут быть хорошей отправной точкой - вы всегда можете от них отказаться!",
|
||||||
"onboarding.follows.title": "Популярно на Mastodon",
|
"onboarding.follows.title": "Популярно на Mastodon",
|
||||||
|
"onboarding.profile.discoverable": "Сделать мой профиль открытым",
|
||||||
|
"onboarding.profile.discoverable_hint": "Если вы соглашаетесь на открытость на Mastodon, ваши сообщения могут появляться в результатах поиска и трендах, а ваш профиль может быть предложен людям со схожими с вами интересами.",
|
||||||
|
"onboarding.profile.display_name": "Отображаемое имя",
|
||||||
|
"onboarding.profile.display_name_hint": "Ваше полное имя или псевдоним…",
|
||||||
|
"onboarding.profile.lead": "Вы всегда можете завершить это позже в настройках, где доступны еще более широкие возможности настройки.",
|
||||||
|
"onboarding.profile.note": "О себе",
|
||||||
|
"onboarding.profile.note_hint": "Вы можете @упоминать других людей или использовать #хэштеги…",
|
||||||
|
"onboarding.profile.save_and_continue": "Сохранить и продолжить",
|
||||||
|
"onboarding.profile.title": "Настройка профиля",
|
||||||
|
"onboarding.profile.upload_avatar": "Загрузить фотографию профиля",
|
||||||
|
"onboarding.profile.upload_header": "Загрузить заголовок профиля",
|
||||||
"onboarding.share.lead": "Расскажите людям, как они могут найти вас на Mastodon!",
|
"onboarding.share.lead": "Расскажите людям, как они могут найти вас на Mastodon!",
|
||||||
"onboarding.share.message": "Я {username} на #Mastodon! Следуйте за мной по адресу {url}",
|
"onboarding.share.message": "Я {username} на #Mastodon! Следуйте за мной по адресу {url}",
|
||||||
"onboarding.share.next_steps": "Возможные дальнейшие шаги:",
|
"onboarding.share.next_steps": "Возможные дальнейшие шаги:",
|
||||||
|
@ -520,6 +535,7 @@
|
||||||
"privacy.unlisted.short": "Скрытый",
|
"privacy.unlisted.short": "Скрытый",
|
||||||
"privacy_policy.last_updated": "Последнее обновление {date}",
|
"privacy_policy.last_updated": "Последнее обновление {date}",
|
||||||
"privacy_policy.title": "Политика конфиденциальности",
|
"privacy_policy.title": "Политика конфиденциальности",
|
||||||
|
"recommended": "Рекомендуется",
|
||||||
"refresh": "Обновить",
|
"refresh": "Обновить",
|
||||||
"regeneration_indicator.label": "Загрузка…",
|
"regeneration_indicator.label": "Загрузка…",
|
||||||
"regeneration_indicator.sublabel": "Один момент, мы подготавливаем вашу ленту!",
|
"regeneration_indicator.sublabel": "Один момент, мы подготавливаем вашу ленту!",
|
||||||
|
@ -590,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Посты, соответствующие {x}",
|
"search.quick_action.status_search": "Посты, соответствующие {x}",
|
||||||
"search.search_or_paste": "Поиск (или вставьте URL)",
|
"search.search_or_paste": "Поиск (или вставьте URL)",
|
||||||
"search_popout.full_text_search_disabled_message": "Недоступно на {domain}.",
|
"search_popout.full_text_search_disabled_message": "Недоступно на {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Доступно только при авторизации.",
|
||||||
"search_popout.language_code": "Код языка по стандарту ISO",
|
"search_popout.language_code": "Код языка по стандарту ISO",
|
||||||
"search_popout.options": "Параметры поиска",
|
"search_popout.options": "Параметры поиска",
|
||||||
"search_popout.quick_actions": "Быстрые действия",
|
"search_popout.quick_actions": "Быстрые действия",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Objave, ki se ujemajo z {x}",
|
"search.quick_action.status_search": "Objave, ki se ujemajo z {x}",
|
||||||
"search.search_or_paste": "Iščite ali prilepite URL",
|
"search.search_or_paste": "Iščite ali prilepite URL",
|
||||||
"search_popout.full_text_search_disabled_message": "Ni dostopno na {domain}.",
|
"search_popout.full_text_search_disabled_message": "Ni dostopno na {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Na voljo le, če ste prijavljeni.",
|
||||||
"search_popout.language_code": "Koda ISO jezika",
|
"search_popout.language_code": "Koda ISO jezika",
|
||||||
"search_popout.options": "Možnosti iskanja",
|
"search_popout.options": "Možnosti iskanja",
|
||||||
"search_popout.quick_actions": "Hitra dejanja",
|
"search_popout.quick_actions": "Hitra dejanja",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Podudaranje objava {x}",
|
"search.quick_action.status_search": "Podudaranje objava {x}",
|
||||||
"search.search_or_paste": "Pretražite ili unesite adresu",
|
"search.search_or_paste": "Pretražite ili unesite adresu",
|
||||||
"search_popout.full_text_search_disabled_message": "Nije dostupno na {domain}.",
|
"search_popout.full_text_search_disabled_message": "Nije dostupno na {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Dostupno samo kada ste prijavljeni.",
|
||||||
"search_popout.language_code": "ISO kod jezika",
|
"search_popout.language_code": "ISO kod jezika",
|
||||||
"search_popout.options": "Opcije pretrage",
|
"search_popout.options": "Opcije pretrage",
|
||||||
"search_popout.quick_actions": "Brze radnje",
|
"search_popout.quick_actions": "Brze radnje",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Подударање објава {x}",
|
"search.quick_action.status_search": "Подударање објава {x}",
|
||||||
"search.search_or_paste": "Претражите или унесите адресу",
|
"search.search_or_paste": "Претражите или унесите адресу",
|
||||||
"search_popout.full_text_search_disabled_message": "Није доступно на {domain}.",
|
"search_popout.full_text_search_disabled_message": "Није доступно на {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Доступно само када сте пријављени.",
|
||||||
"search_popout.language_code": "ISO код језика",
|
"search_popout.language_code": "ISO код језика",
|
||||||
"search_popout.options": "Опције претраге",
|
"search_popout.options": "Опције претраге",
|
||||||
"search_popout.quick_actions": "Брзе радње",
|
"search_popout.quick_actions": "Брзе радње",
|
||||||
|
|
|
@ -604,6 +604,7 @@
|
||||||
"search.quick_action.status_search": "Inlägg som matchar {x}",
|
"search.quick_action.status_search": "Inlägg som matchar {x}",
|
||||||
"search.search_or_paste": "Sök eller klistra in URL",
|
"search.search_or_paste": "Sök eller klistra in URL",
|
||||||
"search_popout.full_text_search_disabled_message": "Inte tillgänglig på {domain}.",
|
"search_popout.full_text_search_disabled_message": "Inte tillgänglig på {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Endast tillgängligt när du är inloggad.",
|
||||||
"search_popout.language_code": "ISO språkkod",
|
"search_popout.language_code": "ISO språkkod",
|
||||||
"search_popout.options": "Sökalternativ",
|
"search_popout.options": "Sökalternativ",
|
||||||
"search_popout.quick_actions": "Snabbåtgärder",
|
"search_popout.quick_actions": "Snabbåtgärder",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "โพสต์ที่ตรงกับ {x}",
|
"search.quick_action.status_search": "โพสต์ที่ตรงกับ {x}",
|
||||||
"search.search_or_paste": "ค้นหาหรือวาง URL",
|
"search.search_or_paste": "ค้นหาหรือวาง URL",
|
||||||
"search_popout.full_text_search_disabled_message": "ไม่พร้อมใช้งานใน {domain}",
|
"search_popout.full_text_search_disabled_message": "ไม่พร้อมใช้งานใน {domain}",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "พร้อมใช้งานเฉพาะเมื่อเข้าสู่ระบบแล้วเท่านั้น",
|
||||||
"search_popout.language_code": "รหัสภาษา ISO",
|
"search_popout.language_code": "รหัสภาษา ISO",
|
||||||
"search_popout.options": "ตัวเลือกการค้นหา",
|
"search_popout.options": "ตัวเลือกการค้นหา",
|
||||||
"search_popout.quick_actions": "การกระทำด่วน",
|
"search_popout.quick_actions": "การกระทำด่วน",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Eşleşen gönderiler {x}",
|
"search.quick_action.status_search": "Eşleşen gönderiler {x}",
|
||||||
"search.search_or_paste": "Ara veya bağlantıyı yapıştır",
|
"search.search_or_paste": "Ara veya bağlantıyı yapıştır",
|
||||||
"search_popout.full_text_search_disabled_message": "{domain} sunucusunda mevcut değil.",
|
"search_popout.full_text_search_disabled_message": "{domain} sunucusunda mevcut değil.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Sadece oturum açıldığında mevcuttur.",
|
||||||
"search_popout.language_code": "ISO dil kodu",
|
"search_popout.language_code": "ISO dil kodu",
|
||||||
"search_popout.options": "Arama seçenekleri",
|
"search_popout.options": "Arama seçenekleri",
|
||||||
"search_popout.quick_actions": "Hızlı eylemler",
|
"search_popout.quick_actions": "Hızlı eylemler",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Збіг дописів {x}",
|
"search.quick_action.status_search": "Збіг дописів {x}",
|
||||||
"search.search_or_paste": "Введіть адресу або пошуковий запит",
|
"search.search_or_paste": "Введіть адресу або пошуковий запит",
|
||||||
"search_popout.full_text_search_disabled_message": "Недоступно на {domain}.",
|
"search_popout.full_text_search_disabled_message": "Недоступно на {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Доступно лише після входу.",
|
||||||
"search_popout.language_code": "Код мови ISO",
|
"search_popout.language_code": "Код мови ISO",
|
||||||
"search_popout.options": "Опції пошуку",
|
"search_popout.options": "Опції пошуку",
|
||||||
"search_popout.quick_actions": "Швидкі дії",
|
"search_popout.quick_actions": "Швидкі дії",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "Tút nhắc đến {x}",
|
"search.quick_action.status_search": "Tút nhắc đến {x}",
|
||||||
"search.search_or_paste": "Tìm kiếm hoặc nhập URL",
|
"search.search_or_paste": "Tìm kiếm hoặc nhập URL",
|
||||||
"search_popout.full_text_search_disabled_message": "Không khả dụng trên {domain}.",
|
"search_popout.full_text_search_disabled_message": "Không khả dụng trên {domain}.",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "Cần đăng nhập trước.",
|
||||||
"search_popout.language_code": "Mã ngôn ngữ ISO",
|
"search_popout.language_code": "Mã ngôn ngữ ISO",
|
||||||
"search_popout.options": "Tùy chọn tìm kiếm",
|
"search_popout.options": "Tùy chọn tìm kiếm",
|
||||||
"search_popout.quick_actions": "Thao tác nhanh",
|
"search_popout.quick_actions": "Thao tác nhanh",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "匹配 {x} 的嘟文",
|
"search.quick_action.status_search": "匹配 {x} 的嘟文",
|
||||||
"search.search_or_paste": "搜索或输入网址",
|
"search.search_or_paste": "搜索或输入网址",
|
||||||
"search_popout.full_text_search_disabled_message": "在 {domain} 不可用",
|
"search_popout.full_text_search_disabled_message": "在 {domain} 不可用",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "只有登录后才可用。",
|
||||||
"search_popout.language_code": "ISO语言代码",
|
"search_popout.language_code": "ISO语言代码",
|
||||||
"search_popout.options": "搜索选项",
|
"search_popout.options": "搜索选项",
|
||||||
"search_popout.quick_actions": "快捷操作",
|
"search_popout.quick_actions": "快捷操作",
|
||||||
|
|
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "符合的帖文 {x}",
|
"search.quick_action.status_search": "符合的帖文 {x}",
|
||||||
"search.search_or_paste": "搜尋或貼上網址",
|
"search.search_or_paste": "搜尋或貼上網址",
|
||||||
"search_popout.full_text_search_disabled_message": "在 {domain} 上無法使用。",
|
"search_popout.full_text_search_disabled_message": "在 {domain} 上無法使用。",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "登入後才可使用。",
|
||||||
"search_popout.language_code": "ISO 語言代碼",
|
"search_popout.language_code": "ISO 語言代碼",
|
||||||
"search_popout.options": "搜尋選項",
|
"search_popout.options": "搜尋選項",
|
||||||
"search_popout.quick_actions": "快速動作",
|
"search_popout.quick_actions": "快速動作",
|
||||||
|
|
|
@ -176,7 +176,7 @@
|
||||||
"confirmations.domain_block.confirm": "封鎖整個網域",
|
"confirmations.domain_block.confirm": "封鎖整個網域",
|
||||||
"confirmations.domain_block.message": "您真的非常確定要封鎖整個 {domain} 網域嗎?大部分情況下,封鎖或靜音少數特定的帳號就能滿足需求了。您將不能在任何公開的時間軸及通知中看到來自此網域的內容。您來自該網域的跟隨者也將被移除。",
|
"confirmations.domain_block.message": "您真的非常確定要封鎖整個 {domain} 網域嗎?大部分情況下,封鎖或靜音少數特定的帳號就能滿足需求了。您將不能在任何公開的時間軸及通知中看到來自此網域的內容。您來自該網域的跟隨者也將被移除。",
|
||||||
"confirmations.edit.confirm": "編輯",
|
"confirmations.edit.confirm": "編輯",
|
||||||
"confirmations.edit.message": "編輯嘟文將覆蓋掉您目前正在撰寫的訊息。是否仍要繼續?",
|
"confirmations.edit.message": "編輯嘟文將覆蓋掉您目前正在撰寫之嘟文內容。您是否仍要繼續?",
|
||||||
"confirmations.logout.confirm": "登出",
|
"confirmations.logout.confirm": "登出",
|
||||||
"confirmations.logout.message": "您確定要登出嗎?",
|
"confirmations.logout.message": "您確定要登出嗎?",
|
||||||
"confirmations.mute.confirm": "靜音",
|
"confirmations.mute.confirm": "靜音",
|
||||||
|
@ -606,6 +606,7 @@
|
||||||
"search.quick_action.status_search": "符合的嘟文 {x}",
|
"search.quick_action.status_search": "符合的嘟文 {x}",
|
||||||
"search.search_or_paste": "搜尋或輸入網址",
|
"search.search_or_paste": "搜尋或輸入網址",
|
||||||
"search_popout.full_text_search_disabled_message": "{domain} 上無法使用。",
|
"search_popout.full_text_search_disabled_message": "{domain} 上無法使用。",
|
||||||
|
"search_popout.full_text_search_logged_out_message": "僅於登入時能使用。",
|
||||||
"search_popout.language_code": "ISO 語言代碼 (ISO language code)",
|
"search_popout.language_code": "ISO 語言代碼 (ISO language code)",
|
||||||
"search_popout.options": "搜尋選項",
|
"search_popout.options": "搜尋選項",
|
||||||
"search_popout.quick_actions": "快捷操作",
|
"search_popout.quick_actions": "快捷操作",
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import ready from '../ready';
|
|
||||||
|
|
||||||
export let assetHost = '';
|
|
||||||
|
|
||||||
ready(() => {
|
|
||||||
const cdnHost = document.querySelector('meta[name=cdn-host]');
|
|
||||||
if (cdnHost) {
|
|
||||||
assetHost = cdnHost.content || '';
|
|
||||||
}
|
|
||||||
});
|
|
13
app/javascript/mastodon/utils/config.ts
Normal file
13
app/javascript/mastodon/utils/config.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import ready from '../ready';
|
||||||
|
|
||||||
|
export let assetHost = '';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
ready(() => {
|
||||||
|
const cdnHost = document.querySelector<HTMLMetaElement>(
|
||||||
|
'meta[name=cdn-host]',
|
||||||
|
);
|
||||||
|
if (cdnHost) {
|
||||||
|
assetHost = cdnHost.content || '';
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,6 +0,0 @@
|
||||||
// NB: This function can still return unsafe HTML
|
|
||||||
export const unescapeHTML = (html) => {
|
|
||||||
const wrapper = document.createElement('div');
|
|
||||||
wrapper.innerHTML = html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, '');
|
|
||||||
return wrapper.textContent;
|
|
||||||
};
|
|
9
app/javascript/mastodon/utils/html.ts
Normal file
9
app/javascript/mastodon/utils/html.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// NB: This function can still return unsafe HTML
|
||||||
|
export const unescapeHTML = (html: string) => {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = html
|
||||||
|
.replace(/<br\s*\/?>/g, '\n')
|
||||||
|
.replace(/<\/p><p>/g, '\n\n')
|
||||||
|
.replace(/<[^>]*>/g, '');
|
||||||
|
return wrapper.textContent;
|
||||||
|
};
|
|
@ -1,13 +1,23 @@
|
||||||
// Copied from emoji-mart for consistency with emoji picker and since
|
// Copied from emoji-mart for consistency with emoji picker and since
|
||||||
// they don't export the icons in the package
|
// they don't export the icons in the package
|
||||||
export const loupeIcon = (
|
export const loupeIcon = (
|
||||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 20 20'
|
||||||
|
width='13'
|
||||||
|
height='13'
|
||||||
|
>
|
||||||
<path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' />
|
<path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const deleteIcon = (
|
export const deleteIcon = (
|
||||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 20 20'
|
||||||
|
width='13'
|
||||||
|
height='13'
|
||||||
|
>
|
||||||
<path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' />
|
<path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
|
@ -1,30 +0,0 @@
|
||||||
// Handles browser quirks, based on
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
|
|
||||||
|
|
||||||
const checkNotificationPromise = () => {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line promise/valid-params, promise/catch-or-return
|
|
||||||
Notification.requestPermission().then();
|
|
||||||
} catch(e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePermission = (permission, callback) => {
|
|
||||||
// Whatever the user answers, we make sure Chrome stores the information
|
|
||||||
if(!('permission' in Notification)) {
|
|
||||||
Notification.permission = permission;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(Notification.permission);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const requestNotificationPermission = (callback) => {
|
|
||||||
if (checkNotificationPromise()) {
|
|
||||||
Notification.requestPermission().then((permission) => handlePermission(permission, callback)).catch(console.warn);
|
|
||||||
} else {
|
|
||||||
Notification.requestPermission((permission) => handlePermission(permission, callback));
|
|
||||||
}
|
|
||||||
};
|
|
13
app/javascript/mastodon/utils/notifications.ts
Normal file
13
app/javascript/mastodon/utils/notifications.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* Tries Notification.requestPermission, console warning instead of rejecting on error.
|
||||||
|
* @param callback Runs with the permission result on completion.
|
||||||
|
*/
|
||||||
|
export const requestNotificationPermission = async (
|
||||||
|
callback: NotificationPermissionCallback,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
callback(await Notification.requestPermission());
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error);
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,8 +1,8 @@
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { __RouterContext } from "react-router";
|
import { __RouterContext } from 'react-router';
|
||||||
|
|
||||||
import hoistStatics from "hoist-non-react-statics";
|
import hoistStatics from 'hoist-non-react-statics';
|
||||||
|
|
||||||
export const WithRouterPropTypes = {
|
export const WithRouterPropTypes = {
|
||||||
match: PropTypes.object.isRequired,
|
match: PropTypes.object.isRequired,
|
||||||
|
@ -16,31 +16,37 @@ export const WithOptionalRouterPropTypes = {
|
||||||
history: PropTypes.object,
|
history: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface OptionalRouterProps {
|
||||||
|
ref: unknown;
|
||||||
|
wrappedComponentRef: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
// This is copied from https://github.com/remix-run/react-router/blob/v5.3.4/packages/react-router/modules/withRouter.js
|
// This is copied from https://github.com/remix-run/react-router/blob/v5.3.4/packages/react-router/modules/withRouter.js
|
||||||
// but does not fail if called outside of a React Router context
|
// but does not fail if called outside of a React Router context
|
||||||
export function withOptionalRouter(Component) {
|
export function withOptionalRouter<
|
||||||
const displayName = `withRouter(${Component.displayName || Component.name})`;
|
ComponentType extends React.ComponentType<OptionalRouterProps>,
|
||||||
const C = props => {
|
>(Component: ComponentType) {
|
||||||
|
const displayName = `withRouter(${Component.displayName ?? Component.name})`;
|
||||||
|
const C = (props: React.ComponentProps<ComponentType>) => {
|
||||||
const { wrappedComponentRef, ...remainingProps } = props;
|
const { wrappedComponentRef, ...remainingProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<__RouterContext.Consumer>
|
<__RouterContext.Consumer>
|
||||||
{context => {
|
{(context) => {
|
||||||
if(context)
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (context) {
|
||||||
return (
|
return (
|
||||||
|
// @ts-expect-error - Dynamic covariant generic components are tough to type.
|
||||||
<Component
|
<Component
|
||||||
{...remainingProps}
|
{...remainingProps}
|
||||||
{...context}
|
{...context}
|
||||||
ref={wrappedComponentRef}
|
ref={wrappedComponentRef}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
else
|
} else {
|
||||||
return (
|
// @ts-expect-error - Dynamic covariant generic components are tough to type.
|
||||||
<Component
|
return <Component {...remainingProps} ref={wrappedComponentRef} />;
|
||||||
{...remainingProps}
|
}
|
||||||
ref={wrappedComponentRef}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
</__RouterContext.Consumer>
|
</__RouterContext.Consumer>
|
||||||
);
|
);
|
||||||
|
@ -53,8 +59,8 @@ export function withOptionalRouter(Component) {
|
||||||
wrappedComponentRef: PropTypes.oneOfType([
|
wrappedComponentRef: PropTypes.oneOfType([
|
||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
PropTypes.func,
|
PropTypes.func,
|
||||||
PropTypes.object
|
PropTypes.object,
|
||||||
])
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
return hoistStatics(C, Component);
|
return hoistStatics(C, Component);
|
|
@ -1,11 +1,7 @@
|
||||||
import { isMobile } from '../is_mobile';
|
import { isMobile } from '../is_mobile';
|
||||||
|
|
||||||
/** @type {number | null} */
|
let cachedScrollbarWidth: number | null = null;
|
||||||
let cachedScrollbarWidth = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
const getActualScrollbarWidth = () => {
|
const getActualScrollbarWidth = () => {
|
||||||
const outer = document.createElement('div');
|
const outer = document.createElement('div');
|
||||||
outer.style.visibility = 'hidden';
|
outer.style.visibility = 'hidden';
|
||||||
|
@ -16,20 +12,19 @@ const getActualScrollbarWidth = () => {
|
||||||
outer.appendChild(inner);
|
outer.appendChild(inner);
|
||||||
|
|
||||||
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
|
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
|
||||||
outer.parentNode.removeChild(outer);
|
outer.remove();
|
||||||
|
|
||||||
return scrollbarWidth;
|
return scrollbarWidth;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
export const getScrollbarWidth = () => {
|
export const getScrollbarWidth = () => {
|
||||||
if (cachedScrollbarWidth !== null) {
|
if (cachedScrollbarWidth !== null) {
|
||||||
return cachedScrollbarWidth;
|
return cachedScrollbarWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollbarWidth = isMobile(window.innerWidth) ? 0 : getActualScrollbarWidth();
|
const scrollbarWidth = isMobile(window.innerWidth)
|
||||||
|
? 0
|
||||||
|
: getActualScrollbarWidth();
|
||||||
cachedScrollbarWidth = scrollbarWidth;
|
cachedScrollbarWidth = scrollbarWidth;
|
||||||
|
|
||||||
return scrollbarWidth;
|
return scrollbarWidth;
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AdminMailer < ApplicationMailer
|
class AdminMailer < ApplicationMailer
|
||||||
layout 'plain_mailer'
|
layout 'admin_mailer'
|
||||||
|
|
||||||
helper :accounts
|
helper :accounts
|
||||||
helper :languages
|
helper :languages
|
||||||
|
|
|
@ -24,12 +24,12 @@ class Admin::ActionLog < ApplicationRecord
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :target, polymorphic: true, optional: true
|
belongs_to :target, polymorphic: true, optional: true
|
||||||
|
|
||||||
default_scope -> { order('id desc') }
|
|
||||||
|
|
||||||
before_validation :set_human_identifier
|
before_validation :set_human_identifier
|
||||||
before_validation :set_route_param
|
before_validation :set_route_param
|
||||||
before_validation :set_permalink
|
before_validation :set_permalink
|
||||||
|
|
||||||
|
scope :latest, -> { order(id: :desc) }
|
||||||
|
|
||||||
def action
|
def action
|
||||||
super.to_sym
|
super.to_sym
|
||||||
end
|
end
|
||||||
|
|
|
@ -72,7 +72,7 @@ class Admin::ActionLogFilter
|
||||||
end
|
end
|
||||||
|
|
||||||
def results
|
def results
|
||||||
scope = Admin::ActionLog.includes(:target)
|
scope = latest_action_logs.includes(:target)
|
||||||
|
|
||||||
params.each do |key, value|
|
params.each do |key, value|
|
||||||
next if key.to_s == 'page'
|
next if key.to_s == 'page'
|
||||||
|
@ -88,14 +88,18 @@ class Admin::ActionLogFilter
|
||||||
def scope_for(key, value)
|
def scope_for(key, value)
|
||||||
case key
|
case key
|
||||||
when 'action_type'
|
when 'action_type'
|
||||||
Admin::ActionLog.where(ACTION_TYPE_MAP[value.to_sym])
|
latest_action_logs.where(ACTION_TYPE_MAP[value.to_sym])
|
||||||
when 'account_id'
|
when 'account_id'
|
||||||
Admin::ActionLog.where(account_id: value)
|
latest_action_logs.where(account_id: value)
|
||||||
when 'target_account_id'
|
when 'target_account_id'
|
||||||
account = Account.find_or_initialize_by(id: value)
|
account = Account.find_or_initialize_by(id: value)
|
||||||
Admin::ActionLog.where(target: [account, account.user].compact)
|
latest_action_logs.where(target: [account, account.user].compact)
|
||||||
else
|
else
|
||||||
raise Mastodon::InvalidParameterError, "Unknown filter: #{key}"
|
raise Mastodon::InvalidParameterError, "Unknown filter: #{key}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def latest_action_logs
|
||||||
|
Admin::ActionLog.latest
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -131,25 +131,25 @@ class Report < ApplicationRecord
|
||||||
Admin::ActionLog.where(
|
Admin::ActionLog.where(
|
||||||
target_type: 'Report',
|
target_type: 'Report',
|
||||||
target_id: id
|
target_id: id
|
||||||
).unscope(:order).arel,
|
).arel,
|
||||||
|
|
||||||
Admin::ActionLog.where(
|
Admin::ActionLog.where(
|
||||||
target_type: 'Account',
|
target_type: 'Account',
|
||||||
target_id: target_account_id
|
target_id: target_account_id
|
||||||
).unscope(:order).arel,
|
).arel,
|
||||||
|
|
||||||
Admin::ActionLog.where(
|
Admin::ActionLog.where(
|
||||||
target_type: 'Status',
|
target_type: 'Status',
|
||||||
target_id: status_ids
|
target_id: status_ids
|
||||||
).unscope(:order).arel,
|
).arel,
|
||||||
|
|
||||||
Admin::ActionLog.where(
|
Admin::ActionLog.where(
|
||||||
target_type: 'AccountWarning',
|
target_type: 'AccountWarning',
|
||||||
target_id: AccountWarning.where(report_id: id).select(:id)
|
target_id: AccountWarning.where(report_id: id).select(:id)
|
||||||
).unscope(:order).arel,
|
).arel,
|
||||||
].reduce { |union, query| Arel::Nodes::UnionAll.new(union, query) }
|
].reduce { |union, query| Arel::Nodes::UnionAll.new(union, query) }
|
||||||
|
|
||||||
Admin::ActionLog.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table))
|
Admin::ActionLog.latest.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -49,7 +49,7 @@ class UserRole < ApplicationRecord
|
||||||
invite_users
|
invite_users
|
||||||
).freeze,
|
).freeze,
|
||||||
|
|
||||||
moderation: %w(
|
moderation: %i(
|
||||||
view_dashboard
|
view_dashboard
|
||||||
view_audit_log
|
view_audit_log
|
||||||
manage_users
|
manage_users
|
||||||
|
@ -63,7 +63,7 @@ class UserRole < ApplicationRecord
|
||||||
manage_invites
|
manage_invites
|
||||||
).freeze,
|
).freeze,
|
||||||
|
|
||||||
administration: %w(
|
administration: %i(
|
||||||
manage_settings
|
manage_settings
|
||||||
manage_rules
|
manage_rules
|
||||||
manage_roles
|
manage_roles
|
||||||
|
@ -72,7 +72,7 @@ class UserRole < ApplicationRecord
|
||||||
manage_announcements
|
manage_announcements
|
||||||
).freeze,
|
).freeze,
|
||||||
|
|
||||||
devops: %w(
|
devops: %i(
|
||||||
view_devops
|
view_devops
|
||||||
).freeze,
|
).freeze,
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,10 @@ class NodeInfo::Serializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def metadata
|
def metadata
|
||||||
{}
|
{
|
||||||
|
nodeName: Setting.site_title,
|
||||||
|
nodeDescription: Setting.site_short_description,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
class REST::ApplicationSerializer < ActiveModel::Serializer
|
class REST::ApplicationSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :name, :website, :scopes, :redirect_uri,
|
attributes :id, :name, :website, :scopes, :redirect_uri,
|
||||||
:client_id, :client_secret, :vapid_key
|
:client_id, :client_secret
|
||||||
|
|
||||||
|
# NOTE: Deprecated in 4.3.0, needs to be removed in 5.0.0
|
||||||
|
attribute :vapid_key
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
|
|
|
@ -48,6 +48,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
status: object.status_page_url,
|
status: object.status_page_url,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vapid: {
|
||||||
|
public_key: Rails.configuration.x.vapid_public_key,
|
||||||
|
},
|
||||||
|
|
||||||
accounts: {
|
accounts: {
|
||||||
max_featured_tags: FeaturedTag::LIMIT,
|
max_featured_tags: FeaturedTag::LIMIT,
|
||||||
},
|
},
|
||||||
|
|
|
@ -100,7 +100,9 @@ class ResolveAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def split_acct(acct)
|
def split_acct(acct)
|
||||||
acct.delete_prefix('acct:').split('@')
|
acct.delete_prefix('acct:').split('@').tap do |parts|
|
||||||
|
raise Webfinger::Error, 'Webfinger response is missing user or host value' unless parts.size == 2
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_account!
|
def fetch_account!
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
- if @account.user_prefers_noindex?
|
- if @account.user_prefers_noindex?
|
||||||
%meta{ name: 'robots', content: 'noindex, noarchive' }/
|
%meta{ name: 'robots', content: 'noindex, noarchive' }/
|
||||||
|
|
||||||
%link{ rel: 'alternate', type: 'application/rss+xml', href: @rss_url }/
|
%link{ rel: 'alternate', type: 'application/rss+xml', href: rss_url }/
|
||||||
%link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
|
%link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
|
||||||
|
|
||||||
- @account.fields.select(&:verifiable?).each do |field|
|
- @account.fields.select(&:verifiable?).each do |field|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
= f.input :report_id, as: :hidden
|
= f.input :report_id, as: :hidden
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :type, as: :radio_buttons, collection: Admin::AccountAction.types_for_account(@account), include_blank: false, wrapper: :with_block_label, label_method: ->(type) { safe_join([I18n.t("simple_form.labels.admin_account_action.types.#{type}"), content_tag(:span, I18n.t("simple_form.hints.admin_account_action.types.#{type}"), class: 'hint')]) }, hint: t('simple_form.hints.admin_account_action.type_html', acct: @account.pretty_acct)
|
= f.input :type, as: :radio_buttons, collection: Admin::AccountAction.types_for_account(@account), include_blank: false, wrapper: :with_block_label, label_method: ->(type) { account_action_type_label(type) }, hint: t('simple_form.hints.admin_account_action.type_html', acct: @account.pretty_acct)
|
||||||
|
|
||||||
- if @account.local?
|
- if @account.local?
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
.filter-subset.filter-subset--with-select
|
.filter-subset.filter-subset--with-select
|
||||||
%strong= t('admin.accounts.moderation.title')
|
%strong= t('admin.accounts.moderation.title')
|
||||||
.input.select.optional
|
.input.select.optional
|
||||||
= select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.disabled'), 'disabled'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all')
|
= select_tag :status, options_for_select(admin_accounts_moderation_options, params[:status]), prompt: I18n.t('generic.all')
|
||||||
.filter-subset.filter-subset--with-select
|
.filter-subset.filter-subset--with-select
|
||||||
%strong= t('admin.accounts.role')
|
%strong= t('admin.accounts.role')
|
||||||
.input.select.optional
|
.input.select.optional
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue