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
|
||||
|
||||
# Precompile assets for test
|
||||
RAILS_ENV=test NODE_ENV=tests ./bin/rails assets:precompile
|
||||
RAILS_ENV=test ./bin/rails assets:precompile
|
||||
|
|
20
.eslintrc.js
20
.eslintrc.js
|
@ -1,4 +1,7 @@
|
|||
module.exports = {
|
||||
// @ts-check
|
||||
const { defineConfig } = require('eslint-define-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
root: true,
|
||||
|
||||
extends: [
|
||||
|
@ -193,6 +196,7 @@ module.exports = {
|
|||
'error',
|
||||
{
|
||||
devDependencies: [
|
||||
'.eslintrc.js',
|
||||
'config/webpack/**',
|
||||
'app/javascript/mastodon/performance.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-invalid-icu': 'error',
|
||||
'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-offset': 'error',
|
||||
'formatjs/no-useless-message': 'error',
|
||||
|
@ -299,6 +302,7 @@ module.exports = {
|
|||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'.eslintrc.js',
|
||||
'*.config.js',
|
||||
'.*rc.js',
|
||||
'ide-helper.js',
|
||||
|
@ -372,14 +376,6 @@ module.exports = {
|
|||
env: {
|
||||
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
|
||||
|
||||
// Requires Webpacker upgrade or replacement
|
||||
'@svgr/webpack',
|
||||
'@types/webpack',
|
||||
'babel-loader',
|
||||
'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
|
||||
labels:
|
||||
type: string
|
||||
file_to_build:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
|
@ -86,6 +88,7 @@ jobs:
|
|||
- uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.file_to_build }}
|
||||
build-args: |
|
||||
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
|
||||
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
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
file_to_build: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
use_native_arm64_builder: true
|
||||
cache: false
|
||||
|
@ -41,3 +42,25 @@ jobs:
|
|||
type=raw,value=nightly
|
||||
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
|
||||
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
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
file_to_build: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
use_native_arm64_builder: true
|
||||
push_to_images: |
|
||||
|
@ -39,3 +40,19 @@ jobs:
|
|||
tags: |
|
||||
type=ref,event=pr
|
||||
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:
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
file_to_build: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
use_native_arm64_builder: true
|
||||
push_to_images: |
|
||||
|
@ -27,3 +28,24 @@ jobs:
|
|||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
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/test-image-build.yml
|
||||
- Dockerfile
|
||||
- streaming/Dockerfile
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
@ -18,4 +19,17 @@ jobs:
|
|||
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
file_to_build: Dockerfile
|
||||
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/paperclip.rb'
|
||||
- 'config/initializers/vapid.rb'
|
||||
- 'lib/mastodon/premailer_webpack_strategy.rb'
|
||||
- 'lib/premailer_webpack_strategy.rb'
|
||||
- 'lib/mastodon/redis_config.rb'
|
||||
- 'lib/tasks/repo.rake'
|
||||
- 'spec/features/profile_spec.rb'
|
||||
|
@ -357,8 +357,8 @@ Style/GuardClause:
|
|||
- 'config/initializers/devise.rb'
|
||||
- 'db/migrate/20170901141119_truncate_preview_cards.rb'
|
||||
- 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb'
|
||||
- 'lib/devise/two_factor_ldap_authenticatable.rb'
|
||||
- 'lib/devise/two_factor_pam_authenticatable.rb'
|
||||
- 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
|
||||
- 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
|
||||
- 'lib/mastodon/cli/accounts.rb'
|
||||
- 'lib/mastodon/cli/maintenance.rb'
|
||||
- 'lib/mastodon/cli/media.rb'
|
||||
|
@ -493,8 +493,8 @@ Style/SafeNavigation:
|
|||
# SupportedStyles: only_raise, only_fail, semantic
|
||||
Style/SignalException:
|
||||
Exclude:
|
||||
- 'lib/devise/two_factor_ldap_authenticatable.rb'
|
||||
- 'lib/devise/two_factor_pam_authenticatable.rb'
|
||||
- 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
|
||||
- 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Style/SingleArgumentDig:
|
||||
|
|
295
Dockerfile
295
Dockerfile
|
@ -1,112 +1,257 @@
|
|||
# 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
|
||||
FROM node:${NODE_VERSION} as build
|
||||
# Please see https://docs.docker.com/engine/reference/builder for information about
|
||||
# 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" \
|
||||
PATH="${PATH}:/opt/ruby/bin"
|
||||
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
||||
# 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
|
||||
|
||||
# hadolint ignore=DL3008,DL3005
|
||||
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 \
|
||||
# Apt update & upgrade to check for security updates to Debian image
|
||||
apt-get update; \
|
||||
apt-get dist-upgrade -yq; \
|
||||
# Install jemalloc, curl and other necessary components
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
ffmpeg \
|
||||
file \
|
||||
imagemagick \
|
||||
libjemalloc2 \
|
||||
patchelf \
|
||||
procps \
|
||||
tini \
|
||||
tzdata \
|
||||
; \
|
||||
# Patch Ruby to use jemalloc
|
||||
patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \
|
||||
# Discard patchelf after use
|
||||
apt-get purge -y \
|
||||
patchelf \
|
||||
;
|
||||
|
||||
# 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 apt-get update && \
|
||||
apt-get -yq dist-upgrade && \
|
||||
apt-get install -y --no-install-recommends build-essential \
|
||||
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 \
|
||||
libjemalloc-dev \
|
||||
zlib1g-dev \
|
||||
libgdbm-dev \
|
||||
libgmp-dev \
|
||||
libssl-dev \
|
||||
libyaml-dev \
|
||||
ca-certificates \
|
||||
libreadline8 \
|
||||
python3 \
|
||||
shared-mime-info && \
|
||||
bundle config set --local deployment 'true' && \
|
||||
bundle config set --local without 'development test' && \
|
||||
bundle config set silence_root_warning true && \
|
||||
corepack enable
|
||||
make \
|
||||
shared-mime-info \
|
||||
zlib1g-dev \
|
||||
;
|
||||
|
||||
COPY Gemfile* package.json yarn.lock .yarnrc.yml /opt/mastodon/
|
||||
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 .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 && \
|
||||
yarn cache clean
|
||||
# Create temporary assets build layer from build layer
|
||||
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
|
||||
ARG MASTODON_VERSION_PRERELEASE="bark"
|
||||
ARG MASTODON_VERSION_METADATA="dev"
|
||||
# Copy bundler and node packages from build layer to container
|
||||
COPY --from=yarn /opt/mastodon /opt/mastodon/
|
||||
COPY --from=bundler /opt/mastodon /opt/mastodon/
|
||||
COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
|
||||
|
||||
ARG UID="991"
|
||||
ARG GID="991"
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
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" \
|
||||
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin"
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# 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,DL3009
|
||||
RUN apt-get update && \
|
||||
echo "Etc/UTC" > /etc/localtime && \
|
||||
groupadd -g "${GID}" mastodon && \
|
||||
useradd -l -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \
|
||||
apt-get -y --no-install-recommends install whois \
|
||||
wget \
|
||||
procps \
|
||||
# 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 \
|
||||
# Mount Corepack and Yarn caches from Docker buildx caches
|
||||
--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 \
|
||||
# Apt update install non-dev versions of necessary components
|
||||
apt-get install -y --no-install-recommends \
|
||||
libssl3 \
|
||||
libpq5 \
|
||||
imagemagick \
|
||||
ffmpeg \
|
||||
libjemalloc2 \
|
||||
libicu72 \
|
||||
libidn12 \
|
||||
libyaml-0-2 \
|
||||
file \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
libreadline8 \
|
||||
tini && \
|
||||
ln -s /opt/mastodon /mastodon && \
|
||||
corepack enable
|
||||
libyaml-0-2 \
|
||||
;
|
||||
|
||||
# Note: no, cleaning here since Debian does this automatically
|
||||
# See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem
|
||||
# Copy Mastodon sources into final layer
|
||||
COPY . /opt/mastodon/
|
||||
|
||||
COPY --chown=mastodon:mastodon . /opt/mastodon
|
||||
COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon
|
||||
# Copy compiled assets to layer
|
||||
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" \
|
||||
NODE_ENV="production" \
|
||||
RAILS_SERVE_STATIC_FILES="true" \
|
||||
BIND="0.0.0.0" \
|
||||
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
|
||||
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}"
|
||||
RUN \
|
||||
# Precompile bootsnap code for faster Rails startup
|
||||
bundle exec bootsnap precompile --gemfile app/ lib/;
|
||||
|
||||
# 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
|
||||
WORKDIR /opt/mastodon
|
||||
|
||||
# Precompile assets
|
||||
RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile
|
||||
|
||||
# Set the work dir and the container entry point
|
||||
# Expose default Puma ports
|
||||
EXPOSE 3000
|
||||
# Set container tini as default 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)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.855.0)
|
||||
aws-partitions (1.857.0)
|
||||
aws-sdk-core (3.188.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
|
@ -140,7 +140,7 @@ GEM
|
|||
aws-sdk-kms (1.73.0)
|
||||
aws-sdk-core (~> 3, >= 3.188.0)
|
||||
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-kms (~> 1)
|
||||
aws-sigv4 (~> 1.6)
|
||||
|
@ -156,7 +156,7 @@ GEM
|
|||
nokogiri (~> 1, >= 1.10.8)
|
||||
base64 (0.2.0)
|
||||
bcp47_spec (0.2.1)
|
||||
bcrypt (3.1.19)
|
||||
bcrypt (3.1.20)
|
||||
better_errors (2.10.1)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
|
@ -272,7 +272,7 @@ GEM
|
|||
et-orbi (1.2.7)
|
||||
tzinfo
|
||||
excon (0.104.0)
|
||||
fabrication (2.30.0)
|
||||
fabrication (2.31.0)
|
||||
faker (3.2.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.3)
|
||||
|
@ -522,7 +522,7 @@ GEM
|
|||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.5.4)
|
||||
pghero (3.3.4)
|
||||
pghero (3.4.0)
|
||||
activerecord (>= 6)
|
||||
posix-spawn (0.3.15)
|
||||
premailer (1.21.0)
|
||||
|
@ -755,7 +755,7 @@ GEM
|
|||
attr_required (>= 0.0.5)
|
||||
httpclient (>= 2.4)
|
||||
sysexits (1.2.0)
|
||||
temple (0.10.2)
|
||||
temple (0.10.3)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
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'
|
||||
|
||||
# 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
|
||||
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AccountsIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s'), analysis: {
|
||||
filter: {
|
||||
english_stop: {
|
||||
|
@ -60,7 +62,7 @@ class AccountsIndex < Chewy::Index
|
|||
field(:following_count, type: 'long')
|
||||
field(:followers_count, type: 'long')
|
||||
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(: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' }
|
||||
|
|
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
|
||||
|
||||
class PublicStatusesIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
||||
filter: {
|
||||
english_stop: {
|
||||
|
@ -62,6 +64,6 @@ class PublicStatusesIndex < Chewy::Index
|
|||
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
|
||||
field(:language, type: 'keyword')
|
||||
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
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StatusesIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
||||
filter: {
|
||||
english_stop: {
|
||||
|
@ -60,6 +62,6 @@ class StatusesIndex < Chewy::Index
|
|||
field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by })
|
||||
field(:language, type: 'keyword')
|
||||
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
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TagsIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s'), analysis: {
|
||||
analyzer: {
|
||||
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(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? })
|
||||
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
|
||||
|
|
|
@ -18,8 +18,6 @@ class AccountsController < ApplicationController
|
|||
respond_to do |format|
|
||||
format.html do
|
||||
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
|
||||
|
||||
format.rss do
|
||||
|
@ -84,29 +82,21 @@ class AccountsController < ApplicationController
|
|||
short_account_url(@account, format: 'rss')
|
||||
end
|
||||
end
|
||||
helper_method :rss_url
|
||||
|
||||
def media_requested?
|
||||
request.path.split('.').first.end_with?('/media') && !tag_requested?
|
||||
path_without_format.end_with?('/media') && !tag_requested?
|
||||
end
|
||||
|
||||
def replies_requested?
|
||||
request.path.split('.').first.end_with?('/with_replies') && !tag_requested?
|
||||
path_without_format.end_with?('/with_replies') && !tag_requested?
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def cached_filtered_status_page
|
||||
cache_collection_paginated_by_id(
|
||||
filtered_statuses,
|
||||
Status,
|
||||
PAGE_SIZE,
|
||||
params_slice(:max_id, :min_id, :since_id)
|
||||
)
|
||||
end
|
||||
|
||||
def params_slice(*keys)
|
||||
params.slice(*keys).permit(*keys)
|
||||
def path_without_format
|
||||
request.path.split('.').first
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ module Admin
|
|||
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||
@warnings = @account.strikes.custom.latest
|
||||
|
||||
render template: 'admin/accounts/show'
|
||||
render 'admin/accounts/show'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ module Admin
|
|||
|
||||
def 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
|
||||
|
||||
private
|
||||
|
|
|
@ -24,7 +24,7 @@ module Admin
|
|||
@relay.enable!
|
||||
redirect_to admin_relays_path
|
||||
else
|
||||
render action: :new
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ module Admin
|
|||
@form = Admin::StatusBatchAction.new
|
||||
@statuses = @report.statuses.with_includes
|
||||
|
||||
render template: 'admin/reports/show'
|
||||
render 'admin/reports/show'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ module ChallengableConcern
|
|||
|
||||
def render_challenge
|
||||
@body_classes = 'lighter'
|
||||
render template: 'auth/challenges/new', layout: 'auth'
|
||||
render 'auth/challenges/new', layout: 'auth'
|
||||
end
|
||||
|
||||
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')
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@appeal = e.record
|
||||
render template: 'disputes/strikes/show'
|
||||
render 'disputes/strikes/show'
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -25,7 +25,7 @@ class FiltersController < ApplicationController
|
|||
if @filter.save
|
||||
redirect_to filters_path
|
||||
else
|
||||
render action: :new
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -33,7 +33,7 @@ class FiltersController < ApplicationController
|
|||
if @filter.update(resource_params)
|
||||
redirect_to filters_path
|
||||
else
|
||||
render action: :edit
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ class StatusesCleanupController < ApplicationController
|
|||
if @policy.update(resource_params)
|
||||
redirect_to statuses_cleanup_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
render action: :show
|
||||
render :show
|
||||
end
|
||||
rescue ActionController::ParameterMissing
|
||||
# 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 api from 'mastodon/api';
|
||||
import Hashtag from 'mastodon/components/hashtag';
|
||||
import { Hashtag } from 'mastodon/components/hashtag';
|
||||
|
||||
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 ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import Hashtag from 'mastodon/components/hashtag';
|
||||
import { Hashtag } from 'mastodon/components/hashtag';
|
||||
|
||||
const messages = defineMessages({
|
||||
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 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 Column from 'mastodon/features/ui/components/column';
|
||||
|
||||
|
|
|
@ -250,6 +250,9 @@
|
|||
"notifications.column_settings.unread_notifications.highlight": "Lig ongelese kennisgewings uit",
|
||||
"notifications.filter.boosts": "Aangestuurde plasings",
|
||||
"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_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!",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Супадзенне паведамленняў {x}",
|
||||
"search.search_or_paste": "Пошук",
|
||||
"search_popout.full_text_search_disabled_message": "Недаступна на {domain}.",
|
||||
"search_popout.full_text_search_logged_out_message": "Даступна толькі пры ўваходзе ў сістэму.",
|
||||
"search_popout.language_code": "ISO код мовы",
|
||||
"search_popout.options": "Параметры пошуку",
|
||||
"search_popout.quick_actions": "Хуткія дзеянні",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Съвпадение на публикации {x}",
|
||||
"search.search_or_paste": "Търсене или поставяне на URL адрес",
|
||||
"search_popout.full_text_search_disabled_message": "Не е достъпно на {domain}.",
|
||||
"search_popout.full_text_search_logged_out_message": "Достъпно само при влизане в системата.",
|
||||
"search_popout.language_code": "Код на езика по ISO",
|
||||
"search_popout.options": "Възможности при търсене",
|
||||
"search_popout.quick_actions": "Бързи действия",
|
||||
|
|
|
@ -605,6 +605,7 @@
|
|||
"search.quick_action.status_search": "Tuts coincidint amb {x}",
|
||||
"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_logged_out_message": "Només disponible en iniciar la sessió.",
|
||||
"search_popout.language_code": "Codi de llengua ISO",
|
||||
"search_popout.options": "Opcions de cerca",
|
||||
"search_popout.quick_actions": "Accions ràpides",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Indlæg matchende {x}",
|
||||
"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_logged_out_message": "Kun tilgængelig, når logget ind.",
|
||||
"search_popout.language_code": "ISO-sprogkode",
|
||||
"search_popout.options": "Søgevalg",
|
||||
"search_popout.quick_actions": "Hurtige handlinger",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Beiträge passend zu {x}",
|
||||
"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_logged_out_message": "Nur verfügbar, wenn angemeldet.",
|
||||
"search_popout.language_code": "ISO-Sprachcode",
|
||||
"search_popout.options": "Suchoptionen",
|
||||
"search_popout.quick_actions": "Schnellaktionen",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Mensajes que coinciden con {x}",
|
||||
"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_logged_out_message": "Solo disponible al iniciar sesión.",
|
||||
"search_popout.language_code": "Código ISO de idioma",
|
||||
"search_popout.options": "Opciones de búsqueda",
|
||||
"search_popout.quick_actions": "Acciones rápidas",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Publicaciones que coinciden con {x}",
|
||||
"search.search_or_paste": "Buscar o pegar URL",
|
||||
"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.options": "Opciones de búsqueda",
|
||||
"search_popout.quick_actions": "Acciones rápidas",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Publicaciones que coinciden con {x}",
|
||||
"search.search_or_paste": "Buscar o pegar URL",
|
||||
"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.options": "Opciones de búsqueda",
|
||||
"search_popout.quick_actions": "Acciones rápidas",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"account.blocked": "Blokeeritud",
|
||||
"account.browse_more_on_origin_server": "Vaata rohkem algsel profiilil",
|
||||
"account.cancel_follow_request": "Võta jälgimistaotlus tagasi",
|
||||
"account.copy": "Kopeeri link profiili",
|
||||
"account.direct": "Maini privaatselt @{name}",
|
||||
"account.disable_notifications": "Peata teavitused @{name} postitustest",
|
||||
"account.domain_blocked": "Domeen peidetud",
|
||||
|
@ -191,6 +192,7 @@
|
|||
"conversation.mark_as_read": "Märgi loetuks",
|
||||
"conversation.open": "Vaata vestlust",
|
||||
"conversation.with": "Koos {names}",
|
||||
"copy_icon_button.copied": "Kopeeritud vahemällu",
|
||||
"copypaste.copied": "Kopeeritud",
|
||||
"copypaste.copy_to_clipboard": "Kopeeri vahemällu",
|
||||
"directory.federated": "Tuntud födiversumist",
|
||||
|
@ -390,6 +392,7 @@
|
|||
"lists.search": "Otsi enda jälgitavate inimeste hulgast",
|
||||
"lists.subheading": "Sinu nimekirjad",
|
||||
"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}}",
|
||||
"moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
|
||||
"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.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.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.message": "Ma olen #Mastodon võrgustikus {username}! tule ja jälgi mind aadressil {url}",
|
||||
"onboarding.share.next_steps": "Võimalikud järgmised sammud:",
|
||||
|
@ -521,6 +535,7 @@
|
|||
"privacy.unlisted.short": "Määramata",
|
||||
"privacy_policy.last_updated": "Viimati uuendatud {date}",
|
||||
"privacy_policy.title": "Isikuandmete kaitse",
|
||||
"recommended": "Soovitatud",
|
||||
"refresh": "Värskenda",
|
||||
"regeneration_indicator.label": "Laeb…",
|
||||
"regeneration_indicator.sublabel": "Su koduvoog on ettevalmistamisel!",
|
||||
|
|
|
@ -16,17 +16,17 @@
|
|||
"account.badges.bot": "Bot-a",
|
||||
"account.badges.group": "Taldea",
|
||||
"account.block": "Blokeatu @{name}",
|
||||
"account.block_domain": "Ezkutatu {domain} domeinuko guztia",
|
||||
"account.block_domain": "Blokeatu {domain} domeinua",
|
||||
"account.block_short": "Blokeatu",
|
||||
"account.blocked": "Blokeatuta",
|
||||
"account.browse_more_on_origin_server": "Arakatu gehiago jatorrizko profilean",
|
||||
"account.cancel_follow_request": "Baztertu jarraitzeko eskaera",
|
||||
"account.copy": "Kopiatu profilerako esteka",
|
||||
"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.edit_profile": "Aldatu profila",
|
||||
"account.enable_notifications": "Jakinarazi @{name} erabiltzaileak bidalketak egitean",
|
||||
"account.enable_notifications": "Jakinarazi @{name} erabiltzaileak argitaratzean",
|
||||
"account.endorse": "Nabarmendu profilean",
|
||||
"account.featured_tags.last_status_at": "Azken bidalketa {date} datan",
|
||||
"account.featured_tags.last_status_never": "Bidalketarik ez",
|
||||
|
@ -40,7 +40,7 @@
|
|||
"account.follows.empty": "Erabiltzaile honek ez du inor jarraitzen oraindik.",
|
||||
"account.follows_you": "Jarraitzen dizu",
|
||||
"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.joined_short": "Elkartuta",
|
||||
"account.languages": "Aldatu harpidetutako hizkuntzak",
|
||||
|
@ -60,8 +60,8 @@
|
|||
"account.report": "Salatu @{name}",
|
||||
"account.requested": "Onarpenaren zain. Egin klik jarraipen-eskaera ezeztatzeko",
|
||||
"account.requested_follow": "{name}-(e)k zu jarraitzeko eskaera egin du",
|
||||
"account.share": "@{name}(e)ren profila elkarbanatu",
|
||||
"account.show_reblogs": "Erakutsi @{name}(r)en bultzadak",
|
||||
"account.share": "Partekatu @{name} erabiltzailearen profila",
|
||||
"account.show_reblogs": "Erakutsi @{name} erabiltzailearen bultzadak",
|
||||
"account.statuses_counter": "{count, plural, one {Bidalketa {counter}} other {{counter} bidalketa}}",
|
||||
"account.unblock": "Desblokeatu @{name}",
|
||||
"account.unblock_domain": "Berriz erakutsi {domain}",
|
||||
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "{x}-(r)ekin bat datozen argitalpenak",
|
||||
"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_logged_out_message": "Soilik erabilgarri saioa hastean.",
|
||||
"search_popout.language_code": "ISO hizkuntza-kodea",
|
||||
"search_popout.options": "Bilaketaren aukerak",
|
||||
"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.title": "Mukauta kotisyötettäsi",
|
||||
"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_hint": "Koko nimesi tai lempinimesi…",
|
||||
"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.search_or_paste": "Hae tai liitä URL-osoite",
|
||||
"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.options": "Hakuvalinnat",
|
||||
"search_popout.quick_actions": "Pikatoiminnot",
|
||||
|
|
|
@ -596,6 +596,7 @@
|
|||
"search.quick_action.status_search": "Postar, ið samsvara {x}",
|
||||
"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_logged_out_message": "Einans tøkt um innritað er.",
|
||||
"search_popout.language_code": "ISO málkoda",
|
||||
"search_popout.options": "Leitimøguleikar",
|
||||
"search_popout.quick_actions": "Skjótar atgerðir",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"account.blocked": "Bloqué·e",
|
||||
"account.browse_more_on_origin_server": "Parcourir davantage sur le profil original",
|
||||
"account.cancel_follow_request": "Retirer cette demande d'abonnement",
|
||||
"account.copy": "Copier le lien vers le profil",
|
||||
"account.direct": "Mention privée @{name}",
|
||||
"account.disable_notifications": "Ne plus me notifier quand @{name} publie",
|
||||
"account.domain_blocked": "Domaine bloqué",
|
||||
|
@ -191,6 +192,7 @@
|
|||
"conversation.mark_as_read": "Marquer comme lu",
|
||||
"conversation.open": "Afficher cette conversation",
|
||||
"conversation.with": "Avec {names}",
|
||||
"copy_icon_button.copied": "Copié dans le presse-papier",
|
||||
"copypaste.copied": "Copié",
|
||||
"copypaste.copy_to_clipboard": "Copier dans le presse-papiers",
|
||||
"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.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.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_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_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…",
|
||||
"onboarding.profile.save_and_continue": "Enregistrer et continuer",
|
||||
"onboarding.profile.title": "Configuration du 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.message": "Je suis {username} sur #Mastodon! Suivez-moi sur {url}",
|
||||
"onboarding.share.next_steps": "Étapes suivantes possibles:",
|
||||
|
@ -528,6 +535,7 @@
|
|||
"privacy.unlisted.short": "Non listé",
|
||||
"privacy_policy.last_updated": "Dernière mise à jour {date}",
|
||||
"privacy_policy.title": "Politique de confidentialité",
|
||||
"recommended": "Recommandé",
|
||||
"refresh": "Actualiser",
|
||||
"regeneration_indicator.label": "Chargement…",
|
||||
"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.search_or_paste": "Rechercher ou saisir un URL",
|
||||
"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.options": "Options de recherche",
|
||||
"search_popout.quick_actions": "Actions rapides",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"account.blocked": "Bloqué·e",
|
||||
"account.browse_more_on_origin_server": "Parcourir davantage sur le profil original",
|
||||
"account.cancel_follow_request": "Annuler le suivi",
|
||||
"account.copy": "Copier le lien vers le profil",
|
||||
"account.direct": "Mention privée @{name}",
|
||||
"account.disable_notifications": "Ne plus me notifier quand @{name} publie quelque chose",
|
||||
"account.domain_blocked": "Domaine bloqué",
|
||||
|
@ -191,6 +192,7 @@
|
|||
"conversation.mark_as_read": "Marquer comme lu",
|
||||
"conversation.open": "Afficher la conversation",
|
||||
"conversation.with": "Avec {names}",
|
||||
"copy_icon_button.copied": "Copié dans le presse-papier",
|
||||
"copypaste.copied": "Copié",
|
||||
"copypaste.copy_to_clipboard": "Copier dans le presse-papiers",
|
||||
"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.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.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_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_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…",
|
||||
"onboarding.profile.save_and_continue": "Enregistrer et continuer",
|
||||
"onboarding.profile.title": "Configuration du 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.message": "Je suis {username} sur #Mastodon ! Suivez-moi sur {url}",
|
||||
"onboarding.share.next_steps": "Étapes suivantes possibles :",
|
||||
|
@ -528,6 +535,7 @@
|
|||
"privacy.unlisted.short": "Non listé",
|
||||
"privacy_policy.last_updated": "Dernière mise à jour {date}",
|
||||
"privacy_policy.title": "Politique de confidentialité",
|
||||
"recommended": "Recommandé",
|
||||
"refresh": "Actualiser",
|
||||
"regeneration_indicator.label": "Chargement…",
|
||||
"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.search_or_paste": "Rechercher ou saisir une URL",
|
||||
"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.options": "Options de recherche",
|
||||
"search_popout.quick_actions": "Actions rapides",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"account.blocked": "Blokkearre",
|
||||
"account.browse_more_on_origin_server": "Mear op it orizjinele profyl besjen",
|
||||
"account.cancel_follow_request": "Folchfersyk annulearje",
|
||||
"account.copy": "Keppeling nei profyl kopiearje",
|
||||
"account.direct": "Privee fermelde @{name}",
|
||||
"account.disable_notifications": "Jou gjin melding mear wannear @{name} in berjocht pleatst",
|
||||
"account.domain_blocked": "Domein blokkearre",
|
||||
|
@ -191,6 +192,7 @@
|
|||
"conversation.mark_as_read": "As lêzen markearje",
|
||||
"conversation.open": "Petear toane",
|
||||
"conversation.with": "Mei {names}",
|
||||
"copy_icon_button.copied": "Nei klamboerd kopiearre",
|
||||
"copypaste.copied": "Kopiearre",
|
||||
"copypaste.copy_to_clipboard": "Nei klamboerd kopiearje",
|
||||
"directory.federated": "Fediverse (wat bekend is)",
|
||||
|
@ -390,6 +392,7 @@
|
|||
"lists.search": "Sykje nei minsken dy’t jo folgje",
|
||||
"lists.subheading": "Jo listen",
|
||||
"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}}",
|
||||
"moved_to_account_banner.text": "Omdat jo nei {movedToAccount} ferhuze binne is jo account {disabledAccount} op dit stuit útskeakele.",
|
||||
"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.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.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.message": "Ik bin {username} op #Mastodon! Folgje my op {url}",
|
||||
"onboarding.share.next_steps": "Mooglike folgjende stappen:",
|
||||
|
@ -521,6 +535,7 @@
|
|||
"privacy.unlisted.short": "Minder iepenbier",
|
||||
"privacy_policy.last_updated": "Lêst bywurke op {date}",
|
||||
"privacy_policy.title": "Privacybelied",
|
||||
"recommended": "Oanrekommandearre",
|
||||
"refresh": "Ferfarskje",
|
||||
"regeneration_indicator.label": "Lade…",
|
||||
"regeneration_indicator.sublabel": "Jo starttiidline wurdt oanmakke!",
|
||||
|
@ -591,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Berjochten dy’t oerienkomme mei {x}",
|
||||
"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_logged_out_message": "Allinnich beskikber as jo oanmeld binne.",
|
||||
"search_popout.language_code": "ISO-taalkoade",
|
||||
"search_popout.options": "Sykopsjes",
|
||||
"search_popout.quick_actions": "Flugge aksjes",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Publicacións coincidentes {x}",
|
||||
"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_logged_out_message": "Só dispoñible ao iniciar sesión.",
|
||||
"search_popout.language_code": "Código ISO do idioma",
|
||||
"search_popout.options": "Opcións de busca",
|
||||
"search_popout.quick_actions": "Accións rápidas",
|
||||
|
@ -686,7 +687,7 @@
|
|||
"status.translated_from_with": "Traducido do {lang} usando {provider}",
|
||||
"status.uncached_media_warning": "A vista previa non está dispoñíble",
|
||||
"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.save": "Gardar cambios",
|
||||
"subscribed_languages.target": "Cambiar a subscrición a idiomas para {target}",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "הודעות המכילות {x}",
|
||||
"search.search_or_paste": "חפש או הזן קישור",
|
||||
"search_popout.full_text_search_disabled_message": "בלתי זמין על {domain}.",
|
||||
"search_popout.full_text_search_logged_out_message": "זמין רק לאחר כניסה לאתר.",
|
||||
"search_popout.language_code": "קוד ISO לשפה",
|
||||
"search_popout.options": "אפשרויות חיפוש",
|
||||
"search_popout.quick_actions": "פעולות זריזות",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Bejegyzések a következő keresésre: {x}",
|
||||
"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_logged_out_message": "Csak bejelentkezve érhető el.",
|
||||
"search_popout.language_code": "ISO nyelvkód",
|
||||
"search_popout.options": "Keresési beállítások",
|
||||
"search_popout.quick_actions": "Gyors műveletek",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Færslur sem samsvara {x}",
|
||||
"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_logged_out_message": "Aðeins tiltækt eftir innskráningu.",
|
||||
"search_popout.language_code": "ISO-kóði tungumáls",
|
||||
"search_popout.options": "Leitarvalkostir",
|
||||
"search_popout.quick_actions": "Flýtiaðgerðir",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Post corrispondenti a {x}",
|
||||
"search.search_or_paste": "Cerca o incolla URL",
|
||||
"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.options": "Opzioni di ricerca",
|
||||
"search_popout.quick_actions": "Azioni rapide",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "{x}에 맞는 게시물",
|
||||
"search.search_or_paste": "검색하거나 URL 붙여넣기",
|
||||
"search_popout.full_text_search_disabled_message": "{domain}에서는 이용할 수 없습니다.",
|
||||
"search_popout.full_text_search_logged_out_message": "로그인되어 있을 때만 할 수 있습니다.",
|
||||
"search_popout.language_code": "ISO 언어코드",
|
||||
"search_popout.options": "검색 옵션",
|
||||
"search_popout.quick_actions": "빠른 작업",
|
||||
|
|
|
@ -445,6 +445,7 @@
|
|||
"search.placeholder": "Paieška",
|
||||
"search.search_or_paste": "Ieškok arba įklijuok URL",
|
||||
"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.specific_date": "konkreti data",
|
||||
"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.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.lock": "besloten",
|
||||
"compose_form.lock_disclaimer.lock": "vergrendeld",
|
||||
"compose_form.placeholder": "Wat wil je kwijt?",
|
||||
"compose_form.poll.add_option": "Keuze toevoegen",
|
||||
"compose_form.poll.duration": "Duur van de peiling",
|
||||
|
@ -382,7 +382,7 @@
|
|||
"lists.delete": "Lijst verwijderen",
|
||||
"lists.edit": "Lijst bewerken",
|
||||
"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.title_placeholder": "Naam nieuwe lijst",
|
||||
"lists.replies_policy.followed": "Elke gevolgde gebruiker",
|
||||
|
@ -395,7 +395,7 @@
|
|||
"loading_indicator.label": "Laden…",
|
||||
"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.",
|
||||
"mute_modal.duration": "Duur",
|
||||
"mute_modal.duration": "Tijdsduur",
|
||||
"mute_modal.hide_notifications": "Verberg meldingen van deze persoon?",
|
||||
"mute_modal.indefinite": "Voor onbepaalde tijd",
|
||||
"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.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.profile.discoverable": "Maak mij profiel ontdekbaar",
|
||||
"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": "Maak mijn profiel vindbaar",
|
||||
"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_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.",
|
||||
|
@ -491,7 +491,7 @@
|
|||
"onboarding.profile.save_and_continue": "Opslaan en doorgaan",
|
||||
"onboarding.profile.title": "Profiel instellen",
|
||||
"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.message": "Ik ben {username} op #Mastodon! Volg mij op {url}",
|
||||
"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.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.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.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!",
|
||||
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Berichten die overeenkomen met {x}",
|
||||
"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_logged_out_message": "Alleen beschikbaar als je bent ingelogd.",
|
||||
"search_popout.language_code": "ISO-taalcode",
|
||||
"search_popout.options": "Zoekopties",
|
||||
"search_popout.quick_actions": "Snelle acties",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Wpisy pasujące do {x}",
|
||||
"search.search_or_paste": "Wyszukaj lub wklej adres",
|
||||
"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.options": "Opcje wyszukiwania",
|
||||
"search_popout.quick_actions": "Szybkie akcje",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Publicações correspondentes a {x}",
|
||||
"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_logged_out_message": "Disponível apenas quando conectado.",
|
||||
"search_popout.language_code": "Código ISO do idioma",
|
||||
"search_popout.options": "Opções de pesquisa",
|
||||
"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.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_logged_out_message": "Apenas disponível quando tem sessão iniciada.",
|
||||
"search_popout.language_code": "Código ISO do idioma",
|
||||
"search_popout.options": "Opções de pesquisa",
|
||||
"search_popout.quick_actions": "Ações rápidas",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"account.blocked": "Заблокировано",
|
||||
"account.browse_more_on_origin_server": "Посмотреть в оригинальном профиле",
|
||||
"account.cancel_follow_request": "Отозвать запрос на подписку",
|
||||
"account.copy": "Скопировать ссылку на профиль",
|
||||
"account.direct": "Лично упоминать @{name}",
|
||||
"account.disable_notifications": "Не уведомлять о постах от @{name}",
|
||||
"account.domain_blocked": "Домен заблокирован",
|
||||
|
@ -191,6 +192,7 @@
|
|||
"conversation.mark_as_read": "Отметить как прочитанное",
|
||||
"conversation.open": "Просмотр беседы",
|
||||
"conversation.with": "С {names}",
|
||||
"copy_icon_button.copied": "Скопировано в буфер обмена",
|
||||
"copypaste.copied": "Скопировано",
|
||||
"copypaste.copy_to_clipboard": "Копировать в буфер обмена",
|
||||
"directory.federated": "Со всей федерации",
|
||||
|
@ -222,6 +224,7 @@
|
|||
"emoji_button.search_results": "Результаты поиска",
|
||||
"emoji_button.symbols": "Символы",
|
||||
"emoji_button.travel": "Путешествия и места",
|
||||
"empty_column.account_hides_collections": "Данный пользователь решил не предоставлять эту информацию",
|
||||
"empty_column.account_suspended": "Учетная запись заблокирована",
|
||||
"empty_column.account_timeline": "Здесь нет постов!",
|
||||
"empty_column.account_unavailable": "Профиль недоступен",
|
||||
|
@ -389,6 +392,7 @@
|
|||
"lists.search": "Искать среди подписок",
|
||||
"lists.subheading": "Ваши списки",
|
||||
"load_pending": "{count, plural, one {# новый элемент} few {# новых элемента} other {# новых элементов}}",
|
||||
"loading_indicator.label": "Загрузка…",
|
||||
"media_gallery.toggle_visible": "Показать/скрыть {number, plural, =1 {изображение} other {изображения}}",
|
||||
"moved_to_account_banner.text": "Ваша учетная запись {disabledAccount} в настоящее время заморожена, потому что вы переехали на {movedToAccount}.",
|
||||
"mute_modal.duration": "Продолжительность",
|
||||
|
@ -477,6 +481,17 @@
|
|||
"onboarding.follows.empty": "К сожалению, сейчас нет результатов. Вы можете попробовать использовать поиск или просмотреть страницу \"Исследования\", чтобы найти людей, за которыми можно следить, или повторить попытку позже.",
|
||||
"onboarding.follows.lead": "Вы сами формируете свою домашнюю ленту. Чем больше людей, за которыми вы следите, тем активнее и интереснее она будет. Эти профили могут быть хорошей отправной точкой - вы всегда можете от них отказаться!",
|
||||
"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.message": "Я {username} на #Mastodon! Следуйте за мной по адресу {url}",
|
||||
"onboarding.share.next_steps": "Возможные дальнейшие шаги:",
|
||||
|
@ -520,6 +535,7 @@
|
|||
"privacy.unlisted.short": "Скрытый",
|
||||
"privacy_policy.last_updated": "Последнее обновление {date}",
|
||||
"privacy_policy.title": "Политика конфиденциальности",
|
||||
"recommended": "Рекомендуется",
|
||||
"refresh": "Обновить",
|
||||
"regeneration_indicator.label": "Загрузка…",
|
||||
"regeneration_indicator.sublabel": "Один момент, мы подготавливаем вашу ленту!",
|
||||
|
@ -590,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Посты, соответствующие {x}",
|
||||
"search.search_or_paste": "Поиск (или вставьте URL)",
|
||||
"search_popout.full_text_search_disabled_message": "Недоступно на {domain}.",
|
||||
"search_popout.full_text_search_logged_out_message": "Доступно только при авторизации.",
|
||||
"search_popout.language_code": "Код языка по стандарту ISO",
|
||||
"search_popout.options": "Параметры поиска",
|
||||
"search_popout.quick_actions": "Быстрые действия",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Objave, ki se ujemajo z {x}",
|
||||
"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_logged_out_message": "Na voljo le, če ste prijavljeni.",
|
||||
"search_popout.language_code": "Koda ISO jezika",
|
||||
"search_popout.options": "Možnosti iskanja",
|
||||
"search_popout.quick_actions": "Hitra dejanja",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Podudaranje objava {x}",
|
||||
"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_logged_out_message": "Dostupno samo kada ste prijavljeni.",
|
||||
"search_popout.language_code": "ISO kod jezika",
|
||||
"search_popout.options": "Opcije pretrage",
|
||||
"search_popout.quick_actions": "Brze radnje",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Подударање објава {x}",
|
||||
"search.search_or_paste": "Претражите или унесите адресу",
|
||||
"search_popout.full_text_search_disabled_message": "Није доступно на {domain}.",
|
||||
"search_popout.full_text_search_logged_out_message": "Доступно само када сте пријављени.",
|
||||
"search_popout.language_code": "ISO код језика",
|
||||
"search_popout.options": "Опције претраге",
|
||||
"search_popout.quick_actions": "Брзе радње",
|
||||
|
|
|
@ -604,6 +604,7 @@
|
|||
"search.quick_action.status_search": "Inlägg som matchar {x}",
|
||||
"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_logged_out_message": "Endast tillgängligt när du är inloggad.",
|
||||
"search_popout.language_code": "ISO språkkod",
|
||||
"search_popout.options": "Sökalternativ",
|
||||
"search_popout.quick_actions": "Snabbåtgärder",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "โพสต์ที่ตรงกับ {x}",
|
||||
"search.search_or_paste": "ค้นหาหรือวาง URL",
|
||||
"search_popout.full_text_search_disabled_message": "ไม่พร้อมใช้งานใน {domain}",
|
||||
"search_popout.full_text_search_logged_out_message": "พร้อมใช้งานเฉพาะเมื่อเข้าสู่ระบบแล้วเท่านั้น",
|
||||
"search_popout.language_code": "รหัสภาษา ISO",
|
||||
"search_popout.options": "ตัวเลือกการค้นหา",
|
||||
"search_popout.quick_actions": "การกระทำด่วน",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Eşleşen gönderiler {x}",
|
||||
"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_logged_out_message": "Sadece oturum açıldığında mevcuttur.",
|
||||
"search_popout.language_code": "ISO dil kodu",
|
||||
"search_popout.options": "Arama seçenekleri",
|
||||
"search_popout.quick_actions": "Hızlı eylemler",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "Збіг дописів {x}",
|
||||
"search.search_or_paste": "Введіть адресу або пошуковий запит",
|
||||
"search_popout.full_text_search_disabled_message": "Недоступно на {domain}.",
|
||||
"search_popout.full_text_search_logged_out_message": "Доступно лише після входу.",
|
||||
"search_popout.language_code": "Код мови ISO",
|
||||
"search_popout.options": "Опції пошуку",
|
||||
"search_popout.quick_actions": "Швидкі дії",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"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_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.options": "Tùy chọn tìm kiếm",
|
||||
"search_popout.quick_actions": "Thao tác nhanh",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "匹配 {x} 的嘟文",
|
||||
"search.search_or_paste": "搜索或输入网址",
|
||||
"search_popout.full_text_search_disabled_message": "在 {domain} 不可用",
|
||||
"search_popout.full_text_search_logged_out_message": "只有登录后才可用。",
|
||||
"search_popout.language_code": "ISO语言代码",
|
||||
"search_popout.options": "搜索选项",
|
||||
"search_popout.quick_actions": "快捷操作",
|
||||
|
|
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "符合的帖文 {x}",
|
||||
"search.search_or_paste": "搜尋或貼上網址",
|
||||
"search_popout.full_text_search_disabled_message": "在 {domain} 上無法使用。",
|
||||
"search_popout.full_text_search_logged_out_message": "登入後才可使用。",
|
||||
"search_popout.language_code": "ISO 語言代碼",
|
||||
"search_popout.options": "搜尋選項",
|
||||
"search_popout.quick_actions": "快速動作",
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
"confirmations.domain_block.confirm": "封鎖整個網域",
|
||||
"confirmations.domain_block.message": "您真的非常確定要封鎖整個 {domain} 網域嗎?大部分情況下,封鎖或靜音少數特定的帳號就能滿足需求了。您將不能在任何公開的時間軸及通知中看到來自此網域的內容。您來自該網域的跟隨者也將被移除。",
|
||||
"confirmations.edit.confirm": "編輯",
|
||||
"confirmations.edit.message": "編輯嘟文將覆蓋掉您目前正在撰寫的訊息。是否仍要繼續?",
|
||||
"confirmations.edit.message": "編輯嘟文將覆蓋掉您目前正在撰寫之嘟文內容。您是否仍要繼續?",
|
||||
"confirmations.logout.confirm": "登出",
|
||||
"confirmations.logout.message": "您確定要登出嗎?",
|
||||
"confirmations.mute.confirm": "靜音",
|
||||
|
@ -606,6 +606,7 @@
|
|||
"search.quick_action.status_search": "符合的嘟文 {x}",
|
||||
"search.search_or_paste": "搜尋或輸入網址",
|
||||
"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.options": "搜尋選項",
|
||||
"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
|
||||
// they don't export the icons in the package
|
||||
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' />
|
||||
</svg>
|
||||
);
|
||||
|
||||
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' />
|
||||
</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 = {
|
||||
match: PropTypes.object.isRequired,
|
||||
|
@ -16,31 +16,37 @@ export const WithOptionalRouterPropTypes = {
|
|||
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
|
||||
// but does not fail if called outside of a React Router context
|
||||
export function withOptionalRouter(Component) {
|
||||
const displayName = `withRouter(${Component.displayName || Component.name})`;
|
||||
const C = props => {
|
||||
export function withOptionalRouter<
|
||||
ComponentType extends React.ComponentType<OptionalRouterProps>,
|
||||
>(Component: ComponentType) {
|
||||
const displayName = `withRouter(${Component.displayName ?? Component.name})`;
|
||||
const C = (props: React.ComponentProps<ComponentType>) => {
|
||||
const { wrappedComponentRef, ...remainingProps } = props;
|
||||
|
||||
return (
|
||||
<__RouterContext.Consumer>
|
||||
{context => {
|
||||
if(context)
|
||||
{(context) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (context) {
|
||||
return (
|
||||
// @ts-expect-error - Dynamic covariant generic components are tough to type.
|
||||
<Component
|
||||
{...remainingProps}
|
||||
{...context}
|
||||
ref={wrappedComponentRef}
|
||||
/>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<Component
|
||||
{...remainingProps}
|
||||
ref={wrappedComponentRef}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// @ts-expect-error - Dynamic covariant generic components are tough to type.
|
||||
return <Component {...remainingProps} ref={wrappedComponentRef} />;
|
||||
}
|
||||
}}
|
||||
</__RouterContext.Consumer>
|
||||
);
|
||||
|
@ -53,8 +59,8 @@ export function withOptionalRouter(Component) {
|
|||
wrappedComponentRef: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.func,
|
||||
PropTypes.object
|
||||
])
|
||||
PropTypes.object,
|
||||
]),
|
||||
};
|
||||
|
||||
return hoistStatics(C, Component);
|
|
@ -1,11 +1,7 @@
|
|||
import { isMobile } from '../is_mobile';
|
||||
|
||||
/** @type {number | null} */
|
||||
let cachedScrollbarWidth = null;
|
||||
let cachedScrollbarWidth: number | null = null;
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
const getActualScrollbarWidth = () => {
|
||||
const outer = document.createElement('div');
|
||||
outer.style.visibility = 'hidden';
|
||||
|
@ -16,20 +12,19 @@ const getActualScrollbarWidth = () => {
|
|||
outer.appendChild(inner);
|
||||
|
||||
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
|
||||
outer.parentNode.removeChild(outer);
|
||||
outer.remove();
|
||||
|
||||
return scrollbarWidth;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
export const getScrollbarWidth = () => {
|
||||
if (cachedScrollbarWidth !== null) {
|
||||
return cachedScrollbarWidth;
|
||||
}
|
||||
|
||||
const scrollbarWidth = isMobile(window.innerWidth) ? 0 : getActualScrollbarWidth();
|
||||
const scrollbarWidth = isMobile(window.innerWidth)
|
||||
? 0
|
||||
: getActualScrollbarWidth();
|
||||
cachedScrollbarWidth = scrollbarWidth;
|
||||
|
||||
return scrollbarWidth;
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AdminMailer < ApplicationMailer
|
||||
layout 'plain_mailer'
|
||||
layout 'admin_mailer'
|
||||
|
||||
helper :accounts
|
||||
helper :languages
|
||||
|
|
|
@ -24,12 +24,12 @@ class Admin::ActionLog < ApplicationRecord
|
|||
belongs_to :account
|
||||
belongs_to :target, polymorphic: true, optional: true
|
||||
|
||||
default_scope -> { order('id desc') }
|
||||
|
||||
before_validation :set_human_identifier
|
||||
before_validation :set_route_param
|
||||
before_validation :set_permalink
|
||||
|
||||
scope :latest, -> { order(id: :desc) }
|
||||
|
||||
def action
|
||||
super.to_sym
|
||||
end
|
||||
|
|
|
@ -72,7 +72,7 @@ class Admin::ActionLogFilter
|
|||
end
|
||||
|
||||
def results
|
||||
scope = Admin::ActionLog.includes(:target)
|
||||
scope = latest_action_logs.includes(:target)
|
||||
|
||||
params.each do |key, value|
|
||||
next if key.to_s == 'page'
|
||||
|
@ -88,14 +88,18 @@ class Admin::ActionLogFilter
|
|||
def scope_for(key, value)
|
||||
case key
|
||||
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'
|
||||
Admin::ActionLog.where(account_id: value)
|
||||
latest_action_logs.where(account_id: value)
|
||||
when 'target_account_id'
|
||||
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
|
||||
raise Mastodon::InvalidParameterError, "Unknown filter: #{key}"
|
||||
end
|
||||
end
|
||||
|
||||
def latest_action_logs
|
||||
Admin::ActionLog.latest
|
||||
end
|
||||
end
|
||||
|
|
|
@ -131,25 +131,25 @@ class Report < ApplicationRecord
|
|||
Admin::ActionLog.where(
|
||||
target_type: 'Report',
|
||||
target_id: id
|
||||
).unscope(:order).arel,
|
||||
).arel,
|
||||
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'Account',
|
||||
target_id: target_account_id
|
||||
).unscope(:order).arel,
|
||||
).arel,
|
||||
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'Status',
|
||||
target_id: status_ids
|
||||
).unscope(:order).arel,
|
||||
).arel,
|
||||
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'AccountWarning',
|
||||
target_id: AccountWarning.where(report_id: id).select(:id)
|
||||
).unscope(:order).arel,
|
||||
).arel,
|
||||
].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
|
||||
|
||||
private
|
||||
|
|
|
@ -49,7 +49,7 @@ class UserRole < ApplicationRecord
|
|||
invite_users
|
||||
).freeze,
|
||||
|
||||
moderation: %w(
|
||||
moderation: %i(
|
||||
view_dashboard
|
||||
view_audit_log
|
||||
manage_users
|
||||
|
@ -63,7 +63,7 @@ class UserRole < ApplicationRecord
|
|||
manage_invites
|
||||
).freeze,
|
||||
|
||||
administration: %w(
|
||||
administration: %i(
|
||||
manage_settings
|
||||
manage_rules
|
||||
manage_roles
|
||||
|
@ -72,7 +72,7 @@ class UserRole < ApplicationRecord
|
|||
manage_announcements
|
||||
).freeze,
|
||||
|
||||
devops: %w(
|
||||
devops: %i(
|
||||
view_devops
|
||||
).freeze,
|
||||
|
||||
|
|
|
@ -38,7 +38,10 @@ class NodeInfo::Serializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def metadata
|
||||
{}
|
||||
{
|
||||
nodeName: Setting.site_title,
|
||||
nodeDescription: Setting.site_short_description,
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
class REST::ApplicationSerializer < ActiveModel::Serializer
|
||||
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
|
||||
object.id.to_s
|
||||
|
|
|
@ -48,6 +48,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
|||
status: object.status_page_url,
|
||||
},
|
||||
|
||||
vapid: {
|
||||
public_key: Rails.configuration.x.vapid_public_key,
|
||||
},
|
||||
|
||||
accounts: {
|
||||
max_featured_tags: FeaturedTag::LIMIT,
|
||||
},
|
||||
|
|
|
@ -100,7 +100,9 @@ class ResolveAccountService < BaseService
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def fetch_account!
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
- if @account.user_prefers_noindex?
|
||||
%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) }/
|
||||
|
||||
- @account.fields.select(&:verifiable?).each do |field|
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
= f.input :report_id, as: :hidden
|
||||
|
||||
.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?
|
||||
%hr.spacer/
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
.filter-subset.filter-subset--with-select
|
||||
%strong= t('admin.accounts.moderation.title')
|
||||
.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
|
||||
%strong= t('admin.accounts.role')
|
||||
.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