mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-22 12:55:56 +01:00
Merge branch 'main' into patch-11
This commit is contained in:
commit
c266bab9ea
539 changed files with 8133 additions and 4793 deletions
59
.annotaterb.yml
Normal file
59
.annotaterb.yml
Normal file
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
:position: before
|
||||
:position_in_additional_file_patterns: before
|
||||
:position_in_class: before
|
||||
:position_in_factory: before
|
||||
:position_in_fixture: before
|
||||
:position_in_routes: before
|
||||
:position_in_serializer: before
|
||||
:position_in_test: before
|
||||
:classified_sort: true
|
||||
:exclude_controllers: true
|
||||
:exclude_factories: true
|
||||
:exclude_fixtures: true
|
||||
:exclude_helpers: true
|
||||
:exclude_scaffolds: true
|
||||
:exclude_serializers: true
|
||||
:exclude_sti_subclasses: true
|
||||
:exclude_tests: true
|
||||
:force: false
|
||||
:format_markdown: false
|
||||
:format_rdoc: false
|
||||
:format_yard: false
|
||||
:frozen: false
|
||||
:ignore_model_sub_dir: false
|
||||
:ignore_unknown_models: false
|
||||
:include_version: false
|
||||
:show_complete_foreign_keys: false
|
||||
:show_foreign_keys: false
|
||||
:show_indexes: false
|
||||
:simple_indexes: false
|
||||
:sort: false
|
||||
:timestamp: false
|
||||
:trace: false
|
||||
:with_comment: true
|
||||
:with_column_comments: true
|
||||
:with_table_comments: true
|
||||
:active_admin: false
|
||||
:command:
|
||||
:debug: false
|
||||
:hide_default_column_types: ''
|
||||
:hide_limit_column_types: 'integer,boolean'
|
||||
:ignore_columns:
|
||||
:ignore_routes:
|
||||
:models: true
|
||||
:routes: false
|
||||
:skip_on_db_migrate: false
|
||||
:target_action: :do_annotations
|
||||
:wrapper:
|
||||
:wrapper_close:
|
||||
:wrapper_open:
|
||||
:classes_default_to_s: []
|
||||
:additional_file_patterns: []
|
||||
:model_dir:
|
||||
- app/models
|
||||
:require: []
|
||||
:root_dir:
|
||||
- ''
|
||||
|
||||
:show_check_constraints: false
|
|
@ -69,7 +69,7 @@ services:
|
|||
hard: -1
|
||||
|
||||
libretranslate:
|
||||
image: libretranslate/libretranslate:v1.6.1
|
||||
image: libretranslate/libretranslate:v1.6.2
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- lt-data:/home/libretranslate/.local
|
||||
|
|
1
.github/workflows/build-container-image.yml
vendored
1
.github/workflows/build-container-image.yml
vendored
|
@ -92,6 +92,7 @@ jobs:
|
|||
build-args: |
|
||||
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
|
||||
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
|
||||
SOURCE_COMMIT=${{ github.sha }}
|
||||
platforms: ${{ inputs.platforms }}
|
||||
provenance: false
|
||||
builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.3.5
|
||||
3.3.6
|
||||
|
|
42
CHANGELOG.md
42
CHANGELOG.md
|
@ -2,6 +2,48 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.3.1] - 2024-10-21
|
||||
|
||||
### Added
|
||||
|
||||
- Add more explicit explanations about author attribution and `fediverse:creator` (#32383 by @ClearlyClaire)
|
||||
- Add ability to group follow notifications in WebUI, can be disabled in the column settings (#32520 by @renchap)
|
||||
- Add back a 6 hours mute duration option (#32522 by @renchap)
|
||||
- Add note about not changing ActiveRecord encryption secrets once they are set (#32413, #32476, #32512, and #32537 by @ClearlyClaire and @mjankowski)
|
||||
|
||||
### Changed
|
||||
|
||||
- Change translation feature to translate to selected regional variant (e.g. pt-BR) if available (#32428 by @c960657)
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove ability to get embed code for remote posts (#32578 by @ClearlyClaire)\
|
||||
Getting the embed code is only reliable for local posts.\
|
||||
It never worked for non-Mastodon servers, and stopped working correctly with the changes made in 4.3.0.\
|
||||
We have therefore decided to remove the menu entry while we investigate solutions.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix follow recommendation moderation page default language when using regional variant (#32580 by @ClearlyClaire)
|
||||
- Fix column-settings spacing in local timeline in advanced view (#32567 by @lindwurm)
|
||||
- Fix broken i18n in text welcome mailer tags area (#32571 by @mjankowski)
|
||||
- Fix missing or incorrect cache-control headers for Streaming server (#32551 by @ThisIsMissEm)
|
||||
- Fix only the first paragraph being displayed in some notifications (#32348 by @ClearlyClaire)
|
||||
- Fix reblog icons on account media view (#32506 by @tribela)
|
||||
- Fix Content-Security-Policy not allowing OpenStack SWIFT object storage URI (#32439 by @kenkiku1021)
|
||||
- Fix back arrow pointing to the incorrect direction in RTL languages (#32485 by @renchap)
|
||||
- Fix streaming server using `REDIS_USERNAME` instead of `REDIS_USER` (#32493 by @ThisIsMissEm)
|
||||
- Fix follow recommendation carrousel scrolling on RTL layouts (#32462 and #32505 by @ClearlyClaire)
|
||||
- Fix follow recommendation suppressions not applying immediately (#32392 by @ClearlyClaire)
|
||||
- Fix language of push notifications (#32415 by @ClearlyClaire)
|
||||
- Fix mute duration not being shown in list of muted accounts in web UI (#32388 by @ClearlyClaire)
|
||||
- Fix “Mark every notification as read” not updating the read marker if scrolled down (#32385 by @ClearlyClaire)
|
||||
- Fix “Mention” appearing for otherwise filtered posts (#32356 by @ClearlyClaire)
|
||||
- Fix notification requests from suspended accounts still being listed (#32354 by @ClearlyClaire)
|
||||
- Fix list edition modal styling (#32358 and #32367 by @ClearlyClaire and @vmstan)
|
||||
- Fix 4 columns barely not fitting on 1920px screen (#32361 by @ClearlyClaire)
|
||||
- Fix icon alignment in applications list (#32293 by @mjankowski)
|
||||
|
||||
## [4.3.0] - 2024-10-08
|
||||
|
||||
The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by @mjankowski.
|
||||
|
|
309
Dockerfile
309
Dockerfile
|
@ -12,7 +12,7 @@ ARG BUILDPLATFORM=${BUILDPLATFORM}
|
|||
|
||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"]
|
||||
# renovate: datasource=docker depName=docker.io/ruby
|
||||
ARG RUBY_VERSION="3.3.5"
|
||||
ARG RUBY_VERSION="3.3.6"
|
||||
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||
# renovate: datasource=node-version depName=node
|
||||
ARG NODE_MAJOR_VERSION="22"
|
||||
|
@ -29,6 +29,8 @@ FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby
|
|||
ARG MASTODON_VERSION_PRERELEASE=""
|
||||
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-123456"]
|
||||
ARG MASTODON_VERSION_METADATA=""
|
||||
# Will be available as Mastodon::Version.source_commit
|
||||
ARG SOURCE_COMMIT=""
|
||||
|
||||
# Allow Ruby on Rails to serve static files
|
||||
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
|
||||
|
@ -45,30 +47,31 @@ ARG GID="991"
|
|||
|
||||
# Apply Mastodon build options based on options above
|
||||
ENV \
|
||||
# Apply Mastodon version information
|
||||
# Apply Mastodon version information
|
||||
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
|
||||
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \
|
||||
# Apply Mastodon static files and YJIT options
|
||||
SOURCE_COMMIT="${SOURCE_COMMIT}" \
|
||||
# Apply Mastodon static files and YJIT options
|
||||
RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \
|
||||
RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \
|
||||
# Apply timezone
|
||||
# Apply timezone
|
||||
TZ=${TZ}
|
||||
|
||||
ENV \
|
||||
# Configure the IP to bind Mastodon to when serving traffic
|
||||
# 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
|
||||
# Use production settings for Yarn, Node and related nodejs based tools
|
||||
NODE_ENV="production" \
|
||||
# Use production settings for Ruby on Rails
|
||||
# Use production settings for Ruby on Rails
|
||||
RAILS_ENV="production" \
|
||||
# Add Ruby and Mastodon installation to the PATH
|
||||
# Add Ruby and Mastodon installation to the PATH
|
||||
DEBIAN_FRONTEND="noninteractive" \
|
||||
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \
|
||||
# Optimize jemalloc 5.x performance
|
||||
# Optimize jemalloc 5.x performance
|
||||
MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \
|
||||
# Enable libvips, should not be changed
|
||||
# Enable libvips, should not be changed
|
||||
MASTODON_USE_LIBVIPS=true \
|
||||
# Sidekiq will touch tmp/sidekiq_process_has_started_and_will_begin_processing_jobs to indicate it is ready. This can be used for a readiness check in Kubernetes
|
||||
# Sidekiq will touch tmp/sidekiq_process_has_started_and_will_begin_processing_jobs to indicate it is ready. This can be used for a readiness check in Kubernetes
|
||||
MASTODON_SIDEKIQ_READY_FILENAME=sidekiq_process_has_started_and_will_begin_processing_jobs
|
||||
|
||||
# Set default shell used for running commands
|
||||
|
@ -79,14 +82,14 @@ ARG TARGETPLATFORM
|
|||
RUN echo "Target platform is $TARGETPLATFORM"
|
||||
|
||||
RUN \
|
||||
# Remove automatic apt cache Docker cleanup scripts
|
||||
# Remove automatic apt cache Docker cleanup scripts
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean; \
|
||||
# Sets timezone
|
||||
# Sets timezone
|
||||
echo "${TZ}" > /etc/localtime; \
|
||||
# Creates mastodon user/group and sets home directory
|
||||
# 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
|
||||
# Creates /mastodon symlink to /opt/mastodon
|
||||
ln -s /opt/mastodon /mastodon;
|
||||
|
||||
# Set /opt/mastodon as working directory
|
||||
|
@ -94,28 +97,28 @@ 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
|
||||
# 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
|
||||
# Install jemalloc, curl and other necessary components
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
file \
|
||||
libjemalloc2 \
|
||||
patchelf \
|
||||
procps \
|
||||
tini \
|
||||
tzdata \
|
||||
wget \
|
||||
curl \
|
||||
file \
|
||||
libjemalloc2 \
|
||||
patchelf \
|
||||
procps \
|
||||
tini \
|
||||
tzdata \
|
||||
wget \
|
||||
; \
|
||||
# Patch Ruby to use jemalloc
|
||||
# Patch Ruby to use jemalloc
|
||||
patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \
|
||||
# Discard patchelf after use
|
||||
# Discard patchelf after use
|
||||
apt-get purge -y \
|
||||
patchelf \
|
||||
patchelf \
|
||||
;
|
||||
|
||||
# Create temporary build layer from base image
|
||||
|
@ -132,56 +135,56 @@ ARG TARGETPLATFORM
|
|||
|
||||
# hadolint ignore=DL3008
|
||||
RUN \
|
||||
# Mount Apt cache and lib directories from Docker buildx caches
|
||||
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||
# Install build tools and bundler dependencies from APT
|
||||
# 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 \
|
||||
autoconf \
|
||||
automake \
|
||||
build-essential \
|
||||
cmake \
|
||||
git \
|
||||
libgdbm-dev \
|
||||
libglib2.0-dev \
|
||||
libgmp-dev \
|
||||
libicu-dev \
|
||||
libidn-dev \
|
||||
libpq-dev \
|
||||
libssl-dev \
|
||||
libtool \
|
||||
meson \
|
||||
nasm \
|
||||
pkg-config \
|
||||
shared-mime-info \
|
||||
xz-utils \
|
||||
# libvips components
|
||||
libcgif-dev \
|
||||
libexif-dev \
|
||||
libexpat1-dev \
|
||||
libgirepository1.0-dev \
|
||||
libheif-dev \
|
||||
libimagequant-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
liblcms2-dev \
|
||||
liborc-dev \
|
||||
libspng-dev \
|
||||
libtiff-dev \
|
||||
libwebp-dev \
|
||||
autoconf \
|
||||
automake \
|
||||
build-essential \
|
||||
cmake \
|
||||
git \
|
||||
libgdbm-dev \
|
||||
libglib2.0-dev \
|
||||
libgmp-dev \
|
||||
libicu-dev \
|
||||
libidn-dev \
|
||||
libpq-dev \
|
||||
libssl-dev \
|
||||
libtool \
|
||||
meson \
|
||||
nasm \
|
||||
pkg-config \
|
||||
shared-mime-info \
|
||||
xz-utils \
|
||||
# libvips components
|
||||
libcgif-dev \
|
||||
libexif-dev \
|
||||
libexpat1-dev \
|
||||
libgirepository1.0-dev \
|
||||
libheif-dev \
|
||||
libimagequant-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
liblcms2-dev \
|
||||
liborc-dev \
|
||||
libspng-dev \
|
||||
libtiff-dev \
|
||||
libwebp-dev \
|
||||
# ffmpeg components
|
||||
libdav1d-dev \
|
||||
liblzma-dev \
|
||||
libmp3lame-dev \
|
||||
libopus-dev \
|
||||
libsnappy-dev \
|
||||
libvorbis-dev \
|
||||
libvpx-dev \
|
||||
libx264-dev \
|
||||
libx265-dev \
|
||||
libdav1d-dev \
|
||||
liblzma-dev \
|
||||
libmp3lame-dev \
|
||||
libopus-dev \
|
||||
libsnappy-dev \
|
||||
libvorbis-dev \
|
||||
libvpx-dev \
|
||||
libx264-dev \
|
||||
libx265-dev \
|
||||
;
|
||||
|
||||
RUN \
|
||||
# Configure Corepack
|
||||
# Configure Corepack
|
||||
rm /usr/local/bin/yarn*; \
|
||||
corepack enable; \
|
||||
corepack prepare --activate;
|
||||
|
@ -228,28 +231,28 @@ WORKDIR /usr/local/ffmpeg/src/ffmpeg-${FFMPEG_VERSION}
|
|||
# Configure and compile ffmpeg
|
||||
RUN \
|
||||
./configure \
|
||||
--prefix=/usr/local/ffmpeg \
|
||||
--toolchain=hardened \
|
||||
--disable-debug \
|
||||
--disable-devices \
|
||||
--disable-doc \
|
||||
--disable-ffplay \
|
||||
--disable-network \
|
||||
--disable-static \
|
||||
--enable-ffmpeg \
|
||||
--enable-ffprobe \
|
||||
--enable-gpl \
|
||||
--enable-libdav1d \
|
||||
--enable-libmp3lame \
|
||||
--enable-libopus \
|
||||
--enable-libsnappy \
|
||||
--enable-libvorbis \
|
||||
--enable-libvpx \
|
||||
--enable-libwebp \
|
||||
--enable-libx264 \
|
||||
--enable-libx265 \
|
||||
--enable-shared \
|
||||
--enable-version3 \
|
||||
--prefix=/usr/local/ffmpeg \
|
||||
--toolchain=hardened \
|
||||
--disable-debug \
|
||||
--disable-devices \
|
||||
--disable-doc \
|
||||
--disable-ffplay \
|
||||
--disable-network \
|
||||
--disable-static \
|
||||
--enable-ffmpeg \
|
||||
--enable-ffprobe \
|
||||
--enable-gpl \
|
||||
--enable-libdav1d \
|
||||
--enable-libmp3lame \
|
||||
--enable-libopus \
|
||||
--enable-libsnappy \
|
||||
--enable-libvorbis \
|
||||
--enable-libvpx \
|
||||
--enable-libwebp \
|
||||
--enable-libx264 \
|
||||
--enable-libx265 \
|
||||
--enable-shared \
|
||||
--enable-version3 \
|
||||
; \
|
||||
make -j$(nproc); \
|
||||
make install;
|
||||
|
@ -263,17 +266,17 @@ ARG TARGETPLATFORM
|
|||
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
|
||||
# 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
|
||||
# Configure bundle to not cache downloaded Gems
|
||||
bundle config set --global cache_all "false"; \
|
||||
# Configure bundle to only process production Gems
|
||||
# Configure bundle to only process production Gems
|
||||
bundle config set --local without "development test"; \
|
||||
# Configure bundle to not warn about root user
|
||||
# Configure bundle to not warn about root user
|
||||
bundle config set silence_root_warning "true"; \
|
||||
# Download and install required Gems
|
||||
# Download and install required Gems
|
||||
bundle install -j"$(nproc)";
|
||||
|
||||
# Create temporary node specific build layer from build layer
|
||||
|
@ -288,9 +291,9 @@ COPY .yarn /opt/mastodon/.yarn
|
|||
|
||||
# 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
|
||||
--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;
|
||||
|
||||
# Create temporary assets build layer from build layer
|
||||
|
@ -311,10 +314,10 @@ ARG TARGETPLATFORM
|
|||
|
||||
RUN \
|
||||
ldconfig; \
|
||||
# Use Ruby on Rails to create Mastodon assets
|
||||
# Use Ruby on Rails to create Mastodon assets
|
||||
SECRET_KEY_BASE_DUMMY=1 \
|
||||
bundle exec rails assets:precompile; \
|
||||
# Cleanup temporary files
|
||||
# Cleanup temporary files
|
||||
rm -fr /opt/mastodon/tmp;
|
||||
|
||||
# Prep final Mastodon Ruby layer
|
||||
|
@ -324,49 +327,49 @@ ARG TARGETPLATFORM
|
|||
|
||||
# hadolint ignore=DL3008
|
||||
RUN \
|
||||
# Mount Apt cache and lib directories from Docker buildx caches
|
||||
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||
# 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
|
||||
# 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 \
|
||||
libexpat1 \
|
||||
libglib2.0-0 \
|
||||
libicu72 \
|
||||
libidn12 \
|
||||
libpq5 \
|
||||
libreadline8 \
|
||||
libssl3 \
|
||||
libyaml-0-2 \
|
||||
libexpat1 \
|
||||
libglib2.0-0 \
|
||||
libicu72 \
|
||||
libidn12 \
|
||||
libpq5 \
|
||||
libreadline8 \
|
||||
libssl3 \
|
||||
libyaml-0-2 \
|
||||
# libvips components
|
||||
libcgif0 \
|
||||
libexif12 \
|
||||
libheif1 \
|
||||
libimagequant0 \
|
||||
libjpeg62-turbo \
|
||||
liblcms2-2 \
|
||||
liborc-0.4-0 \
|
||||
libspng0 \
|
||||
libtiff6 \
|
||||
libwebp7 \
|
||||
libwebpdemux2 \
|
||||
libwebpmux3 \
|
||||
libcgif0 \
|
||||
libexif12 \
|
||||
libheif1 \
|
||||
libimagequant0 \
|
||||
libjpeg62-turbo \
|
||||
liblcms2-2 \
|
||||
liborc-0.4-0 \
|
||||
libspng0 \
|
||||
libtiff6 \
|
||||
libwebp7 \
|
||||
libwebpdemux2 \
|
||||
libwebpmux3 \
|
||||
# ffmpeg components
|
||||
libdav1d6 \
|
||||
libmp3lame0 \
|
||||
libopencore-amrnb0 \
|
||||
libopencore-amrwb0 \
|
||||
libopus0 \
|
||||
libsnappy1v5 \
|
||||
libtheora0 \
|
||||
libvorbis0a \
|
||||
libvorbisenc2 \
|
||||
libvorbisfile3 \
|
||||
libvpx7 \
|
||||
libx264-164 \
|
||||
libx265-199 \
|
||||
libdav1d6 \
|
||||
libmp3lame0 \
|
||||
libopencore-amrnb0 \
|
||||
libopencore-amrwb0 \
|
||||
libopus0 \
|
||||
libsnappy1v5 \
|
||||
libtheora0 \
|
||||
libvorbis0a \
|
||||
libvorbisenc2 \
|
||||
libvorbisfile3 \
|
||||
libvpx7 \
|
||||
libx264-164 \
|
||||
libx265-199 \
|
||||
;
|
||||
|
||||
# Copy Mastodon sources into final layer
|
||||
|
@ -386,7 +389,7 @@ COPY --from=ffmpeg /usr/local/ffmpeg/lib /usr/local/lib
|
|||
|
||||
RUN \
|
||||
ldconfig; \
|
||||
# Smoketest media processors
|
||||
# Smoketest media processors
|
||||
vips -v; \
|
||||
ffmpeg -version; \
|
||||
ffprobe -version;
|
||||
|
@ -396,10 +399,10 @@ RUN \
|
|||
bundle exec bootsnap precompile --gemfile app/ lib/;
|
||||
|
||||
RUN \
|
||||
# Pre-create and chown system volume to Mastodon user
|
||||
# Pre-create and chown system volume to Mastodon user
|
||||
mkdir -p /opt/mastodon/public/system; \
|
||||
chown mastodon:mastodon /opt/mastodon/public/system; \
|
||||
# Set Mastodon user as owner of tmp folder
|
||||
# Set Mastodon user as owner of tmp folder
|
||||
chown -R mastodon:mastodon /opt/mastodon/tmp;
|
||||
|
||||
# Set the running user for resulting container
|
||||
|
|
14
Gemfile
14
Gemfile
|
@ -6,7 +6,7 @@ ruby '>= 3.2.0'
|
|||
gem 'propshaft'
|
||||
gem 'puma', '~> 6.3'
|
||||
gem 'rack', '~> 2.2.7'
|
||||
gem 'rails', '~> 7.1.1'
|
||||
gem 'rails', '~> 7.2.0'
|
||||
gem 'thor', '~> 1.2'
|
||||
|
||||
gem 'dotenv'
|
||||
|
@ -25,7 +25,7 @@ gem 'ruby-vips', '~> 2.2', require: false
|
|||
gem 'active_model_serializers', '~> 0.10'
|
||||
gem 'addressable', '~> 2.8'
|
||||
gem 'bootsnap', '~> 1.18.0', require: false
|
||||
gem 'browser', '< 6' # https://github.com/fnando/browser/issues/543
|
||||
gem 'browser'
|
||||
gem 'charlock_holmes', '~> 0.7.7'
|
||||
gem 'chewy', '~> 7.3'
|
||||
gem 'devise', '~> 4.9'
|
||||
|
@ -47,13 +47,14 @@ gem 'color_diff', '~> 0.1'
|
|||
gem 'csv', '~> 3.2'
|
||||
gem 'discard', '~> 1.2'
|
||||
gem 'doorkeeper', '~> 5.6'
|
||||
gem 'faraday-httpclient'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
gem 'hiredis', '~> 0.6'
|
||||
gem 'htmlentities', '~> 4.3'
|
||||
gem 'http', '~> 5.2.0'
|
||||
gem 'http_accept_language', '~> 2.1'
|
||||
gem 'httplog', '~> 1.7.0'
|
||||
gem 'httplog', '~> 1.7.0', require: false
|
||||
gem 'i18n'
|
||||
gem 'idn-ruby', require: 'idn'
|
||||
gem 'inline_svg'
|
||||
|
@ -62,6 +63,7 @@ gem 'kaminari', '~> 1.2'
|
|||
gem 'link_header', '~> 0.0'
|
||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||
gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar'
|
||||
gem 'mutex_m'
|
||||
gem 'nokogiri', '~> 1.15'
|
||||
gem 'oj', '~> 3.14'
|
||||
gem 'ox', '~> 2.14'
|
||||
|
@ -112,7 +114,7 @@ group :opentelemetry do
|
|||
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
|
||||
gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rack', '~> 0.25.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rails', '~> 0.32.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rails', '~> 0.33.0', require: false
|
||||
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
|
||||
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
|
||||
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
||||
|
@ -170,7 +172,7 @@ group :development do
|
|||
gem 'rubocop-rspec_rails', require: false
|
||||
|
||||
# Annotates modules with schema
|
||||
gem 'annotate', '~> 3.2'
|
||||
gem 'annotaterb', '~> 4.13'
|
||||
|
||||
# Enhanced error message pages for development
|
||||
gem 'better_errors', '~> 2.9'
|
||||
|
@ -220,7 +222,7 @@ gem 'concurrent-ruby', require: false
|
|||
gem 'connection_pool', require: false
|
||||
gem 'xorcist', '~> 1.1'
|
||||
|
||||
gem 'net-http', '~> 0.4.0'
|
||||
gem 'net-http', '~> 0.5.0'
|
||||
gem 'rubyzip', '~> 2.3'
|
||||
|
||||
gem 'hcaptcha', '~> 7.1'
|
||||
|
|
253
Gemfile.lock
253
Gemfile.lock
|
@ -10,51 +10,46 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.1.4.2)
|
||||
actionpack (= 7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
actioncable (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.1.4.2)
|
||||
actionpack (= 7.1.4.2)
|
||||
activejob (= 7.1.4.2)
|
||||
activerecord (= 7.1.4.2)
|
||||
activestorage (= 7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.1.4.2)
|
||||
actionpack (= 7.1.4.2)
|
||||
actionview (= 7.1.4.2)
|
||||
activejob (= 7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailbox (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.4.2)
|
||||
actionview (= 7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
actionpack (7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
rack (>= 2.2.4, < 3.2)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actiontext (7.1.4.2)
|
||||
actionpack (= 7.1.4.2)
|
||||
activerecord (= 7.1.4.2)
|
||||
activestorage (= 7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
useragent (~> 0.16)
|
||||
actiontext (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
actionview (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
|
@ -64,62 +59,63 @@ GEM
|
|||
activemodel (>= 4.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
activejob (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
activerecord (7.1.4.2)
|
||||
activemodel (= 7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
activemodel (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
activerecord (7.2.2)
|
||||
activemodel (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.1.4.2)
|
||||
actionpack (= 7.1.4.2)
|
||||
activejob (= 7.1.4.2)
|
||||
activerecord (= 7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
activestorage (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.1.4.2)
|
||||
activesupport (7.2.2)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
mutex_m
|
||||
tzinfo (~> 2.0)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
android_key_attestation (0.3.0)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
annotaterb (4.13.0)
|
||||
ast (2.4.2)
|
||||
attr_required (1.0.2)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.997.0)
|
||||
aws-sdk-core (3.211.0)
|
||||
aws-partitions (1.1012.0)
|
||||
aws-sdk-core (3.213.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.95.0)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.169.0)
|
||||
aws-sdk-s3 (1.173.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-blob (0.5.2)
|
||||
azure-blob (0.5.3)
|
||||
rexml
|
||||
base64 (0.2.0)
|
||||
bcp47_spec (0.2.1)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.0)
|
||||
better_errors (2.10.1)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
|
@ -133,7 +129,7 @@ GEM
|
|||
msgpack (~> 1.2)
|
||||
brakeman (6.2.2)
|
||||
racc
|
||||
browser (5.3.1)
|
||||
browser (6.1.0)
|
||||
brpoplpush-redis_script (0.1.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
redis (>= 1.0, < 6)
|
||||
|
@ -180,7 +176,7 @@ GEM
|
|||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.3.4)
|
||||
date (3.4.0)
|
||||
debug (1.9.2)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
|
@ -191,20 +187,20 @@ GEM
|
|||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (6.0.0)
|
||||
activesupport (~> 7.0)
|
||||
devise-two-factor (6.1.0)
|
||||
activesupport (>= 7.0, < 8.1)
|
||||
devise (~> 4.0)
|
||||
railties (~> 7.0)
|
||||
railties (>= 7.0, < 8.1)
|
||||
rotp (~> 6.0)
|
||||
devise_pam_authenticatable2 (9.2.0)
|
||||
devise (>= 4.0.0)
|
||||
rpam2 (~> 4.0)
|
||||
diff-lcs (1.5.1)
|
||||
discard (1.3.0)
|
||||
activerecord (>= 4.2, < 8)
|
||||
discard (1.4.0)
|
||||
activerecord (>= 4.2, < 9.0)
|
||||
docile (1.4.1)
|
||||
domain_name (0.6.20240107)
|
||||
doorkeeper (5.7.1)
|
||||
doorkeeper (5.8.0)
|
||||
railties (>= 5)
|
||||
dotenv (3.1.4)
|
||||
drb (2.2.1)
|
||||
|
@ -229,29 +225,14 @@ GEM
|
|||
fabrication (2.31.0)
|
||||
faker (3.5.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday (2.12.0)
|
||||
faraday-net_http (>= 2.0, < 3.4)
|
||||
json
|
||||
logger
|
||||
faraday-httpclient (2.0.1)
|
||||
httpclient (>= 2.2)
|
||||
faraday-net_http (3.3.0)
|
||||
net-http
|
||||
fast_blank (1.0.1)
|
||||
fastimage (2.3.1)
|
||||
ffi (1.17.0)
|
||||
|
@ -347,7 +328,7 @@ GEM
|
|||
azure-blob (~> 0.5.2)
|
||||
hashie (~> 5.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.4)
|
||||
json (2.8.1)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.15.3.1)
|
||||
activesupport (>= 4.2)
|
||||
|
@ -362,10 +343,10 @@ GEM
|
|||
rack (>= 2.2, < 4)
|
||||
rdf (~> 3.3)
|
||||
rexml (~> 3.2)
|
||||
json-ld-preloaded (3.3.0)
|
||||
json-ld-preloaded (3.3.1)
|
||||
json-ld (~> 3.3)
|
||||
rdf (~> 3.3)
|
||||
json-schema (5.0.1)
|
||||
json-schema (5.1.0)
|
||||
addressable (~> 2.8)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.7.1)
|
||||
|
@ -424,17 +405,16 @@ GEM
|
|||
mime-types (3.6.0)
|
||||
logger
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2024.1001)
|
||||
mime-types-data (3.2024.1105)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.25.1)
|
||||
msgpack (1.7.3)
|
||||
msgpack (1.7.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.2.0)
|
||||
net-http (0.4.1)
|
||||
net-http (0.5.0)
|
||||
uri
|
||||
net-imap (0.5.0)
|
||||
net-imap (0.5.1)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.19.0)
|
||||
|
@ -448,7 +428,7 @@ GEM
|
|||
nokogiri (1.16.7)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.6)
|
||||
oj (3.16.7)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
omniauth (2.1.2)
|
||||
|
@ -498,13 +478,13 @@ GEM
|
|||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-action_pack (0.9.0)
|
||||
opentelemetry-instrumentation-action_pack (0.10.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-rack (~> 0.21)
|
||||
opentelemetry-instrumentation-action_view (0.7.2)
|
||||
opentelemetry-instrumentation-action_view (0.7.3)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||
opentelemetry-instrumentation-active_support (~> 0.6)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-active_job (0.7.8)
|
||||
opentelemetry-api (~> 1.0)
|
||||
|
@ -547,10 +527,10 @@ GEM
|
|||
opentelemetry-instrumentation-rack (0.25.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-rails (0.32.0)
|
||||
opentelemetry-instrumentation-rails (0.33.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-action_mailer (~> 0.2.0)
|
||||
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
||||
opentelemetry-instrumentation-action_pack (~> 0.10.0)
|
||||
opentelemetry-instrumentation-action_view (~> 0.7.0)
|
||||
opentelemetry-instrumentation-active_job (~> 0.7.0)
|
||||
opentelemetry-instrumentation-active_record (~> 0.8.0)
|
||||
|
@ -572,10 +552,10 @@ GEM
|
|||
opentelemetry-semantic_conventions (1.10.1)
|
||||
opentelemetry-api (~> 1.0)
|
||||
orm_adapter (0.5.0)
|
||||
ostruct (0.6.0)
|
||||
ostruct (0.6.1)
|
||||
ox (2.14.18)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.5.0)
|
||||
parser (3.3.6.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
parslet (2.0.0)
|
||||
|
@ -597,7 +577,7 @@ GEM
|
|||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
railties (>= 7.0.0)
|
||||
psych (5.1.2)
|
||||
psych (5.2.0)
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.4.3)
|
||||
|
@ -629,20 +609,20 @@ GEM
|
|||
rackup (1.0.0)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (7.1.4.2)
|
||||
actioncable (= 7.1.4.2)
|
||||
actionmailbox (= 7.1.4.2)
|
||||
actionmailer (= 7.1.4.2)
|
||||
actionpack (= 7.1.4.2)
|
||||
actiontext (= 7.1.4.2)
|
||||
actionview (= 7.1.4.2)
|
||||
activejob (= 7.1.4.2)
|
||||
activemodel (= 7.1.4.2)
|
||||
activerecord (= 7.1.4.2)
|
||||
activestorage (= 7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
rails (7.2.2)
|
||||
actioncable (= 7.2.2)
|
||||
actionmailbox (= 7.2.2)
|
||||
actionmailer (= 7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
actiontext (= 7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activemodel (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.1.4.2)
|
||||
railties (= 7.2.2)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
|
@ -657,10 +637,10 @@ GEM
|
|||
rails-i18n (7.0.10)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
railties (7.1.4.2)
|
||||
actionpack (= 7.1.4.2)
|
||||
activesupport (= 7.1.4.2)
|
||||
irb
|
||||
railties (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
|
@ -682,7 +662,7 @@ GEM
|
|||
redlock (1.3.2)
|
||||
redis (>= 3.0.0, < 6.0)
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.10)
|
||||
reline (0.5.11)
|
||||
io-console (~> 0.5)
|
||||
request_store (1.6.0)
|
||||
rack (>= 1.4)
|
||||
|
@ -691,7 +671,7 @@ GEM
|
|||
railties (>= 5.2)
|
||||
rexml (3.3.9)
|
||||
rotp (6.3.0)
|
||||
rouge (4.4.0)
|
||||
rouge (4.5.1)
|
||||
rpam2 (4.0.2)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
|
@ -711,7 +691,7 @@ GEM
|
|||
rspec-mocks (3.13.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (7.0.1)
|
||||
rspec-rails (7.1.0)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
|
@ -760,7 +740,6 @@ GEM
|
|||
ruby-vips (2.2.2)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
rufus-scheduler (3.9.1)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
|
@ -772,6 +751,7 @@ GEM
|
|||
scenic (1.8.0)
|
||||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
securerandom (0.3.2)
|
||||
selenium-webdriver (4.26.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
|
@ -812,8 +792,8 @@ GEM
|
|||
stackprof (0.2.26)
|
||||
stoplight (4.1.0)
|
||||
redlock (~> 1.0)
|
||||
stringio (3.1.1)
|
||||
strong_migrations (2.0.2)
|
||||
stringio (3.1.2)
|
||||
strong_migrations (2.1.0)
|
||||
activerecord (>= 6.1)
|
||||
swd (1.3.0)
|
||||
activesupport (>= 3)
|
||||
|
@ -828,7 +808,7 @@ GEM
|
|||
test-prof (1.4.2)
|
||||
thor (1.3.2)
|
||||
tilt (2.4.0)
|
||||
timeout (0.4.1)
|
||||
timeout (0.4.2)
|
||||
tpm-key_attestation (0.12.1)
|
||||
bindata (~> 2.4)
|
||||
openssl (> 2.0)
|
||||
|
@ -855,6 +835,7 @@ GEM
|
|||
unf_ext (0.0.9.1)
|
||||
unicode-display_width (2.6.0)
|
||||
uri (0.13.1)
|
||||
useragent (0.16.10)
|
||||
validate_email (0.1.6)
|
||||
activemodel (>= 3.0)
|
||||
mail (>= 2.2.5)
|
||||
|
@ -884,7 +865,7 @@ GEM
|
|||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webrick (1.8.2)
|
||||
webrick (1.9.0)
|
||||
websocket (1.2.11)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
|
@ -901,14 +882,14 @@ PLATFORMS
|
|||
DEPENDENCIES
|
||||
active_model_serializers (~> 0.10)
|
||||
addressable (~> 2.8)
|
||||
annotate (~> 3.2)
|
||||
annotaterb (~> 4.13)
|
||||
aws-sdk-s3 (~> 1.123)
|
||||
better_errors (~> 2.9)
|
||||
binding_of_caller (~> 1.0)
|
||||
blurhash (~> 0.1)
|
||||
bootsnap (~> 1.18.0)
|
||||
brakeman (~> 6.0)
|
||||
browser (< 6)
|
||||
browser
|
||||
bundler-audit (~> 0.9)
|
||||
capybara (~> 3.39)
|
||||
charlock_holmes (~> 0.7.7)
|
||||
|
@ -930,6 +911,7 @@ DEPENDENCIES
|
|||
email_spec
|
||||
fabrication (~> 2.30)
|
||||
faker (~> 3.2)
|
||||
faraday-httpclient
|
||||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
flatware-rspec
|
||||
|
@ -962,7 +944,8 @@ DEPENDENCIES
|
|||
mario-redis-lock (~> 1.2)
|
||||
memory_profiler
|
||||
mime-types (~> 3.6.0)
|
||||
net-http (~> 0.4.0)
|
||||
mutex_m
|
||||
net-http (~> 0.5.0)
|
||||
net-ldap (~> 0.18)
|
||||
nokogiri (~> 1.15)
|
||||
oj (~> 3.14)
|
||||
|
@ -983,7 +966,7 @@ DEPENDENCIES
|
|||
opentelemetry-instrumentation-net_http (~> 0.22.4)
|
||||
opentelemetry-instrumentation-pg (~> 0.29.0)
|
||||
opentelemetry-instrumentation-rack (~> 0.25.0)
|
||||
opentelemetry-instrumentation-rails (~> 0.32.0)
|
||||
opentelemetry-instrumentation-rails (~> 0.33.0)
|
||||
opentelemetry-instrumentation-redis (~> 0.25.3)
|
||||
opentelemetry-instrumentation-sidekiq (~> 0.25.2)
|
||||
opentelemetry-sdk (~> 1.4)
|
||||
|
@ -1000,7 +983,7 @@ DEPENDENCIES
|
|||
rack-attack (~> 6.6)
|
||||
rack-cors (~> 2.0)
|
||||
rack-test (~> 2.1)
|
||||
rails (~> 7.1.1)
|
||||
rails (~> 7.2.0)
|
||||
rails-controller-testing (~> 1.0)
|
||||
rails-i18n (~> 7.0)
|
||||
rdf-normalize (~> 0.5)
|
||||
|
|
2
Rakefile
2
Rakefile
|
@ -3,6 +3,6 @@
|
|||
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
||||
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
||||
|
||||
require File.expand_path('config/application', __dir__)
|
||||
require_relative 'config/application'
|
||||
|
||||
Rails.application.load_tasks
|
||||
|
|
|
@ -5,7 +5,7 @@ module Admin
|
|||
def index
|
||||
authorize :email_domain_block, :index?
|
||||
|
||||
@email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page])
|
||||
@email_domain_blocks = EmailDomainBlock.parents.includes(:children).order(id: :desc).page(params[:page])
|
||||
@form = Form::EmailDomainBlockBatch.new
|
||||
end
|
||||
|
||||
|
@ -58,10 +58,7 @@ module Admin
|
|||
private
|
||||
|
||||
def set_resolved_records
|
||||
Resolv::DNS.open do |dns|
|
||||
dns.timeouts = 5
|
||||
@resolved_records = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a
|
||||
end
|
||||
@resolved_records = DomainResource.new(@email_domain_block.domain).mx
|
||||
end
|
||||
|
||||
def resource_params
|
||||
|
|
|
@ -5,6 +5,8 @@ module Admin
|
|||
before_action :set_instances, only: :index
|
||||
before_action :set_instance, except: :index
|
||||
|
||||
LOGS_LIMIT = 5
|
||||
|
||||
def index
|
||||
authorize :instance, :index?
|
||||
preload_delivery_failures!
|
||||
|
@ -13,7 +15,7 @@ module Admin
|
|||
def show
|
||||
authorize :instance, :show?
|
||||
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
||||
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(5)
|
||||
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT)
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
|
|
@ -32,7 +32,7 @@ module Admin
|
|||
|
||||
def deactivate_all
|
||||
authorize :invite, :deactivate_all?
|
||||
Invite.available.in_batches.update_all(expires_at: Time.now.utc)
|
||||
Invite.available.in_batches.touch_all(:expires_at)
|
||||
redirect_to admin_invites_path
|
||||
end
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ module Admin
|
|||
@relay = Relay.new(resource_params)
|
||||
|
||||
if @relay.save
|
||||
log_action :create, @relay
|
||||
@relay.enable!
|
||||
redirect_to admin_relays_path
|
||||
else
|
||||
|
@ -31,18 +32,21 @@ module Admin
|
|||
def destroy
|
||||
authorize :relay, :update?
|
||||
@relay.destroy
|
||||
log_action :destroy, @relay
|
||||
redirect_to admin_relays_path
|
||||
end
|
||||
|
||||
def enable
|
||||
authorize :relay, :update?
|
||||
@relay.enable!
|
||||
log_action :enable, @relay
|
||||
redirect_to admin_relays_path
|
||||
end
|
||||
|
||||
def disable
|
||||
authorize :relay, :update?
|
||||
@relay.disable!
|
||||
log_action :disable, @relay
|
||||
redirect_to admin_relays_path
|
||||
end
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ module Admin
|
|||
|
||||
def show
|
||||
authorize [:admin, @status], :show?
|
||||
|
||||
@status_batch_action = Admin::StatusBatchAction.new
|
||||
end
|
||||
|
||||
def batch
|
||||
|
|
|
@ -12,7 +12,7 @@ class Api::V1::Accounts::FamiliarFollowersController < Api::BaseController
|
|||
private
|
||||
|
||||
def set_accounts
|
||||
@accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections')
|
||||
@accounts = Account.without_suspended.where(id: account_ids).select(:id, :hide_collections)
|
||||
end
|
||||
|
||||
def familiar_followers
|
||||
|
|
|
@ -17,6 +17,17 @@ class Api::V1::AnnualReportsController < Api::BaseController
|
|||
relationships: @relationships
|
||||
end
|
||||
|
||||
def show
|
||||
with_read_replica do
|
||||
@presenter = AnnualReportsPresenter.new([@annual_report])
|
||||
@relationships = StatusRelationshipsPresenter.new(@presenter.statuses, current_account.id)
|
||||
end
|
||||
|
||||
render json: @presenter,
|
||||
serializer: REST::AnnualReportsSerializer,
|
||||
relationships: @relationships
|
||||
end
|
||||
|
||||
def read
|
||||
@annual_report.view!
|
||||
render_empty
|
||||
|
|
|
@ -5,6 +5,8 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
|||
before_action :require_user!
|
||||
before_action :set_recently_used_tags, only: :index
|
||||
|
||||
RECENT_TAGS_LIMIT = 10
|
||||
|
||||
def index
|
||||
render json: @recently_used_tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@recently_used_tags, current_user&.account_id)
|
||||
end
|
||||
|
@ -12,6 +14,6 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
|||
private
|
||||
|
||||
def set_recently_used_tags
|
||||
@recently_used_tags = Tag.suggestions_for_account(current_account).limit(10)
|
||||
@recently_used_tags = Tag.suggestions_for_account(current_account).limit(RECENT_TAGS_LIMIT)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,17 +15,12 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def create
|
||||
ApplicationRecord.transaction do
|
||||
list_accounts.each do |account|
|
||||
@list.accounts << account
|
||||
end
|
||||
end
|
||||
|
||||
AddAccountsToListService.new.call(@list, Account.find(account_ids))
|
||||
render_empty
|
||||
end
|
||||
|
||||
def destroy
|
||||
ListAccount.where(list: @list, account_id: account_ids).destroy_all
|
||||
RemoveAccountsFromListService.new.call(@list, Account.where(id: account_ids))
|
||||
render_empty
|
||||
end
|
||||
|
||||
|
@ -43,10 +38,6 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
|||
end
|
||||
end
|
||||
|
||||
def list_accounts
|
||||
Account.find(account_ids)
|
||||
end
|
||||
|
||||
def account_ids
|
||||
Array(resource_params[:account_ids])
|
||||
end
|
||||
|
|
|
@ -7,7 +7,6 @@ module WebAppControllerConcern
|
|||
vary_by 'Accept, Accept-Language, Cookie'
|
||||
|
||||
before_action :redirect_unauthenticated_to_permalinks!
|
||||
before_action :set_app_body_class
|
||||
|
||||
content_security_policy do |p|
|
||||
policy = ContentSecurityPolicy.new
|
||||
|
@ -24,10 +23,6 @@ module WebAppControllerConcern
|
|||
!(ENV['ONE_CLICK_SSO_LOGIN'] == 'true' && ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil?
|
||||
end
|
||||
|
||||
def set_app_body_class
|
||||
@body_classes = 'app-body'
|
||||
end
|
||||
|
||||
def redirect_unauthenticated_to_permalinks!
|
||||
return if user_signed_in? && current_account.moved_to_account_id.nil?
|
||||
|
||||
|
|
|
@ -35,12 +35,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||
end
|
||||
|
||||
def set_last_used_at_by_app
|
||||
@last_used_at_by_app = Doorkeeper::AccessToken
|
||||
.select('DISTINCT ON (application_id) application_id, last_used_at')
|
||||
.where(resource_owner_id: current_resource_owner.id)
|
||||
.where.not(last_used_at: nil)
|
||||
.order(application_id: :desc, last_used_at: :desc)
|
||||
.pluck(:application_id, :last_used_at)
|
||||
.to_h
|
||||
@last_used_at_by_app = current_resource_owner.applications_last_used
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
|||
before_action :set_featured_tag, except: [:index, :create]
|
||||
before_action :set_recently_used_tags, only: :index
|
||||
|
||||
RECENT_TAGS_LIMIT = 10
|
||||
|
||||
def index
|
||||
@featured_tag = FeaturedTag.new
|
||||
end
|
||||
|
@ -38,7 +40,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
|||
end
|
||||
|
||||
def set_recently_used_tags
|
||||
@recently_used_tags = Tag.suggestions_for_account(current_account).limit(10)
|
||||
@recently_used_tags = Tag.suggestions_for_account(current_account).limit(RECENT_TAGS_LIMIT)
|
||||
end
|
||||
|
||||
def featured_tag_params
|
||||
|
|
|
@ -24,6 +24,8 @@ class Settings::ImportsController < Settings::BaseController
|
|||
lists: false,
|
||||
}.freeze
|
||||
|
||||
RECENT_IMPORTS_LIMIT = 10
|
||||
|
||||
def index
|
||||
@import = Form::Import.new(current_account: current_account)
|
||||
end
|
||||
|
@ -96,6 +98,6 @@ class Settings::ImportsController < Settings::BaseController
|
|||
end
|
||||
|
||||
def set_recent_imports
|
||||
@recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(10)
|
||||
@recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(RECENT_IMPORTS_LIMIT)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,12 +12,12 @@ module Admin::AccountModerationNotesHelper
|
|||
)
|
||||
end
|
||||
|
||||
def admin_account_inline_link_to(account)
|
||||
def admin_account_inline_link_to(account, path: nil)
|
||||
return if account.nil?
|
||||
|
||||
link_to(
|
||||
account_inline_text(account),
|
||||
admin_account_path(account.id),
|
||||
path || admin_account_path(account.id),
|
||||
class: class_names('inline-name-tag', suspended: suspended_account?(account)),
|
||||
title: account.acct
|
||||
)
|
||||
|
|
|
@ -33,6 +33,8 @@ module Admin::ActionLogsHelper
|
|||
else
|
||||
I18n.t('admin.action_logs.deleted_account')
|
||||
end
|
||||
when 'Relay'
|
||||
link_to log.human_identifier, admin_relays_path
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ module ApplicationHelper
|
|||
|
||||
def html_title
|
||||
safe_join(
|
||||
[content_for(:page_title).to_s.chomp, title]
|
||||
[content_for(:page_title), title]
|
||||
.compact_blank,
|
||||
' - '
|
||||
)
|
||||
|
|
|
@ -16,6 +16,6 @@ module RegistrationHelper
|
|||
end
|
||||
|
||||
def ip_blocked?(remote_ip)
|
||||
IpBlock.where(severity: :sign_up_block).exists?(['ip >>= ?', remote_ip.to_s])
|
||||
IpBlock.severity_sign_up_block.containing(remote_ip.to_s).exists?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SelfDestructHelper
|
||||
VERIFY_PURPOSE = 'self-destruct'
|
||||
|
||||
def self.self_destruct?
|
||||
value = ENV.fetch('SELF_DESTRUCT', nil)
|
||||
value.present? && Rails.application.message_verifier('self-destruct').verify(value) == ENV['LOCAL_DOMAIN']
|
||||
value = Rails.configuration.x.mastodon.self_destruct_value
|
||||
value.present? && Rails.application.message_verifier(VERIFY_PURPOSE).verify(value) == ENV['LOCAL_DOMAIN']
|
||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||
false
|
||||
end
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module StatusesHelper
|
||||
EMBEDDED_CONTROLLER = 'statuses'
|
||||
EMBEDDED_ACTION = 'embed'
|
||||
|
||||
VISIBLITY_ICONS = {
|
||||
public: 'globe',
|
||||
unlisted: 'lock_open',
|
||||
|
@ -60,18 +57,10 @@ module StatusesHelper
|
|||
components.compact_blank.join("\n\n")
|
||||
end
|
||||
|
||||
def stream_link_target
|
||||
embedded_view? ? '_blank' : nil
|
||||
end
|
||||
|
||||
def visibility_icon(status)
|
||||
VISIBLITY_ICONS[status.visibility.to_sym]
|
||||
end
|
||||
|
||||
def embedded_view?
|
||||
params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION
|
||||
end
|
||||
|
||||
def prefers_autoplay?
|
||||
ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif
|
||||
end
|
||||
|
|
BIN
app/javascript/images/archetypes/booster.png
Executable file
BIN
app/javascript/images/archetypes/booster.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 620 KiB |
BIN
app/javascript/images/archetypes/lurker.png
Executable file
BIN
app/javascript/images/archetypes/lurker.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 1 MiB |
BIN
app/javascript/images/archetypes/oracle.png
Executable file
BIN
app/javascript/images/archetypes/oracle.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
BIN
app/javascript/images/archetypes/pollster.png
Executable file
BIN
app/javascript/images/archetypes/pollster.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 710 KiB |
BIN
app/javascript/images/archetypes/replier.png
Executable file
BIN
app/javascript/images/archetypes/replier.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 786 KiB |
|
@ -1,8 +1,5 @@
|
|||
import api from '../api';
|
||||
|
||||
import { showAlertForError } from './alerts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
|
||||
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
||||
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
||||
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
|
||||
|
@ -11,45 +8,10 @@ export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
|
|||
export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
|
||||
export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
|
||||
|
||||
export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
|
||||
export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET';
|
||||
export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP';
|
||||
|
||||
export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
|
||||
export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
|
||||
export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL';
|
||||
|
||||
export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
|
||||
export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
|
||||
export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL';
|
||||
|
||||
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
|
||||
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
|
||||
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
|
||||
|
||||
export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
|
||||
export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
|
||||
export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL';
|
||||
|
||||
export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
|
||||
export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY';
|
||||
export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
|
||||
|
||||
export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
|
||||
export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
|
||||
export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL';
|
||||
|
||||
export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
|
||||
export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
|
||||
export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
|
||||
|
||||
export const LIST_ADDER_RESET = 'LIST_ADDER_RESET';
|
||||
export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP';
|
||||
|
||||
export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST';
|
||||
export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS';
|
||||
export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL';
|
||||
|
||||
export const fetchList = id => (dispatch, getState) => {
|
||||
if (getState().getIn(['lists', id])) {
|
||||
return;
|
||||
|
@ -100,89 +62,6 @@ export const fetchListsFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const submitListEditor = shouldReset => (dispatch, getState) => {
|
||||
const listId = getState().getIn(['listEditor', 'listId']);
|
||||
const title = getState().getIn(['listEditor', 'title']);
|
||||
|
||||
if (listId === null) {
|
||||
dispatch(createList(title, shouldReset));
|
||||
} else {
|
||||
dispatch(updateList(listId, title, shouldReset));
|
||||
}
|
||||
};
|
||||
|
||||
export const setupListEditor = listId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: LIST_EDITOR_SETUP,
|
||||
list: getState().getIn(['lists', listId]),
|
||||
});
|
||||
|
||||
dispatch(fetchListAccounts(listId));
|
||||
};
|
||||
|
||||
export const changeListEditorTitle = value => ({
|
||||
type: LIST_EDITOR_TITLE_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const createList = (title, shouldReset) => (dispatch) => {
|
||||
dispatch(createListRequest());
|
||||
|
||||
api().post('/api/v1/lists', { title }).then(({ data }) => {
|
||||
dispatch(createListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetListEditor());
|
||||
}
|
||||
}).catch(err => dispatch(createListFail(err)));
|
||||
};
|
||||
|
||||
export const createListRequest = () => ({
|
||||
type: LIST_CREATE_REQUEST,
|
||||
});
|
||||
|
||||
export const createListSuccess = list => ({
|
||||
type: LIST_CREATE_SUCCESS,
|
||||
list,
|
||||
});
|
||||
|
||||
export const createListFail = error => ({
|
||||
type: LIST_CREATE_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch) => {
|
||||
dispatch(updateListRequest(id));
|
||||
|
||||
api().put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
|
||||
dispatch(updateListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetListEditor());
|
||||
}
|
||||
}).catch(err => dispatch(updateListFail(id, err)));
|
||||
};
|
||||
|
||||
export const updateListRequest = id => ({
|
||||
type: LIST_UPDATE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const updateListSuccess = list => ({
|
||||
type: LIST_UPDATE_SUCCESS,
|
||||
list,
|
||||
});
|
||||
|
||||
export const updateListFail = (id, error) => ({
|
||||
type: LIST_UPDATE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetListEditor = () => ({
|
||||
type: LIST_EDITOR_RESET,
|
||||
});
|
||||
|
||||
export const deleteList = id => (dispatch) => {
|
||||
dispatch(deleteListRequest(id));
|
||||
|
||||
|
@ -206,167 +85,3 @@ export const deleteListFail = (id, error) => ({
|
|||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchListAccounts = listId => (dispatch) => {
|
||||
dispatch(fetchListAccountsRequest(listId));
|
||||
|
||||
api().get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchListAccountsSuccess(listId, data));
|
||||
}).catch(err => dispatch(fetchListAccountsFail(listId, err)));
|
||||
};
|
||||
|
||||
export const fetchListAccountsRequest = id => ({
|
||||
type: LIST_ACCOUNTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchListAccountsSuccess = (id, accounts, next) => ({
|
||||
type: LIST_ACCOUNTS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
export const fetchListAccountsFail = (id, error) => ({
|
||||
type: LIST_ACCOUNTS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchListSuggestions = q => (dispatch) => {
|
||||
const params = {
|
||||
q,
|
||||
resolve: false,
|
||||
limit: 4,
|
||||
following: true,
|
||||
};
|
||||
|
||||
api().get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchListSuggestionsReady(q, data));
|
||||
}).catch(error => dispatch(showAlertForError(error)));
|
||||
};
|
||||
|
||||
export const fetchListSuggestionsReady = (query, accounts) => ({
|
||||
type: LIST_EDITOR_SUGGESTIONS_READY,
|
||||
query,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const clearListSuggestions = () => ({
|
||||
type: LIST_EDITOR_SUGGESTIONS_CLEAR,
|
||||
});
|
||||
|
||||
export const changeListSuggestions = value => ({
|
||||
type: LIST_EDITOR_SUGGESTIONS_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const addToListEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
|
||||
};
|
||||
|
||||
export const addToList = (listId, accountId) => (dispatch) => {
|
||||
dispatch(addToListRequest(listId, accountId));
|
||||
|
||||
api().post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
|
||||
.then(() => dispatch(addToListSuccess(listId, accountId)))
|
||||
.catch(err => dispatch(addToListFail(listId, accountId, err)));
|
||||
};
|
||||
|
||||
export const addToListRequest = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_ADD_REQUEST,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addToListSuccess = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_ADD_SUCCESS,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addToListFail = (listId, accountId, error) => ({
|
||||
type: LIST_EDITOR_ADD_FAIL,
|
||||
listId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeFromListEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
|
||||
};
|
||||
|
||||
export const removeFromList = (listId, accountId) => (dispatch) => {
|
||||
dispatch(removeFromListRequest(listId, accountId));
|
||||
|
||||
api().delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
|
||||
.then(() => dispatch(removeFromListSuccess(listId, accountId)))
|
||||
.catch(err => dispatch(removeFromListFail(listId, accountId, err)));
|
||||
};
|
||||
|
||||
export const removeFromListRequest = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_REMOVE_REQUEST,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeFromListSuccess = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_REMOVE_SUCCESS,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeFromListFail = (listId, accountId, error) => ({
|
||||
type: LIST_EDITOR_REMOVE_FAIL,
|
||||
listId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetListAdder = () => ({
|
||||
type: LIST_ADDER_RESET,
|
||||
});
|
||||
|
||||
export const setupListAdder = accountId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: LIST_ADDER_SETUP,
|
||||
account: getState().getIn(['accounts', accountId]),
|
||||
});
|
||||
dispatch(fetchLists());
|
||||
dispatch(fetchAccountLists(accountId));
|
||||
};
|
||||
|
||||
export const fetchAccountLists = accountId => (dispatch) => {
|
||||
dispatch(fetchAccountListsRequest(accountId));
|
||||
|
||||
api().get(`/api/v1/accounts/${accountId}/lists`)
|
||||
.then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
|
||||
.catch(err => dispatch(fetchAccountListsFail(accountId, err)));
|
||||
};
|
||||
|
||||
export const fetchAccountListsRequest = id => ({
|
||||
type:LIST_ADDER_LISTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAccountListsSuccess = (id, lists) => ({
|
||||
type: LIST_ADDER_LISTS_FETCH_SUCCESS,
|
||||
id,
|
||||
lists,
|
||||
});
|
||||
|
||||
export const fetchAccountListsFail = (id, err) => ({
|
||||
type: LIST_ADDER_LISTS_FETCH_FAIL,
|
||||
id,
|
||||
err,
|
||||
});
|
||||
|
||||
export const addToListAdder = listId => (dispatch, getState) => {
|
||||
dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export const removeFromListAdder = listId => (dispatch, getState) => {
|
||||
dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId'])));
|
||||
};
|
||||
|
|
13
app/javascript/mastodon/actions/lists_typed.ts
Normal file
13
app/javascript/mastodon/actions/lists_typed.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { apiCreate, apiUpdate } from 'mastodon/api/lists';
|
||||
import type { List } from 'mastodon/models/list';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
export const createList = createDataLoadingThunk(
|
||||
'list/create',
|
||||
(list: Partial<List>) => apiCreate(list),
|
||||
);
|
||||
|
||||
export const updateList = createDataLoadingThunk(
|
||||
'list/update',
|
||||
(list: Partial<List>) => apiUpdate(list),
|
||||
);
|
|
@ -141,6 +141,9 @@ export const pollRecentNotifications = createDataLoadingThunk(
|
|||
|
||||
return { notifications };
|
||||
},
|
||||
{
|
||||
useLoadingBar: false,
|
||||
},
|
||||
);
|
||||
|
||||
export const processNewNotificationForGroups = createAppAsyncThunk(
|
||||
|
|
|
@ -68,6 +68,7 @@ export async function apiRequest<ApiResponse = unknown>(
|
|||
method: Method,
|
||||
url: string,
|
||||
args: {
|
||||
signal?: AbortSignal;
|
||||
params?: RequestParamsOrData;
|
||||
data?: RequestParamsOrData;
|
||||
timeout?: number;
|
||||
|
|
32
app/javascript/mastodon/api/lists.ts
Normal file
32
app/javascript/mastodon/api/lists.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {
|
||||
apiRequestPost,
|
||||
apiRequestPut,
|
||||
apiRequestGet,
|
||||
apiRequestDelete,
|
||||
} from 'mastodon/api';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type { ApiListJSON } from 'mastodon/api_types/lists';
|
||||
|
||||
export const apiCreate = (list: Partial<ApiListJSON>) =>
|
||||
apiRequestPost<ApiListJSON>('v1/lists', list);
|
||||
|
||||
export const apiUpdate = (list: Partial<ApiListJSON>) =>
|
||||
apiRequestPut<ApiListJSON>(`v1/lists/${list.id}`, list);
|
||||
|
||||
export const apiGetAccounts = (listId: string) =>
|
||||
apiRequestGet<ApiAccountJSON[]>(`v1/lists/${listId}/accounts`, {
|
||||
limit: 0,
|
||||
});
|
||||
|
||||
export const apiGetAccountLists = (accountId: string) =>
|
||||
apiRequestGet<ApiListJSON[]>(`v1/accounts/${accountId}/lists`);
|
||||
|
||||
export const apiAddAccountToList = (listId: string, accountId: string) =>
|
||||
apiRequestPost(`v1/lists/${listId}/accounts`, {
|
||||
account_ids: [accountId],
|
||||
});
|
||||
|
||||
export const apiRemoveAccountFromList = (listId: string, accountId: string) =>
|
||||
apiRequestDelete(`v1/lists/${listId}/accounts`, {
|
||||
account_ids: [accountId],
|
||||
});
|
10
app/javascript/mastodon/api_types/lists.ts
Normal file
10
app/javascript/mastodon/api_types/lists.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
// See app/serializers/rest/list_serializer.rb
|
||||
|
||||
export type RepliesPolicyType = 'list' | 'followed' | 'none';
|
||||
|
||||
export interface ApiListJSON {
|
||||
id: string;
|
||||
title: string;
|
||||
exclusive: boolean;
|
||||
replies_policy: RepliesPolicyType;
|
||||
}
|
|
@ -20,6 +20,7 @@ export const allNotificationTypes = [
|
|||
'admin.report',
|
||||
'moderation_warning',
|
||||
'severed_relationships',
|
||||
'annual_report',
|
||||
];
|
||||
|
||||
export type NotificationWithStatusType =
|
||||
|
@ -37,7 +38,8 @@ export type NotificationType =
|
|||
| 'moderation_warning'
|
||||
| 'severed_relationships'
|
||||
| 'admin.sign_up'
|
||||
| 'admin.report';
|
||||
| 'admin.report'
|
||||
| 'annual_report';
|
||||
|
||||
export interface BaseNotificationJSON {
|
||||
id: string;
|
||||
|
@ -130,6 +132,15 @@ interface AccountRelationshipSeveranceNotificationJSON
|
|||
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||
}
|
||||
|
||||
export interface ApiAnnualReportEventJSON {
|
||||
year: string;
|
||||
}
|
||||
|
||||
interface AnnualReportNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
type: 'annual_report';
|
||||
annual_report: ApiAnnualReportEventJSON;
|
||||
}
|
||||
|
||||
export type ApiNotificationJSON =
|
||||
| SimpleNotificationJSON
|
||||
| ReportNotificationJSON
|
||||
|
@ -142,7 +153,8 @@ export type ApiNotificationGroupJSON =
|
|||
| ReportNotificationGroupJSON
|
||||
| AccountRelationshipSeveranceNotificationGroupJSON
|
||||
| NotificationGroupWithStatusJSON
|
||||
| ModerationWarningNotificationGroupJSON;
|
||||
| ModerationWarningNotificationGroupJSON
|
||||
| AnnualReportNotificationGroupJSON;
|
||||
|
||||
export interface ApiNotificationGroupsResultJSON {
|
||||
accounts: ApiAccountJSON[];
|
||||
|
|
|
@ -7,11 +7,11 @@ import { Icon } from './icon';
|
|||
|
||||
interface Props {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
indeterminate: boolean;
|
||||
name: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
label: React.ReactNode;
|
||||
checked?: boolean;
|
||||
indeterminate?: boolean;
|
||||
name?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CheckBox: React.FC<Props> = ({
|
||||
|
@ -30,6 +30,7 @@ export const CheckBox: React.FC<Props> = ({
|
|||
value={value}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
readOnly={!onChange}
|
||||
/>
|
||||
|
||||
<span
|
||||
|
@ -42,7 +43,7 @@ export const CheckBox: React.FC<Props> = ({
|
|||
)}
|
||||
</span>
|
||||
|
||||
<span>{label}</span>
|
||||
{label && <span>{label}</span>}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -97,12 +97,12 @@ class Item extends PureComponent {
|
|||
height = 50;
|
||||
}
|
||||
|
||||
if (attachment.get('description')?.length > 0) {
|
||||
badges.push(<AltTextBadge key='alt' description={attachment.get('description')} />);
|
||||
}
|
||||
|
||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||
|
||||
if (description?.length > 0) {
|
||||
badges.push(<AltTextBadge key='alt' description={description} />);
|
||||
}
|
||||
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
||||
|
|
|
@ -13,11 +13,14 @@ class ModalRoot extends PureComponent {
|
|||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
backgroundColor: PropTypes.shape({
|
||||
r: PropTypes.number,
|
||||
g: PropTypes.number,
|
||||
b: PropTypes.number,
|
||||
}),
|
||||
backgroundColor: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
r: PropTypes.number,
|
||||
g: PropTypes.number,
|
||||
b: PropTypes.number,
|
||||
}),
|
||||
]),
|
||||
ignoreFocus: PropTypes.bool,
|
||||
...WithOptionalRouterPropTypes,
|
||||
};
|
||||
|
@ -141,14 +144,17 @@ class ModalRoot extends PureComponent {
|
|||
|
||||
let backgroundColor = null;
|
||||
|
||||
if (this.props.backgroundColor) {
|
||||
backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
|
||||
if (this.props.backgroundColor && typeof this.props.backgroundColor === 'string') {
|
||||
backgroundColor = this.props.backgroundColor;
|
||||
} else if (this.props.backgroundColor) {
|
||||
const darkenedColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
|
||||
backgroundColor = `rgb(${darkenedColor.r}, ${darkenedColor.g}, ${darkenedColor.b})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='modal-root' ref={this.setRef}>
|
||||
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
|
||||
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.9)` : null }} />
|
||||
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor }} />
|
||||
<div role='dialog' className='modal-root__container'>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -80,6 +80,7 @@ class ScrollableList extends PureComponent {
|
|||
children: PropTypes.node,
|
||||
bindToDocument: PropTypes.bool,
|
||||
preventScroll: PropTypes.bool,
|
||||
footer: PropTypes.node,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -324,7 +325,7 @@ class ScrollableList extends PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
|
||||
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
|
||||
const { fullscreen } = this.state;
|
||||
const childrenCount = Children.count(children);
|
||||
|
||||
|
@ -342,11 +343,13 @@ class ScrollableList extends PureComponent {
|
|||
<div className='scrollable__append'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
|
||||
scrollableArea = (
|
||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||
<div role='feed' className='item-list'>
|
||||
{prepend}
|
||||
|
||||
|
@ -375,6 +378,8 @@ class ScrollableList extends PureComponent {
|
|||
|
||||
{!hasMore && append}
|
||||
</div>
|
||||
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -385,6 +390,8 @@ class ScrollableList extends PureComponent {
|
|||
<div className='empty-column-indicator'>
|
||||
{emptyMessage}
|
||||
</div>
|
||||
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
|||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
@ -164,32 +166,18 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleClick = e => {
|
||||
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.handleHotkeyOpen(e);
|
||||
};
|
||||
|
||||
handleMouseUp = e => {
|
||||
// Only handle clicks on the empty space above the content
|
||||
|
||||
if (e.target !== e.currentTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
this.handleHotkeyOpen();
|
||||
};
|
||||
|
||||
handlePrependAccountClick = e => {
|
||||
this.handleAccountClick(e, false);
|
||||
};
|
||||
|
||||
handleAccountClick = (e, proper = true) => {
|
||||
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
this._openProfile(proper);
|
||||
this.handleClick(e);
|
||||
};
|
||||
|
||||
handleExpandedToggle = () => {
|
||||
|
@ -287,7 +275,7 @@ class Status extends ImmutablePureComponent {
|
|||
this.props.onMention(this._properStatus().get('account'));
|
||||
};
|
||||
|
||||
handleHotkeyOpen = () => {
|
||||
handleHotkeyOpen = (e) => {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick();
|
||||
return;
|
||||
|
@ -300,7 +288,13 @@ class Status extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
||||
const path = `/@${status.getIn(['account', 'acct'])}/${status.get('id')}`;
|
||||
|
||||
if (e?.button === 0 && !(e?.ctrlKey || e?.metaKey)) {
|
||||
history.push(path);
|
||||
} else if (e?.button === 1 || (e?.button === 0 && (e?.ctrlKey || e?.metaKey))) {
|
||||
window.open(path, '_blank', 'noreferrer noopener');
|
||||
}
|
||||
};
|
||||
|
||||
handleHotkeyOpenProfile = () => {
|
||||
|
@ -394,17 +388,6 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
let media, statusAvatar, prepend, rebloggedByText;
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={unfocusable ? null : 0}>
|
||||
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
||||
<span>{status.get('content')}</span>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
const connectUp = previousId && previousId === status.get('in_reply_to_id');
|
||||
const connectToRoot = rootId && rootId === status.get('in_reply_to_id');
|
||||
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
|
||||
|
@ -423,7 +406,7 @@ class Status extends ImmutablePureComponent {
|
|||
prepend = (
|
||||
<div className='status__prepend'>
|
||||
<div className='status__prepend__icon'><Icon id='retweet' icon={RepeatIcon} /></div>
|
||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <Link data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} to={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></Link> }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -444,6 +427,20 @@ class Status extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const expanded = (!matchedFilters || this.state.showDespiteFilter) && (!status.get('hidden') || status.get('spoiler_text').length === 0);
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={unfocusable ? null : 0}>
|
||||
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
||||
{status.get('spoiler_text').length > 0 && (<span>{status.get('spoiler_text')}</span>)}
|
||||
{expanded && <span>{status.get('content')}</span>}
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
if (pictureInPicture.get('inUse')) {
|
||||
media = <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
|
@ -538,7 +535,6 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
const expanded = (!matchedFilters || this.state.showDespiteFilter) && (!status.get('hidden') || status.get('spoiler_text').length === 0);
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||
|
@ -548,20 +544,19 @@ class Status extends ImmutablePureComponent {
|
|||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
|
||||
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
||||
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div onClick={this.handleClick} className='status__info'>
|
||||
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||
<div onMouseUp={this.handleMouseUp} className='status__info'>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time'>
|
||||
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
|
||||
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} data-hover-card-account={status.getIn(['account', 'id'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} data-hover-card-account={status.getIn(['account', 'id'])} className='status__display-name'>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar}
|
||||
</div>
|
||||
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />}
|
||||
|
|
|
@ -204,8 +204,8 @@ class StatusContent extends PureComponent {
|
|||
element = element.parentNode;
|
||||
}
|
||||
|
||||
if (deltaX + deltaY < 5 && e.button === 0 && this.props.onClick) {
|
||||
this.props.onClick();
|
||||
if (deltaX + deltaY < 5 && (e.button === 0 || e.button === 1) && this.props.onClick) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
|
||||
this.startXY = null;
|
||||
|
|
|
@ -15,6 +15,13 @@ const mapStateToProps = state => ({
|
|||
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {any} dispatch
|
||||
* @param {Object} root0
|
||||
* @param {any} [root0.status]
|
||||
* @param {any} root0.items
|
||||
* @param {any} [root0.scrollKey]
|
||||
*/
|
||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
onOpen(id, onItemClick, keyboard) {
|
||||
if (status) {
|
||||
|
|
69
app/javascript/mastodon/features/annual_report/archetype.tsx
Normal file
69
app/javascript/mastodon/features/annual_report/archetype.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import booster from '@/images/archetypes/booster.png';
|
||||
import lurker from '@/images/archetypes/lurker.png';
|
||||
import oracle from '@/images/archetypes/oracle.png';
|
||||
import pollster from '@/images/archetypes/pollster.png';
|
||||
import replier from '@/images/archetypes/replier.png';
|
||||
import type { Archetype as ArchetypeData } from 'mastodon/models/annual_report';
|
||||
|
||||
export const Archetype: React.FC<{
|
||||
data: ArchetypeData;
|
||||
}> = ({ data }) => {
|
||||
let illustration, label;
|
||||
|
||||
switch (data) {
|
||||
case 'booster':
|
||||
illustration = booster;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.booster'
|
||||
defaultMessage='The cool-hunter'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'replier':
|
||||
illustration = replier;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.replier'
|
||||
defaultMessage='The social butterfly'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'pollster':
|
||||
illustration = pollster;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.pollster'
|
||||
defaultMessage='The pollster'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'lurker':
|
||||
illustration = lurker;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.lurker'
|
||||
defaultMessage='The lurker'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'oracle':
|
||||
illustration = oracle;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.archetype.oracle'
|
||||
defaultMessage='The oracle'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__archetype'>
|
||||
<div className='annual-report__summary__archetype__label'>{label}</div>
|
||||
<img src={illustration} alt='' />
|
||||
</div>
|
||||
);
|
||||
};
|
69
app/javascript/mastodon/features/annual_report/followers.tsx
Normal file
69
app/javascript/mastodon/features/annual_report/followers.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
|
||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import type { TimeSeriesMonth } from 'mastodon/models/annual_report';
|
||||
|
||||
export const Followers: React.FC<{
|
||||
data: TimeSeriesMonth[];
|
||||
total?: number;
|
||||
}> = ({ data, total }) => {
|
||||
const change = data.reduce((sum, item) => sum + item.followers, 0);
|
||||
|
||||
const cumulativeGraph = data.reduce(
|
||||
(newData, item) => [
|
||||
...newData,
|
||||
item.followers + (newData[newData.length - 1] ?? 0),
|
||||
],
|
||||
[0],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__followers'>
|
||||
<Sparklines data={cumulativeGraph} margin={0}>
|
||||
<svg>
|
||||
<defs>
|
||||
<linearGradient id='gradient' x1='0%' y1='0%' x2='0%' y2='100%'>
|
||||
<stop
|
||||
offset='0%'
|
||||
stopColor='var(--sparkline-gradient-top)'
|
||||
stopOpacity='1'
|
||||
/>
|
||||
<stop
|
||||
offset='100%'
|
||||
stopColor='var(--sparkline-gradient-bottom)'
|
||||
stopOpacity='0'
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
|
||||
<div className='annual-report__summary__followers__foreground'>
|
||||
<div className='annual-report__summary__followers__number'>
|
||||
{change > -1 ? '+' : '-'}
|
||||
<FormattedNumber value={change} />
|
||||
</div>
|
||||
|
||||
<div className='annual-report__summary__followers__label'>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.followers.followers'
|
||||
defaultMessage='followers'
|
||||
/>
|
||||
</span>
|
||||
<div className='annual-report__summary__followers__footnote'>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.followers.total'
|
||||
defaultMessage='{count} total'
|
||||
values={{ count: <ShortNumber value={total ?? 0} /> }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,105 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-return,
|
||||
@typescript-eslint/no-explicit-any,
|
||||
@typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { toggleStatusSpoilers } from 'mastodon/actions/statuses';
|
||||
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import type { TopStatuses } from 'mastodon/models/annual_report';
|
||||
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
|
||||
const getPictureInPicture = makeGetPictureInPicture() as unknown as (
|
||||
arg0: any,
|
||||
arg1: any,
|
||||
) => any;
|
||||
|
||||
export const HighlightedPost: React.FC<{
|
||||
data: TopStatuses;
|
||||
}> = ({ data }) => {
|
||||
let statusId, label;
|
||||
|
||||
if (data.by_reblogs) {
|
||||
statusId = data.by_reblogs;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.by_reblogs'
|
||||
defaultMessage='most boosted post'
|
||||
/>
|
||||
);
|
||||
} else if (data.by_favourites) {
|
||||
statusId = data.by_favourites;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.by_favourites'
|
||||
defaultMessage='most favourited post'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
statusId = data.by_replies;
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.by_replies'
|
||||
defaultMessage='post with the most replies'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const domain = useAppSelector((state) => state.meta.get('domain'));
|
||||
const status = useAppSelector((state) =>
|
||||
statusId ? getStatus(state, { id: statusId }) : undefined,
|
||||
);
|
||||
const pictureInPicture = useAppSelector((state) =>
|
||||
statusId ? getPictureInPicture(state, { id: statusId }) : undefined,
|
||||
);
|
||||
const account = useAppSelector((state) =>
|
||||
me ? state.accounts.get(me) : undefined,
|
||||
);
|
||||
|
||||
const handleToggleHidden = useCallback(() => {
|
||||
dispatch(toggleStatusSpoilers(statusId));
|
||||
}, [dispatch, statusId]);
|
||||
|
||||
if (!status) {
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__most-boosted-post' />
|
||||
);
|
||||
}
|
||||
|
||||
const displayName = (
|
||||
<span className='display-name'>
|
||||
<strong className='display-name__html'>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.highlighted_post.possessive'
|
||||
defaultMessage="{name}'s"
|
||||
values={{
|
||||
name: account && (
|
||||
<bdi
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</strong>
|
||||
<span className='display-name__account'>{label}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__most-boosted-post'>
|
||||
<DetailedStatus
|
||||
status={status}
|
||||
pictureInPicture={pictureInPicture}
|
||||
domain={domain}
|
||||
onToggleHidden={handleToggleHidden}
|
||||
overrideDisplayName={displayName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
99
app/javascript/mastodon/features/annual_report/index.tsx
Normal file
99
app/javascript/mastodon/features/annual_report/index.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import {
|
||||
importFetchedStatuses,
|
||||
importFetchedAccounts,
|
||||
} from 'mastodon/actions/importer';
|
||||
import { apiRequestGet, apiRequestPost } from 'mastodon/api';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
import type { AnnualReport as AnnualReportData } from 'mastodon/models/annual_report';
|
||||
import type { Status } from 'mastodon/models/status';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { Archetype } from './archetype';
|
||||
import { Followers } from './followers';
|
||||
import { HighlightedPost } from './highlighted_post';
|
||||
import { MostUsedHashtag } from './most_used_hashtag';
|
||||
import { NewPosts } from './new_posts';
|
||||
import { Percentile } from './percentile';
|
||||
|
||||
interface AnnualReportResponse {
|
||||
annual_reports: AnnualReportData[];
|
||||
accounts: Account[];
|
||||
statuses: Status[];
|
||||
}
|
||||
|
||||
export const AnnualReport: React.FC<{
|
||||
year: string;
|
||||
}> = ({ year }) => {
|
||||
const [response, setResponse] = useState<AnnualReportResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const currentAccount = useAppSelector((state) =>
|
||||
me ? state.accounts.get(me) : undefined,
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
apiRequestGet<AnnualReportResponse>(`v1/annual_reports/${year}`)
|
||||
.then((data) => {
|
||||
dispatch(importFetchedStatuses(data.statuses));
|
||||
dispatch(importFetchedAccounts(data.accounts));
|
||||
|
||||
setResponse(data);
|
||||
setLoading(false);
|
||||
|
||||
return apiRequestPost(`v1/annual_reports/${year}/read`);
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [dispatch, year, setResponse, setLoading]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
const report = response?.annual_reports[0];
|
||||
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='annual-report'>
|
||||
<div className='annual-report__header'>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.thanks'
|
||||
defaultMessage='Thanks for being part of Mastodon!'
|
||||
/>
|
||||
</h1>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.here_it_is'
|
||||
defaultMessage='Here is your {year} in review:'
|
||||
values={{ year: report.year }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='annual-report__bento annual-report__summary'>
|
||||
<Archetype data={report.data.archetype} />
|
||||
<HighlightedPost data={report.data.top_statuses} />
|
||||
<Followers
|
||||
data={report.data.time_series}
|
||||
total={currentAccount?.followers_count}
|
||||
/>
|
||||
<MostUsedHashtag data={report.data.top_hashtags} />
|
||||
<Percentile data={report.data.percentiles} />
|
||||
<NewPosts data={report.data.time_series} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import type { NameAndCount } from 'mastodon/models/annual_report';
|
||||
|
||||
export const MostUsedApp: React.FC<{
|
||||
data: NameAndCount[];
|
||||
}> = ({ data }) => {
|
||||
const app = data[0];
|
||||
|
||||
if (!app) {
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__most-used-app' />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__most-used-app'>
|
||||
<div className='annual-report__summary__most-used-app__icon'>
|
||||
{app.name}
|
||||
</div>
|
||||
<div className='annual-report__summary__most-used-app__label'>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_app.most_used_app'
|
||||
defaultMessage='most used app'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import type { NameAndCount } from 'mastodon/models/annual_report';
|
||||
|
||||
export const MostUsedHashtag: React.FC<{
|
||||
data: NameAndCount[];
|
||||
}> = ({ data }) => {
|
||||
const hashtag = data[0];
|
||||
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__most-used-hashtag'>
|
||||
<div className='annual-report__summary__most-used-hashtag__hashtag'>
|
||||
{hashtag ? (
|
||||
<>#{hashtag.name}</>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.none'
|
||||
defaultMessage='None'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='annual-report__summary__most-used-hashtag__label'>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.most_used_hashtag'
|
||||
defaultMessage='most used hashtag'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
53
app/javascript/mastodon/features/annual_report/new_posts.tsx
Normal file
53
app/javascript/mastodon/features/annual_report/new_posts.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { FormattedNumber, FormattedMessage } from 'react-intl';
|
||||
|
||||
import ChatBubbleIcon from '@/material-icons/400-24px/chat_bubble.svg?react';
|
||||
import type { TimeSeriesMonth } from 'mastodon/models/annual_report';
|
||||
|
||||
export const NewPosts: React.FC<{
|
||||
data: TimeSeriesMonth[];
|
||||
}> = ({ data }) => {
|
||||
const posts = data.reduce((sum, item) => sum + item.statuses, 0);
|
||||
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__new-posts'>
|
||||
<svg width={500} height={500}>
|
||||
<defs>
|
||||
<pattern
|
||||
id='posts'
|
||||
x='0'
|
||||
y='0'
|
||||
width='32'
|
||||
height='35'
|
||||
patternUnits='userSpaceOnUse'
|
||||
>
|
||||
<circle cx='12' cy='12' r='12' fill='var(--lime)' />
|
||||
<ChatBubbleIcon
|
||||
fill='var(--indigo-1)'
|
||||
x='4'
|
||||
y='4'
|
||||
width='16'
|
||||
height='16'
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect
|
||||
width={500}
|
||||
height={500}
|
||||
fill='url(#posts)'
|
||||
style={{ opacity: 0.2 }}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div className='annual-report__summary__new-posts__number'>
|
||||
<FormattedNumber value={posts} />
|
||||
</div>
|
||||
<div className='annual-report__summary__new-posts__label'>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.new_posts.new_posts'
|
||||
defaultMessage='new posts'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
|
||||
import type { Percentiles } from 'mastodon/models/annual_report';
|
||||
|
||||
export const Percentile: React.FC<{
|
||||
data: Percentiles;
|
||||
}> = ({ data }) => {
|
||||
const percentile = data.statuses;
|
||||
|
||||
return (
|
||||
<div className='annual-report__bento__box annual-report__summary__percentile'>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.percentile.text'
|
||||
defaultMessage='<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of Mastodon users.</bottomLabel>'
|
||||
values={{
|
||||
topLabel: (str) => (
|
||||
<div className='annual-report__summary__percentile__label'>
|
||||
{str}
|
||||
</div>
|
||||
),
|
||||
percentage: () => (
|
||||
<div className='annual-report__summary__percentile__number'>
|
||||
<FormattedNumber
|
||||
value={Math.min(percentile, 99) / 100}
|
||||
style='percent'
|
||||
maximumFractionDigits={percentile < 1 ? 1 : 0}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
bottomLabel: (str) => (
|
||||
<div>
|
||||
<div className='annual-report__summary__percentile__label'>
|
||||
{str}
|
||||
</div>
|
||||
|
||||
{percentile < 6 && (
|
||||
<div className='annual-report__summary__percentile__footnote'>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.percentile.we_wont_tell_bernie'
|
||||
defaultMessage="We won't tell Bernie."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{(message) => <>{message}</>}
|
||||
</FormattedMessage>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -68,7 +68,7 @@ class FollowRequests extends ImmutablePureComponent {
|
|||
);
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} icon='user-plus' iconComponent={PersonAddIcon} heading={intl.formatMessage(messages.heading)}>
|
||||
<Column bindToDocument={!multiColumn} icon='user-plus' iconComponent={PersonAddIcon} heading={intl.formatMessage(messages.heading)} alwaysShowBackButton>
|
||||
<ScrollableList
|
||||
scrollKey='follow_requests'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(injectIntl(Account));
|
|
@ -1,75 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
|
||||
});
|
||||
|
||||
const MapStateToProps = (state, { listId, added }) => ({
|
||||
list: state.get('lists').get(listId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['listAdder', 'lists', 'items']).includes(listId) : added,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { listId }) => ({
|
||||
onRemove: () => dispatch(removeFromListAdder(listId)),
|
||||
onAdd: () => dispatch(addToListAdder(listId)),
|
||||
});
|
||||
|
||||
class List extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
list: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { list, intl, onRemove, onAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='list'>
|
||||
<div className='list__wrapper'>
|
||||
<div className='list__display-name'>
|
||||
<Icon id='list-ul' icon={ListAltIcon} className='column-link__icon' />
|
||||
{list.get('title')}
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(List));
|
|
@ -1,76 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { setupListAdder, resetListAdder } from '../../actions/lists';
|
||||
import NewListForm from '../lists/components/new_list_form';
|
||||
|
||||
import Account from './components/account';
|
||||
import List from './components/list';
|
||||
// hack
|
||||
|
||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||
if (!lists) {
|
||||
return lists;
|
||||
}
|
||||
|
||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
listIds: getOrderedLists(state).map(list=>list.get('id')),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: accountId => dispatch(setupListAdder(accountId)),
|
||||
onReset: () => dispatch(resetListAdder()),
|
||||
});
|
||||
|
||||
class ListAdder extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
listIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, accountId } = this.props;
|
||||
onInitialize(accountId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountId, listIds } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-adder'>
|
||||
<div className='list-adder__account'>
|
||||
<Account accountId={accountId} />
|
||||
</div>
|
||||
|
||||
<NewListForm />
|
||||
|
||||
|
||||
<div className='list-adder__lists'>
|
||||
{listIds.map(ListId => <List key={ListId} listId={ListId} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListAdder));
|
213
app/javascript/mastodon/features/list_adder/index.tsx
Normal file
213
app/javascript/mastodon/features/list_adder/index.tsx
Normal file
|
@ -0,0 +1,213 @@
|
|||
import { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
import { createList } from 'mastodon/actions/lists_typed';
|
||||
import {
|
||||
apiGetAccountLists,
|
||||
apiAddAccountToList,
|
||||
apiRemoveAccountFromList,
|
||||
} from 'mastodon/api/lists';
|
||||
import type { ApiListJSON } from 'mastodon/api_types/lists';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CheckBox } from 'mastodon/components/check_box';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { getOrderedLists } from 'mastodon/selectors/lists';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
newList: {
|
||||
id: 'lists.new_list_name',
|
||||
defaultMessage: 'New list name',
|
||||
},
|
||||
createList: {
|
||||
id: 'lists.create',
|
||||
defaultMessage: 'Create',
|
||||
},
|
||||
close: {
|
||||
id: 'lightbox.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
});
|
||||
|
||||
const ListItem: React.FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
checked: boolean;
|
||||
onChange: (id: string, checked: boolean) => void;
|
||||
}> = ({ id, title, checked, onChange }) => {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(id, e.target.checked);
|
||||
},
|
||||
[id, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label className='lists__item'>
|
||||
<div className='lists__item__title'>
|
||||
<Icon id='list-ul' icon={ListAltIcon} />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
|
||||
<CheckBox value={id} checked={checked} onChange={handleChange} />
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
const NewListItem: React.FC<{
|
||||
onCreate: (list: ApiListJSON) => void;
|
||||
}> = ({ onCreate }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const [title, setTitle] = useState('');
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(value);
|
||||
},
|
||||
[setTitle],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (title.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void dispatch(createList({ title })).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
onCreate(result.payload);
|
||||
setTitle('');
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
}, [setTitle, dispatch, onCreate, title]);
|
||||
|
||||
return (
|
||||
<form className='lists__item' onSubmit={handleSubmit}>
|
||||
<label className='lists__item__title'>
|
||||
<Icon id='list-ul' icon={ListAltIcon} />
|
||||
|
||||
<input
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={handleChange}
|
||||
maxLength={30}
|
||||
required
|
||||
placeholder={intl.formatMessage(messages.newList)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button text={intl.formatMessage(messages.createList)} type='submit' />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const ListAdder: React.FC<{
|
||||
accountId: string;
|
||||
onClose: () => void;
|
||||
}> = ({ accountId, onClose }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||
const [listIds, setListIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchLists());
|
||||
|
||||
apiGetAccountLists(accountId)
|
||||
.then((data) => {
|
||||
setListIds(data.map((l) => l.id));
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
// Nothing
|
||||
});
|
||||
}, [dispatch, setListIds, accountId]);
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(listId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setListIds((currentListIds) => [listId, ...currentListIds]);
|
||||
|
||||
apiAddAccountToList(listId, accountId).catch(() => {
|
||||
setListIds((currentListIds) =>
|
||||
currentListIds.filter((id) => id !== listId),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setListIds((currentListIds) =>
|
||||
currentListIds.filter((id) => id !== listId),
|
||||
);
|
||||
|
||||
apiRemoveAccountFromList(listId, accountId).catch(() => {
|
||||
setListIds((currentListIds) => [listId, ...currentListIds]);
|
||||
});
|
||||
}
|
||||
},
|
||||
[setListIds, accountId],
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(
|
||||
(list: ApiListJSON) => {
|
||||
setListIds((currentListIds) => [list.id, ...currentListIds]);
|
||||
|
||||
apiAddAccountToList(list.id, accountId).catch(() => {
|
||||
setListIds((currentListIds) =>
|
||||
currentListIds.filter((id) => id !== list.id),
|
||||
);
|
||||
});
|
||||
},
|
||||
[setListIds, accountId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal dialog-modal'>
|
||||
<div className='dialog-modal__header'>
|
||||
<IconButton
|
||||
className='dialog-modal__header__close'
|
||||
title={intl.formatMessage(messages.close)}
|
||||
icon='times'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<span className='dialog-modal__header__title'>
|
||||
<FormattedMessage
|
||||
id='lists.add_to_lists'
|
||||
defaultMessage='Add {name} to lists'
|
||||
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='dialog-modal__content'>
|
||||
<div className='lists-scrollable'>
|
||||
<NewListItem onCreate={handleCreate} />
|
||||
|
||||
{lists.map((list) => (
|
||||
<ListItem
|
||||
key={list.id}
|
||||
id={list.id}
|
||||
title={list.title}
|
||||
checked={listIds.includes(list.id)}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ListAdder;
|
|
@ -1,82 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId, added }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||
onRemove: () => dispatch(removeFromListEditor(accountId)),
|
||||
onAdd: () => dispatch(addToListEditor(accountId)),
|
||||
});
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, intl, onRemove, onAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(Account));
|
|
@ -1,76 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
|
||||
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['listEditor', 'title']),
|
||||
disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeListEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitListEditor(false)),
|
||||
});
|
||||
|
||||
class ListForm extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const title = intl.formatMessage(messages.title);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon='check'
|
||||
iconComponent={CheckIcon}
|
||||
title={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListForm));
|
|
@ -1,83 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['listEditor', 'suggestions', 'value']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSubmit: value => dispatch(fetchListSuggestions(value)),
|
||||
onClear: () => dispatch(clearListSuggestions()),
|
||||
onChange: value => dispatch(changeListSuggestions(value)),
|
||||
});
|
||||
|
||||
class Search extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleKeyUp = e => {
|
||||
if (e.keyCode === 13) {
|
||||
this.props.onSubmit(this.props.value);
|
||||
}
|
||||
};
|
||||
|
||||
handleClear = () => {
|
||||
this.props.onClear();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, intl } = this.props;
|
||||
const hasValue = value.length > 0;
|
||||
|
||||
return (
|
||||
<div className='list-editor__search search'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
|
||||
|
||||
<input
|
||||
className='search__input'
|
||||
type='text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
placeholder={intl.formatMessage(messages.search)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}>
|
||||
<Icon id='search' icon={SearchIcon} className={classNames({ active: !hasValue })} />
|
||||
<Icon id='times-circle' icon={CancelIcon} aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Search));
|
|
@ -1,83 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
|
||||
import Account from './components/account';
|
||||
import EditListForm from './components/edit_list_form';
|
||||
import Search from './components/search';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
|
||||
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: listId => dispatch(setupListEditor(listId)),
|
||||
onClear: () => dispatch(clearListSuggestions()),
|
||||
onReset: () => dispatch(resetListEditor()),
|
||||
});
|
||||
|
||||
class ListEditor extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
listId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
searchAccountIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, listId } = this.props;
|
||||
onInitialize(listId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds, searchAccountIds, onClear } = this.props;
|
||||
const showSearch = searchAccountIds.size > 0;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-editor'>
|
||||
<EditListForm />
|
||||
|
||||
<Search />
|
||||
|
||||
<div className='drawer__pager'>
|
||||
<div className='drawer__inner list-editor__accounts'>
|
||||
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
|
||||
</div>
|
||||
|
||||
{showSearch && <div role='button' tabIndex={-1} className='drawer__backdrop' onClick={onClear} />}
|
||||
|
||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
{({ x }) => (
|
||||
<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListEditor));
|
|
@ -1,21 +1,19 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import { fetchList, updateList } from 'mastodon/actions/lists';
|
||||
import { fetchList } from 'mastodon/actions/lists';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { connectListStream } from 'mastodon/actions/streaming';
|
||||
import { expandListTimeline } from 'mastodon/actions/timelines';
|
||||
|
@ -23,17 +21,10 @@ import Column from 'mastodon/components/column';
|
|||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { RadioButton } from 'mastodon/components/radio_button';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
const messages = defineMessages({
|
||||
followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
|
||||
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
|
||||
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
list: state.getIn(['lists', props.params.id]),
|
||||
hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
|
||||
|
@ -115,13 +106,6 @@ class ListTimeline extends PureComponent {
|
|||
this.props.dispatch(expandListTimeline(id, { maxId }));
|
||||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal({
|
||||
modalType: 'LIST_EDITOR',
|
||||
modalProps: { listId: this.props.params.id },
|
||||
}));
|
||||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { dispatch, columnId } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
@ -129,25 +113,11 @@ class ListTimeline extends PureComponent {
|
|||
dispatch(openModal({ modalType: 'CONFIRM_DELETE_LIST', modalProps: { listId: id, columnId } }));
|
||||
};
|
||||
|
||||
handleRepliesPolicyChange = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateList(id, undefined, false, undefined, target.value));
|
||||
};
|
||||
|
||||
onExclusiveToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateList(id, undefined, false, target.checked, undefined));
|
||||
};
|
||||
|
||||
render () {
|
||||
const { hasUnread, columnId, multiColumn, list, intl } = this.props;
|
||||
const { hasUnread, columnId, multiColumn, list } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const pinned = !!columnId;
|
||||
const title = list ? list.get('title') : id;
|
||||
const replies_policy = list ? list.get('replies_policy') : undefined;
|
||||
const isExclusive = list ? list.get('exclusive') : undefined;
|
||||
|
||||
if (typeof list === 'undefined') {
|
||||
return (
|
||||
|
@ -178,35 +148,14 @@ class ListTimeline extends PureComponent {
|
|||
>
|
||||
<div className='column-settings'>
|
||||
<section className='column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||
<Link to={`/lists/${id}/edit`} className='text-btn column-header__setting-btn'>
|
||||
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{replies_policy !== undefined && (
|
||||
<section aria-labelledby={`list-${id}-replies-policy`}>
|
||||
<h3 id={`list-${id}-replies-policy`}><FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
{ ['none', 'list', 'followed'].map(policy => (
|
||||
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
|
||||
|
@ -229,4 +178,4 @@ class ListTimeline extends PureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(injectIntl(ListTimeline)));
|
||||
export default withRouter(connect(mapStateToProps)(ListTimeline));
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeListEditorTitle, submitListEditor } from 'mastodon/actions/lists';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
|
||||
title: { id: 'lists.new.create', defaultMessage: 'Add list' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['listEditor', 'title']),
|
||||
disabled: state.getIn(['listEditor', 'isSubmitting']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeListEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitListEditor(true)),
|
||||
});
|
||||
|
||||
class NewListForm extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const label = intl.formatMessage(messages.label);
|
||||
const title = intl.formatMessage(messages.title);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={this.handleChange}
|
||||
placeholder={label}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
disabled={disabled || !value}
|
||||
text={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(NewListForm));
|
|
@ -1,94 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import ColumnLink from 'mastodon/features/ui/components/column_link';
|
||||
import ColumnSubheading from 'mastodon/features/ui/components/column_subheading';
|
||||
|
||||
import NewListForm from './components/new_list_form';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
|
||||
});
|
||||
|
||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||
if (!lists) {
|
||||
return lists;
|
||||
}
|
||||
|
||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
lists: getOrderedLists(state),
|
||||
});
|
||||
|
||||
class Lists extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
lists: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount () {
|
||||
this.props.dispatch(fetchLists());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, lists, multiColumn } = this.props;
|
||||
|
||||
if (!lists) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.heading)}>
|
||||
<ColumnHeader title={intl.formatMessage(messages.heading)} icon='list-ul' iconComponent={ListAltIcon} multiColumn={multiColumn} />
|
||||
|
||||
<NewListForm />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='lists'
|
||||
emptyMessage={emptyMessage}
|
||||
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{lists.map(list =>
|
||||
<ColumnLink key={list.get('id')} to={`/lists/${list.get('id')}`} icon='list-ul' iconComponent={ListAltIcon} text={list.get('title')} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Lists));
|
145
app/javascript/mastodon/features/lists/index.tsx
Normal file
145
app/javascript/mastodon/features/lists/index.tsx
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { useEffect, useMemo, useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||
import { getOrderedLists } from 'mastodon/selectors/lists';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
create: { id: 'lists.create_list', defaultMessage: 'Create list' },
|
||||
edit: { id: 'lists.edit', defaultMessage: 'Edit list' },
|
||||
delete: { id: 'lists.delete', defaultMessage: 'Delete list' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
});
|
||||
|
||||
const ListItem: React.FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
}> = ({ id, title }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_DELETE_LIST',
|
||||
modalProps: {
|
||||
listId: id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [dispatch, id]);
|
||||
|
||||
const menu = useMemo(
|
||||
() => [
|
||||
{ text: intl.formatMessage(messages.edit), to: `/lists/${id}/edit` },
|
||||
{ text: intl.formatMessage(messages.delete), action: handleDeleteClick },
|
||||
],
|
||||
[intl, id, handleDeleteClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='lists__item'>
|
||||
<Link to={`/lists/${id}`} className='lists__item__title'>
|
||||
<Icon id='list-ul' icon={ListAltIcon} />
|
||||
<span>{title}</span>
|
||||
</Link>
|
||||
|
||||
<DropdownMenuContainer
|
||||
scrollKey='lists'
|
||||
items={menu}
|
||||
icons='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Lists: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchLists());
|
||||
}, [dispatch]);
|
||||
|
||||
const emptyMessage = (
|
||||
<>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='lists.no_lists_yet'
|
||||
defaultMessage='No lists yet.'
|
||||
/>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
id='lists.create_a_list_to_organize'
|
||||
defaultMessage='Create a new list to organize your Home feed'
|
||||
/>
|
||||
</span>
|
||||
|
||||
<SquigglyArrow className='empty-column-indicator__arrow' />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(messages.heading)}
|
||||
>
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
icon='list-ul'
|
||||
iconComponent={ListAltIcon}
|
||||
multiColumn={multiColumn}
|
||||
extraButton={
|
||||
<Link
|
||||
to='/lists/new'
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(messages.create)}
|
||||
aria-label={intl.formatMessage(messages.create)}
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='lists'
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{lists.map((list) => (
|
||||
<ListItem key={list.id} id={list.id} title={list.title} />
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default Lists;
|
373
app/javascript/mastodon/features/lists/members.tsx
Normal file
373
app/javascript/mastodon/features/lists/members.tsx
Normal file
|
@ -0,0 +1,373 @@
|
|||
import { useCallback, useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||
import { fetchFollowing } from 'mastodon/actions/accounts';
|
||||
import { importFetchedAccounts } from 'mastodon/actions/importer';
|
||||
import { fetchList } from 'mastodon/actions/lists';
|
||||
import { apiRequest } from 'mastodon/api';
|
||||
import {
|
||||
apiGetAccounts,
|
||||
apiAddAccountToList,
|
||||
apiRemoveAccountFromList,
|
||||
} from 'mastodon/api/lists';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { FollowersCounter } from 'mastodon/components/counters';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.list_members', defaultMessage: 'Manage list members' },
|
||||
placeholder: {
|
||||
id: 'lists.search_placeholder',
|
||||
defaultMessage: 'Search people you follow',
|
||||
},
|
||||
enterSearch: { id: 'lists.add_to_list', defaultMessage: 'Add to list' },
|
||||
add: { id: 'lists.add_member', defaultMessage: 'Add' },
|
||||
remove: { id: 'lists.remove_member', defaultMessage: 'Remove' },
|
||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||
});
|
||||
|
||||
type Mode = 'remove' | 'add';
|
||||
|
||||
const ColumnSearchHeader: React.FC<{
|
||||
onBack: () => void;
|
||||
onSubmit: (value: string) => void;
|
||||
}> = ({ onBack, onSubmit }) => {
|
||||
const intl = useIntl();
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(value);
|
||||
onSubmit(value);
|
||||
},
|
||||
[setValue, onSubmit],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
onSubmit(value);
|
||||
}, [onSubmit, value]);
|
||||
|
||||
return (
|
||||
<ButtonInTabsBar>
|
||||
<form className='column-search-header' onSubmit={handleSubmit}>
|
||||
<button
|
||||
type='button'
|
||||
className='column-header__back-button compact'
|
||||
onClick={onBack}
|
||||
aria-label={intl.formatMessage(messages.back)}
|
||||
>
|
||||
<Icon
|
||||
id='chevron-left'
|
||||
icon={ArrowBackIcon}
|
||||
className='column-back-button__icon'
|
||||
/>
|
||||
</button>
|
||||
|
||||
<input
|
||||
type='search'
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
/* eslint-disable-next-line jsx-a11y/no-autofocus */
|
||||
autoFocus
|
||||
/>
|
||||
</form>
|
||||
</ButtonInTabsBar>
|
||||
);
|
||||
};
|
||||
|
||||
const AccountItem: React.FC<{
|
||||
accountId: string;
|
||||
listId: string;
|
||||
partOfList: boolean;
|
||||
onToggle: (accountId: string) => void;
|
||||
}> = ({ accountId, listId, partOfList, onToggle }) => {
|
||||
const intl = useIntl();
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (partOfList) {
|
||||
void apiRemoveAccountFromList(listId, accountId);
|
||||
} else {
|
||||
void apiAddAccountToList(listId, accountId);
|
||||
}
|
||||
|
||||
onToggle(accountId);
|
||||
}, [accountId, listId, partOfList, onToggle]);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstVerifiedField = account.fields.find((item) => !!item.verified_at);
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<Link
|
||||
key={account.id}
|
||||
className='account__display-name'
|
||||
title={account.acct}
|
||||
to={`/@${account.acct}`}
|
||||
data-hover-card-account={account.id}
|
||||
>
|
||||
<div className='account__avatar-wrapper'>
|
||||
<Avatar account={account} size={36} />
|
||||
</div>
|
||||
|
||||
<div className='account__contents'>
|
||||
<DisplayName account={account} />
|
||||
|
||||
<div className='account__details'>
|
||||
<ShortNumber
|
||||
value={account.followers_count}
|
||||
renderer={FollowersCounter}
|
||||
/>{' '}
|
||||
{firstVerifiedField && (
|
||||
<VerifiedBadge link={firstVerifiedField.value} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className='account__relationship'>
|
||||
<Button
|
||||
text={intl.formatMessage(
|
||||
partOfList ? messages.remove : messages.add,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ListMembers: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const intl = useIntl();
|
||||
|
||||
const followingAccountIds = useAppSelector(
|
||||
(state) => state.user_lists.getIn(['following', me, 'items']) as string[],
|
||||
);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [accountIds, setAccountIds] = useState<string[]>([]);
|
||||
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [mode, setMode] = useState<Mode>('remove');
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
setLoading(true);
|
||||
dispatch(fetchList(id));
|
||||
|
||||
void apiGetAccounts(id)
|
||||
.then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
setAccountIds(data.map((a) => a.id));
|
||||
setLoading(false);
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
dispatch(fetchFollowing(me));
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleSearchClick = useCallback(() => {
|
||||
setMode('add');
|
||||
}, [setMode]);
|
||||
|
||||
const handleDismissSearchClick = useCallback(() => {
|
||||
setMode('remove');
|
||||
setSearching(false);
|
||||
}, [setMode]);
|
||||
|
||||
const handleAccountToggle = useCallback(
|
||||
(accountId: string) => {
|
||||
const partOfList = accountIds.includes(accountId);
|
||||
|
||||
if (partOfList) {
|
||||
setAccountIds(accountIds.filter((id) => id !== accountId));
|
||||
} else {
|
||||
setAccountIds([accountId, ...accountIds]);
|
||||
}
|
||||
},
|
||||
[accountIds, setAccountIds],
|
||||
);
|
||||
|
||||
const searchRequestRef = useRef<AbortController | null>(null);
|
||||
|
||||
const handleSearch = useDebouncedCallback(
|
||||
(value: string) => {
|
||||
if (searchRequestRef.current) {
|
||||
searchRequestRef.current.abort();
|
||||
}
|
||||
|
||||
if (value.trim().length === 0) {
|
||||
setSearching(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
searchRequestRef.current = new AbortController();
|
||||
|
||||
void apiRequest<ApiAccountJSON[]>('GET', 'v1/accounts/search', {
|
||||
signal: searchRequestRef.current.signal,
|
||||
params: {
|
||||
q: value,
|
||||
resolve: false,
|
||||
following: true,
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
setSearchAccountIds(data.map((a) => a.id));
|
||||
setLoading(false);
|
||||
setSearching(true);
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
setSearching(true);
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
|
||||
let displayedAccountIds: string[];
|
||||
|
||||
if (mode === 'add') {
|
||||
displayedAccountIds = searching ? searchAccountIds : followingAccountIds;
|
||||
} else {
|
||||
displayedAccountIds = accountIds;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(messages.heading)}
|
||||
>
|
||||
{mode === 'remove' ? (
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
icon='list-ul'
|
||||
iconComponent={ListAltIcon}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
extraButton={
|
||||
<button
|
||||
onClick={handleSearchClick}
|
||||
type='button'
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(messages.enterSearch)}
|
||||
aria-label={intl.formatMessage(messages.enterSearch)}
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ColumnSearchHeader
|
||||
onBack={handleDismissSearchClick}
|
||||
onSubmit={handleSearch}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='list_members'
|
||||
trackScroll={!multiColumn}
|
||||
bindToDocument={!multiColumn}
|
||||
isLoading={loading}
|
||||
showLoading={loading && displayedAccountIds.length === 0}
|
||||
hasMore={false}
|
||||
footer={
|
||||
mode === 'remove' && (
|
||||
<>
|
||||
<div className='spacer' />
|
||||
|
||||
<div className='column-footer'>
|
||||
<Link to={`/lists/${id}`} className='button button--block'>
|
||||
<FormattedMessage id='lists.done' defaultMessage='Done' />
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
emptyMessage={
|
||||
mode === 'remove' ? (
|
||||
<>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='lists.no_members_yet'
|
||||
defaultMessage='No members yet.'
|
||||
/>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
id='lists.find_users_to_add'
|
||||
defaultMessage='Find users to add'
|
||||
/>
|
||||
</span>
|
||||
|
||||
<SquigglyArrow className='empty-column-indicator__arrow' />
|
||||
</>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='lists.no_results_found'
|
||||
defaultMessage='No results found.'
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{displayedAccountIds.map((accountId) => (
|
||||
<AccountItem
|
||||
key={accountId}
|
||||
accountId={accountId}
|
||||
listId={id}
|
||||
partOfList={
|
||||
displayedAccountIds === accountIds ||
|
||||
accountIds.includes(accountId)
|
||||
}
|
||||
onToggle={handleAccountToggle}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ListMembers;
|
296
app/javascript/mastodon/features/lists/new.tsx
Normal file
296
app/javascript/mastodon/features/lists/new.tsx
Normal file
|
@ -0,0 +1,296 @@
|
|||
import { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams, useHistory, Link } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { fetchList } from 'mastodon/actions/lists';
|
||||
import { createList, updateList } from 'mastodon/actions/lists_typed';
|
||||
import { apiGetAccounts } from 'mastodon/api/lists';
|
||||
import type { RepliesPolicyType } from 'mastodon/api_types/lists';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
edit: { id: 'column.edit_list', defaultMessage: 'Edit list' },
|
||||
create: { id: 'column.create_list', defaultMessage: 'Create list' },
|
||||
});
|
||||
|
||||
const MembersLink: React.FC<{
|
||||
id: string;
|
||||
}> = ({ id }) => {
|
||||
const [count, setCount] = useState(0);
|
||||
const [avatars, setAvatars] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
void apiGetAccounts(id)
|
||||
.then((data) => {
|
||||
setCount(data.length);
|
||||
setAvatars(data.slice(0, 3).map((a) => a.avatar));
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
// Nothing
|
||||
});
|
||||
}, [id, setCount, setAvatars]);
|
||||
|
||||
return (
|
||||
<Link to={`/lists/${id}/members`} className='app-form__link'>
|
||||
<div className='app-form__link__text'>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='lists.list_members'
|
||||
defaultMessage='List members'
|
||||
/>
|
||||
</strong>
|
||||
<FormattedMessage
|
||||
id='lists.list_members_count'
|
||||
defaultMessage='{count, plural, one {# member} other {# members}}'
|
||||
values={{ count }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='avatar-pile'>
|
||||
{avatars.map((url) => (
|
||||
<img key={url} src={url} alt='' />
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const NewList: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const list = useAppSelector((state) =>
|
||||
id ? state.lists.get(id) : undefined,
|
||||
);
|
||||
const [title, setTitle] = useState('');
|
||||
const [exclusive, setExclusive] = useState(false);
|
||||
const [repliesPolicy, setRepliesPolicy] = useState<RepliesPolicyType>('list');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
dispatch(fetchList(id));
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id && list) {
|
||||
setTitle(list.title);
|
||||
setExclusive(list.exclusive);
|
||||
setRepliesPolicy(list.replies_policy);
|
||||
}
|
||||
}, [setTitle, setExclusive, setRepliesPolicy, id, list]);
|
||||
|
||||
const handleTitleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(value);
|
||||
},
|
||||
[setTitle],
|
||||
);
|
||||
|
||||
const handleExclusiveChange = useCallback(
|
||||
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setExclusive(checked);
|
||||
},
|
||||
[setExclusive],
|
||||
);
|
||||
|
||||
const handleRepliesPolicyChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setRepliesPolicy(value as RepliesPolicyType);
|
||||
},
|
||||
[setRepliesPolicy],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
setSubmitting(true);
|
||||
|
||||
if (id) {
|
||||
void dispatch(
|
||||
updateList({
|
||||
id,
|
||||
title,
|
||||
exclusive,
|
||||
replies_policy: repliesPolicy,
|
||||
}),
|
||||
).then(() => {
|
||||
setSubmitting(false);
|
||||
return '';
|
||||
});
|
||||
} else {
|
||||
void dispatch(
|
||||
createList({
|
||||
title,
|
||||
exclusive,
|
||||
replies_policy: repliesPolicy,
|
||||
}),
|
||||
).then((result) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (isFulfilled(result)) {
|
||||
history.replace(`/lists/${result.payload.id}/edit`);
|
||||
history.push(`/lists/${result.payload.id}/members`);
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
}
|
||||
}, [history, dispatch, setSubmitting, id, title, exclusive, repliesPolicy]);
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
>
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
icon='list-ul'
|
||||
iconComponent={ListAltIcon}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
/>
|
||||
|
||||
<div className='scrollable'>
|
||||
<form className='simple_form app-form' onSubmit={handleSubmit}>
|
||||
<div className='fields-group'>
|
||||
<div className='input with_label'>
|
||||
<div className='label_input'>
|
||||
<label htmlFor='list_title'>
|
||||
<FormattedMessage
|
||||
id='lists.list_name'
|
||||
defaultMessage='List name'
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className='label_input__wrapper'>
|
||||
<input
|
||||
id='list_title'
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
maxLength={30}
|
||||
required
|
||||
placeholder=' '
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='fields-group'>
|
||||
<div className='input with_label'>
|
||||
<div className='label_input'>
|
||||
<label htmlFor='list_replies_policy'>
|
||||
<FormattedMessage
|
||||
id='lists.show_replies_to'
|
||||
defaultMessage='Include replies from list members to'
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className='label_input__wrapper'>
|
||||
<select
|
||||
id='list_replies_policy'
|
||||
value={repliesPolicy}
|
||||
onChange={handleRepliesPolicyChange}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='lists.replies_policy.none'
|
||||
defaultMessage='No one'
|
||||
>
|
||||
{(msg) => <option value='none'>{msg}</option>}
|
||||
</FormattedMessage>
|
||||
<FormattedMessage
|
||||
id='lists.replies_policy.list'
|
||||
defaultMessage='Members of the list'
|
||||
>
|
||||
{(msg) => <option value='list'>{msg}</option>}
|
||||
</FormattedMessage>
|
||||
<FormattedMessage
|
||||
id='lists.replies_policy.followed'
|
||||
defaultMessage='Any followed user'
|
||||
>
|
||||
{(msg) => <option value='followed'>{msg}</option>}
|
||||
</FormattedMessage>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{id && (
|
||||
<div className='fields-group'>
|
||||
<MembersLink id={id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='fields-group'>
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label className='app-form__toggle'>
|
||||
<div className='app-form__toggle__label'>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='lists.exclusive'
|
||||
defaultMessage='Hide members in Home'
|
||||
/>
|
||||
</strong>
|
||||
<span className='hint'>
|
||||
<FormattedMessage
|
||||
id='lists.exclusive_hint'
|
||||
defaultMessage='If someone is on this list, hide them in your Home feed to avoid seeing their posts twice.'
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='app-form__toggle__toggle'>
|
||||
<div>
|
||||
<Toggle
|
||||
checked={exclusive}
|
||||
onChange={handleExclusiveChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className='actions'>
|
||||
<button className='button' type='submit'>
|
||||
{submitting ? (
|
||||
<LoadingIndicator />
|
||||
) : id ? (
|
||||
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||
) : (
|
||||
<FormattedMessage id='lists.create' defaultMessage='Create' />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default NewList;
|
|
@ -43,7 +43,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
|
|||
);
|
||||
|
||||
const handleMouseUp = useCallback<React.MouseEventHandler<HTMLDivElement>>(
|
||||
({ clientX, clientY, target, button }) => {
|
||||
({ clientX, clientY, target, button, ctrlKey, metaKey }) => {
|
||||
const [startX, startY] = clickCoordinatesRef.current ?? [0, 0];
|
||||
const [deltaX, deltaY] = [
|
||||
Math.abs(clientX - startX),
|
||||
|
@ -64,8 +64,14 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
|
|||
element = element.parentNode as HTMLDivElement | null;
|
||||
}
|
||||
|
||||
if (deltaX + deltaY < 5 && button === 0 && account) {
|
||||
history.push(`/@${account.acct}/${statusId}`);
|
||||
if (deltaX + deltaY < 5 && account) {
|
||||
const path = `/@${account.acct}/${statusId}`;
|
||||
|
||||
if (button === 0 && !(ctrlKey || metaKey)) {
|
||||
history.push(path);
|
||||
} else if (button === 1 || (button === 0 && (ctrlKey || metaKey))) {
|
||||
window.open(path, '_blank', 'noreferrer noopener');
|
||||
}
|
||||
}
|
||||
|
||||
clickCoordinatesRef.current = null;
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import CelebrationIcon from '@/material-icons/400-24px/celebration.svg?react';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import type { NotificationGroupAnnualReport } from 'mastodon/models/notification_group';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
export const NotificationAnnualReport: React.FC<{
|
||||
notification: NotificationGroupAnnualReport;
|
||||
unread: boolean;
|
||||
}> = ({ notification: { annualReport }, unread }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const year = annualReport.year;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'ANNUAL_REPORT',
|
||||
modalProps: { year },
|
||||
}),
|
||||
);
|
||||
}, [dispatch, year]);
|
||||
|
||||
return (
|
||||
<div
|
||||
role='button'
|
||||
className={classNames(
|
||||
'notification-group notification-group--link notification-group--annual-report focusable',
|
||||
{ 'notification-group--unread': unread },
|
||||
)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className='notification-group__icon'>
|
||||
<Icon id='celebration' icon={CelebrationIcon} />
|
||||
</div>
|
||||
|
||||
<div className='notification-group__main'>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='notification.annual_report.message'
|
||||
defaultMessage="Your {year} #Wrapstodon awaits! Unveil your year's highlights and memorable moments on Mastodon!"
|
||||
values={{ year }}
|
||||
/>
|
||||
</p>
|
||||
<button onClick={handleClick} className='link-button'>
|
||||
<FormattedMessage
|
||||
id='notification.annual_report.view'
|
||||
defaultMessage='View #Wrapstodon'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -9,6 +9,7 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
|||
|
||||
import { NotificationAdminReport } from './notification_admin_report';
|
||||
import { NotificationAdminSignUp } from './notification_admin_sign_up';
|
||||
import { NotificationAnnualReport } from './notification_annual_report';
|
||||
import { NotificationFavourite } from './notification_favourite';
|
||||
import { NotificationFollow } from './notification_follow';
|
||||
import { NotificationFollowRequest } from './notification_follow_request';
|
||||
|
@ -143,6 +144,14 @@ export const NotificationGroup: React.FC<{
|
|||
/>
|
||||
);
|
||||
break;
|
||||
case 'annual_report':
|
||||
content = (
|
||||
<NotificationAnnualReport
|
||||
unread={unread}
|
||||
notification={notificationGroup}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ export const DetailedStatus: React.FC<{
|
|||
domain: string;
|
||||
showMedia?: boolean;
|
||||
withLogo?: boolean;
|
||||
overrideDisplayName?: React.ReactNode;
|
||||
pictureInPicture: any;
|
||||
onToggleHidden?: (status: any) => void;
|
||||
onToggleMediaVisibility?: () => void;
|
||||
|
@ -62,6 +63,7 @@ export const DetailedStatus: React.FC<{
|
|||
domain,
|
||||
showMedia,
|
||||
withLogo,
|
||||
overrideDisplayName,
|
||||
pictureInPicture,
|
||||
onToggleMediaVisibility,
|
||||
onToggleHidden,
|
||||
|
@ -319,7 +321,11 @@ export const DetailedStatus: React.FC<{
|
|||
<div className='detailed-status__display-avatar'>
|
||||
<Avatar account={status.get('account')} size={46} />
|
||||
</div>
|
||||
<DisplayName account={status.get('account')} localDomain={domain} />
|
||||
|
||||
{overrideDisplayName ?? (
|
||||
<DisplayName account={status.get('account')} localDomain={domain} />
|
||||
)}
|
||||
|
||||
{withLogo && (
|
||||
<>
|
||||
<div className='spacer' />
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
import { AnnualReport } from 'mastodon/features/annual_report';
|
||||
|
||||
const AnnualReportModal: React.FC<{
|
||||
year: string;
|
||||
onChangeBackgroundColor: (arg0: string) => void;
|
||||
}> = ({ year, onChangeBackgroundColor }) => {
|
||||
useEffect(() => {
|
||||
onChangeBackgroundColor('var(--indigo-1)');
|
||||
}, [onChangeBackgroundColor]);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal annual-report-modal'>
|
||||
<AnnualReport year={year} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AnnualReportModal;
|
|
@ -128,6 +128,8 @@ export const BoostModal: React.FC<{
|
|||
? messages.cancel_reblog
|
||||
: messages.reblog,
|
||||
)}
|
||||
/* eslint-disable-next-line jsx-a11y/no-autofocus -- We are in the modal */
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
DomainBlockModal,
|
||||
ReportModal,
|
||||
EmbedModal,
|
||||
ListEditor,
|
||||
ListAdder,
|
||||
CompareHistoryModal,
|
||||
FilterModal,
|
||||
|
@ -18,6 +17,7 @@ import {
|
|||
SubscribedLanguagesModal,
|
||||
ClosedRegistrationsModal,
|
||||
IgnoreNotificationsModal,
|
||||
AnnualReportModal,
|
||||
} from 'mastodon/features/ui/util/async-components';
|
||||
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
|
||||
|
||||
|
@ -63,7 +63,6 @@ export const MODAL_COMPONENTS = {
|
|||
'REPORT': ReportModal,
|
||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
'EMBED': EmbedModal,
|
||||
'LIST_EDITOR': ListEditor,
|
||||
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||
'LIST_ADDER': ListAdder,
|
||||
'COMPARE_HISTORY': CompareHistoryModal,
|
||||
|
@ -72,6 +71,7 @@ export const MODAL_COMPONENTS = {
|
|||
'INTERACTION': InteractionModal,
|
||||
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
|
||||
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
|
||||
'ANNUAL_REPORT': AnnualReportModal,
|
||||
};
|
||||
|
||||
export default class ModalRoot extends PureComponent {
|
||||
|
|
|
@ -58,11 +58,13 @@ import {
|
|||
FollowedTags,
|
||||
LinkTimeline,
|
||||
ListTimeline,
|
||||
Lists,
|
||||
ListEdit,
|
||||
ListMembers,
|
||||
Blocks,
|
||||
DomainBlocks,
|
||||
Mutes,
|
||||
PinnedStatuses,
|
||||
Lists,
|
||||
Directory,
|
||||
Explore,
|
||||
Onboarding,
|
||||
|
@ -205,6 +207,9 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||
<WrappedRoute path='/links/:url' component={LinkTimeline} content={children} />
|
||||
<WrappedRoute path='/lists/new' component={ListEdit} content={children} />
|
||||
<WrappedRoute path='/lists/:id/edit' component={ListEdit} content={children} />
|
||||
<WrappedRoute path='/lists/:id/members' component={ListMembers} content={children} />
|
||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||
<WrappedRoute path='/notifications' component={NotificationsWrapper} content={children} exact />
|
||||
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
|
||||
|
@ -482,7 +487,9 @@ class UI extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
handleHotkeyBack = () => {
|
||||
handleHotkeyBack = e => {
|
||||
e.preventDefault();
|
||||
|
||||
const { history } = this.props;
|
||||
|
||||
if (history.location?.state?.fromMastodon) {
|
||||
|
|
|
@ -150,10 +150,6 @@ export function EmbedModal () {
|
|||
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
|
||||
}
|
||||
|
||||
export function ListEditor () {
|
||||
return import(/* webpackChunkName: "features/list_editor" */'../../list_editor');
|
||||
}
|
||||
|
||||
export function ListAdder () {
|
||||
return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
|
||||
}
|
||||
|
@ -217,3 +213,15 @@ export function NotificationRequest () {
|
|||
export function LinkTimeline () {
|
||||
return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline');
|
||||
}
|
||||
|
||||
export function AnnualReportModal () {
|
||||
return import(/*webpackChunkName: "modals/annual_report_modal" */'../components/annual_report_modal');
|
||||
}
|
||||
|
||||
export function ListEdit () {
|
||||
return import(/*webpackChunkName: "features/lists" */'../../lists/new');
|
||||
}
|
||||
|
||||
export function ListMembers () {
|
||||
return import(/* webpackChunkName: "features/lists" */'../../lists/members');
|
||||
}
|
||||
|
|
|
@ -154,7 +154,6 @@
|
|||
"empty_column.hashtag": "Daar is nog niks vir hierdie hutsetiket nie.",
|
||||
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
|
||||
"empty_column.list": "Hierdie lys is nog leeg. Nuwe plasings deur lyslede sal voortaan hier verskyn.",
|
||||
"empty_column.lists": "Jy het nog geen lyste nie. Wanneer jy een skep, sal dit hier vertoon.",
|
||||
"empty_column.notifications": "Jy het nog geen kennisgewings nie. Interaksie van ander mense met jou, sal hier vertoon.",
|
||||
"explore.search_results": "Soekresultate",
|
||||
"explore.suggested_follows": "Mense",
|
||||
|
@ -222,15 +221,8 @@
|
|||
"limited_account_hint.action": "Vertoon profiel in elk geval",
|
||||
"limited_account_hint.title": "Hierdie profiel is deur moderators van {domain} versteek.",
|
||||
"link_preview.author": "Deur {name}",
|
||||
"lists.account.add": "Voeg by lys",
|
||||
"lists.account.remove": "Verwyder vanaf lys",
|
||||
"lists.delete": "Verwyder lys",
|
||||
"lists.edit": "Redigeer lys",
|
||||
"lists.edit.submit": "Verander titel",
|
||||
"lists.new.create": "Voeg lys by",
|
||||
"lists.new.title_placeholder": "Nuwe lys titel",
|
||||
"lists.search": "Soek tussen mense wat jy volg",
|
||||
"lists.subheading": "Jou lyste",
|
||||
"moved_to_account_banner.text": "Jou rekening {disabledAccount} is tans gedeaktiveer omdat jy na {movedToAccount} verhuis het.",
|
||||
"navigation_bar.about": "Oor",
|
||||
"navigation_bar.bookmarks": "Boekmerke",
|
||||
|
|
|
@ -186,7 +186,6 @@
|
|||
"empty_column.hashtag": "No i hai cosa en este hashtag encara.",
|
||||
"empty_column.home": "La tuya linia temporal ye vueda! Sigue a mas personas pa replenar-la. {suggestions}",
|
||||
"empty_column.list": "No i hai cosa en esta lista encara. Quan miembros d'esta lista publiquen nuevos estatus, estes amaneixerán qui.",
|
||||
"empty_column.lists": "No tiens garra lista. Quan en crees una, s'amostrará aquí.",
|
||||
"empty_column.mutes": "Encara no has silenciau a garra usuario.",
|
||||
"empty_column.notifications": "No tiens garra notificación encara. Interactúa con atros pa empecipiar una conversación.",
|
||||
"empty_column.public": "No i hai cosa aquí! Escribe bella cosa publicament, u sigue usuarios d'atras instancias manualment pa emplir-lo",
|
||||
|
@ -292,19 +291,11 @@
|
|||
"lightbox.previous": "Anterior",
|
||||
"limited_account_hint.action": "Amostrar perfil de totz modos",
|
||||
"limited_account_hint.title": "Este perfil ha estau amagau per los moderadors de {domain}.",
|
||||
"lists.account.add": "Anyadir a lista",
|
||||
"lists.account.remove": "Sacar de lista",
|
||||
"lists.delete": "Borrar lista",
|
||||
"lists.edit": "Editar lista",
|
||||
"lists.edit.submit": "Cambiar titol",
|
||||
"lists.new.create": "Anyadir lista",
|
||||
"lists.new.title_placeholder": "Titol d'a nueva lista",
|
||||
"lists.replies_policy.followed": "Qualsequier usuario seguiu",
|
||||
"lists.replies_policy.list": "Miembros d'a lista",
|
||||
"lists.replies_policy.none": "Dengún",
|
||||
"lists.replies_policy.title": "Amostrar respuestas a:",
|
||||
"lists.search": "Buscar entre la chent a la quala sigues",
|
||||
"lists.subheading": "Las tuyas listas",
|
||||
"load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
|
||||
"moved_to_account_banner.text": "La tuya cuenta {disabledAccount} ye actualment deshabilitada perque t'has mudau a {movedToAccount}.",
|
||||
"navigation_bar.about": "Sobre",
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
"alert.unexpected.title": "المعذرة!",
|
||||
"alt_text_badge.title": "نص بديل",
|
||||
"announcement.announcement": "إعلان",
|
||||
"annual_report.summary.archetype.booster": "The cool-hunter",
|
||||
"attachments_list.unprocessed": "(غير معالَج)",
|
||||
"audio.hide": "إخفاء المقطع الصوتي",
|
||||
"block_modal.remote_users_caveat": "سوف نطلب من الخادم {domain} أن يحترم قرارك، لكن الالتزام غير مضمون لأن بعض الخواديم قد تتعامل مع نصوص الكتل بشكل مختلف. قد تظل المنشورات العامة مرئية للمستخدمين غير المسجلين الدخول.",
|
||||
|
@ -268,7 +269,6 @@
|
|||
"empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
|
||||
"empty_column.home": "إنّ الخيط الزمني لصفحتك الرئيسة فارغ. قم بمتابعة المزيد من الناس كي يمتلأ.",
|
||||
"empty_column.list": "هذه القائمة فارغة مؤقتا و لكن سوف تمتلئ تدريجيا عندما يبدأ الأعضاء المُنتَمين إليها بنشر منشورات.",
|
||||
"empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قوائمك هنا إن قمت بإنشاء واحدة.",
|
||||
"empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.",
|
||||
"empty_column.notification_requests": "لا يوجد شيء هنا. عندما تتلقى إشعارات جديدة، سوف تظهر هنا وفقًا لإعداداتك.",
|
||||
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
|
||||
|
@ -424,20 +424,11 @@
|
|||
"limited_account_hint.title": "تم إخفاء هذا الملف الشخصي من قبل مشرفي {domain}.",
|
||||
"link_preview.author": "مِن {name}",
|
||||
"link_preview.more_from_author": "المزيد من {name}",
|
||||
"lists.account.add": "أضف إلى القائمة",
|
||||
"lists.account.remove": "احذف من القائمة",
|
||||
"lists.delete": "احذف القائمة",
|
||||
"lists.edit": "عدّل القائمة",
|
||||
"lists.edit.submit": "تعديل العنوان",
|
||||
"lists.exclusive": "إخفاء هذه المنشورات من الخيط الرئيسي",
|
||||
"lists.new.create": "إضافة قائمة",
|
||||
"lists.new.title_placeholder": "عنوان القائمة الجديدة",
|
||||
"lists.replies_policy.followed": "أي مستخدم متابَع",
|
||||
"lists.replies_policy.list": "أعضاء القائمة",
|
||||
"lists.replies_policy.none": "لا أحد",
|
||||
"lists.replies_policy.title": "عرض الردود لـ:",
|
||||
"lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
|
||||
"lists.subheading": "قوائمك",
|
||||
"load_pending": "{count, plural, one {# عنصر جديد} other {# عناصر جديدة}}",
|
||||
"loading_indicator.label": "جاري التحميل…",
|
||||
"media_gallery.hide": "إخفاء",
|
||||
|
@ -489,6 +480,7 @@
|
|||
"notification.label.private_reply": "رد خاص",
|
||||
"notification.label.reply": "ردّ",
|
||||
"notification.mention": "إشارة",
|
||||
"notification.mentioned_you": "{name} mentioned you",
|
||||
"notification.moderation-warning.learn_more": "اعرف المزيد",
|
||||
"notification.moderation_warning": "لقد تلقيت تحذيرًا بالإشراف",
|
||||
"notification.moderation_warning.action_delete_statuses": "تم حذف بعض من منشوراتك.",
|
||||
|
|
|
@ -155,7 +155,6 @@
|
|||
"empty_column.hashtag": "Entá nun hai nada con esta etiqueta.",
|
||||
"empty_column.home": "¡La to llinia de tiempu ta balera! Sigui a cuentes pa enllenala.",
|
||||
"empty_column.list": "Nun hai nada nesta llista. Cuando los perfiles d'esta llista espublicen artículos nuevos, apaecen equí.",
|
||||
"empty_column.lists": "Nun tienes nenguna llista. Cuando crees dalguna, apaez equí.",
|
||||
"empty_column.mutes": "Nun tienes nengún perfil colos avisos desactivaos.",
|
||||
"empty_column.notifications": "Nun tienes nengún avisu. Cuando otros perfiles interactúen contigo, apaez equí.",
|
||||
"empty_column.public": "¡Equí nun hai nada! Escribi daqué públicamente o sigui a perfiles d'otros sirvidores pa enllenar esta seición",
|
||||
|
@ -260,15 +259,9 @@
|
|||
"limited_account_hint.action": "Amosar el perfil de toes toes",
|
||||
"lists.delete": "Desaniciar la llista",
|
||||
"lists.edit": "Editar la llista",
|
||||
"lists.edit.submit": "Camudar el títulu",
|
||||
"lists.new.create": "Amestar la llista",
|
||||
"lists.new.title_placeholder": "Títulu",
|
||||
"lists.replies_policy.followed": "Cualesquier perfil siguíu",
|
||||
"lists.replies_policy.list": "Perfiles de la llista",
|
||||
"lists.replies_policy.none": "Naide",
|
||||
"lists.replies_policy.title": "Amosar les rempuestes a:",
|
||||
"lists.search": "Buscar ente los perfiles que sigues",
|
||||
"lists.subheading": "Les tos llistes",
|
||||
"load_pending": "{count, plural, one {# elementu nuevu} other {# elementos nuevos}}",
|
||||
"navigation_bar.about": "Tocante a",
|
||||
"navigation_bar.blocks": "Perfiles bloquiaos",
|
||||
|
|
|
@ -154,7 +154,7 @@
|
|||
"compose_form.hashtag_warning": "Гэты допіс не будзе паказаны пад аніякім хэштэгам, бо ён не публічны. Толькі публічныя допісы можна знайсці па хэштэгу.",
|
||||
"compose_form.lock_disclaimer": "Ваш уліковы запіс не {locked}. Усе могуць падпісацца на вас, каб бачыць допісы толькі для падпісчыкаў.",
|
||||
"compose_form.lock_disclaimer.lock": "закрыты",
|
||||
"compose_form.placeholder": "Што здарылася?",
|
||||
"compose_form.placeholder": "Што ў вас новага?",
|
||||
"compose_form.poll.duration": "Працягласць апытання",
|
||||
"compose_form.poll.multiple": "Множны выбар",
|
||||
"compose_form.poll.option_placeholder": "Варыянт {number}",
|
||||
|
@ -273,7 +273,6 @@
|
|||
"empty_column.hashtag": "Па гэтаму хэштэгу пакуль што нічога няма.",
|
||||
"empty_column.home": "Галоўная стужка пустая! Падпішыцеся на іншых людзей, каб запоўніць яе. {suggestions}",
|
||||
"empty_column.list": "У гэтым спісе пакуль што нічога няма. Калі члены лісту апублікуюць новыя запісы, яны з'явяцца тут.",
|
||||
"empty_column.lists": "Як толькі вы створыце новы спіс ён будзе захоўвацца тут, але пакуль што тут пуста.",
|
||||
"empty_column.mutes": "Вы яшчэ нікога не ігнаруеце.",
|
||||
"empty_column.notification_requests": "Чысціня! Тут нічога няма. Калі вы будзеце атрымліваць новыя апавяшчэння, яны будуць з'яўляцца тут у адпаведнасці з вашымі наладамі.",
|
||||
"empty_column.notifications": "У вас няма ніякіх апавяшчэнняў. Калі іншыя людзі ўзаемадзейнічаюць з вамі, вы ўбачыце гэта тут.",
|
||||
|
@ -427,20 +426,11 @@
|
|||
"link_preview.author": "Ад {name}",
|
||||
"link_preview.more_from_author": "Больш ад {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} допіс} few {{counter} допісы} many {{counter} допісаў} other {{counter} допісу}}",
|
||||
"lists.account.add": "Дадаць да спісу",
|
||||
"lists.account.remove": "Выдаліць са спісу",
|
||||
"lists.delete": "Выдаліць спіс",
|
||||
"lists.edit": "Рэдагаваць спіс",
|
||||
"lists.edit.submit": "Змяніць назву",
|
||||
"lists.exclusive": "Схаваць гэтыя допісы з галоўнай старонкі",
|
||||
"lists.new.create": "Дадаць спіс",
|
||||
"lists.new.title_placeholder": "Назва новага спіса",
|
||||
"lists.replies_policy.followed": "Любы карыстальнік, на якога вы падпісаліся",
|
||||
"lists.replies_policy.list": "Удзельнікі гэтага спісу",
|
||||
"lists.replies_policy.none": "Нікога",
|
||||
"lists.replies_policy.title": "Паказваць адказы:",
|
||||
"lists.search": "Шукайце сярод людзей, на якіх Вы падпісаны",
|
||||
"lists.subheading": "Вашыя спісы",
|
||||
"load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}",
|
||||
"loading_indicator.label": "Загрузка…",
|
||||
"media_gallery.hide": "Схаваць",
|
||||
|
|
|
@ -87,6 +87,24 @@
|
|||
"alert.unexpected.title": "Опаа!",
|
||||
"alt_text_badge.title": "Алтернативен текст",
|
||||
"announcement.announcement": "Оповестяване",
|
||||
"annual_report.summary.archetype.booster": "Якият подсилвател",
|
||||
"annual_report.summary.archetype.lurker": "Дебнещото",
|
||||
"annual_report.summary.archetype.oracle": "Оракул",
|
||||
"annual_report.summary.archetype.pollster": "Анкетьорче",
|
||||
"annual_report.summary.archetype.replier": "Социална пеперуда",
|
||||
"annual_report.summary.followers.followers": "последователи",
|
||||
"annual_report.summary.followers.total": "{count} общо",
|
||||
"annual_report.summary.here_it_is": "Ето преглед на вашата {year} година:",
|
||||
"annual_report.summary.highlighted_post.by_favourites": "най-правено като любима публикация",
|
||||
"annual_report.summary.highlighted_post.by_reblogs": "най-подсилваната публикация",
|
||||
"annual_report.summary.highlighted_post.by_replies": "публикации с най-много отговори",
|
||||
"annual_report.summary.highlighted_post.possessive": "на {name}",
|
||||
"annual_report.summary.most_used_app.most_used_app": "най-употребявано приложение",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "най-употребяван хаштаг",
|
||||
"annual_report.summary.new_posts.new_posts": "нови публикации",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Това ви слага най-отгоре</topLabel><percentage></percentage><bottomLabel>сред потребителите на Mastodon.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "Няма да кажем на Бърни Сандърс.",
|
||||
"annual_report.summary.thanks": "Благодарим, че сте част от Mastodon!",
|
||||
"attachments_list.unprocessed": "(необработено)",
|
||||
"audio.hide": "Скриване на звука",
|
||||
"block_modal.remote_users_caveat": "Ще поискаме сървърът {domain} да почита решението ви. Съгласието обаче не се гарантира откак някои сървъри могат да боравят с блоковете по различен начин. Обществените публикации още може да се виждат от невлезли в системата потребители.",
|
||||
|
@ -121,13 +139,16 @@
|
|||
"column.blocks": "Блокирани потребители",
|
||||
"column.bookmarks": "Отметки",
|
||||
"column.community": "Локален инфопоток",
|
||||
"column.create_list": "Създаване на списък",
|
||||
"column.direct": "Частни споменавания",
|
||||
"column.directory": "Разглеждане на профили",
|
||||
"column.domain_blocks": "Блокирани домейни",
|
||||
"column.edit_list": "Промяна на списъка",
|
||||
"column.favourites": "Любими",
|
||||
"column.firehose": "Инфоканали на живо",
|
||||
"column.follow_requests": "Заявки за последване",
|
||||
"column.home": "Начало",
|
||||
"column.list_members": "Управление на списъка с участници",
|
||||
"column.lists": "Списъци",
|
||||
"column.mutes": "Заглушени потребители",
|
||||
"column.notifications": "Известия",
|
||||
|
@ -158,6 +179,7 @@
|
|||
"compose_form.poll.duration": "Времетраене на анкетата",
|
||||
"compose_form.poll.multiple": "Множествен избор",
|
||||
"compose_form.poll.option_placeholder": "Избор {number}",
|
||||
"compose_form.poll.single": "Единичен избор",
|
||||
"compose_form.poll.switch_to_multiple": "Промяна на анкетата, за да се позволят множество възможни избора",
|
||||
"compose_form.poll.switch_to_single": "Промяна на анкетата, за да се позволи един възможен избор",
|
||||
"compose_form.poll.type": "Стил",
|
||||
|
@ -195,6 +217,8 @@
|
|||
"confirmations.unfollow.message": "Наистина ли искате да не следвате {name}?",
|
||||
"confirmations.unfollow.title": "Спирате ли да следвате потребителя?",
|
||||
"content_warning.hide": "Скриване на публ.",
|
||||
"content_warning.show": "Нека се покаже",
|
||||
"content_warning.show_more": "Показване на още",
|
||||
"conversation.delete": "Изтриване на разговора",
|
||||
"conversation.mark_as_read": "Маркиране като прочетено",
|
||||
"conversation.open": "Преглед на разговора",
|
||||
|
@ -268,7 +292,6 @@
|
|||
"empty_column.hashtag": "Още няма нищо в този хаштаг.",
|
||||
"empty_column.home": "Вашата начална часова ос е празна! Последвайте повече хора, за да я запълните. {suggestions}",
|
||||
"empty_column.list": "Все още списъкът е празен. Членуващите на списъка, публикуващи нови публикации, ще се появят тук.",
|
||||
"empty_column.lists": "Все още нямате списъци. Когато създадете такъв, той ще се покаже тук.",
|
||||
"empty_column.mutes": "Още не сте заглушавали потребители.",
|
||||
"empty_column.notification_requests": "Всичко е чисто! Тук няма нищо. Получавайки нови известия, те ще се появят тук според настройките ви.",
|
||||
"empty_column.notifications": "Все още нямате известия. Взаимодействайте с другите, за да започнете разговора.",
|
||||
|
@ -365,10 +388,19 @@
|
|||
"home.pending_critical_update.link": "Преглед на обновяванията",
|
||||
"home.pending_critical_update.title": "Налично критично обновяване на сигурността!",
|
||||
"home.show_announcements": "Показване на оповестяванията",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon не може да осведоми потребители, че сте пренебрегнали известията им. Пренебрегването на известията няма да спре самите съобщения да не бъдат изпращани.",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Вие все още ще може да приемате, отхвърляте или докладвате потребители",
|
||||
"ignore_notifications_modal.filter_to_avoid_confusion": "Прецеждането помага за избягване на възможно объркване",
|
||||
"ignore_notifications_modal.ignore": "Пренебрегване на известията",
|
||||
"ignore_notifications_modal.limited_accounts_title": "Пренебрегвате ли известията от модерирани акаунти?",
|
||||
"ignore_notifications_modal.new_accounts_title": "Пренебрегвате ли известията от нови акаунти?",
|
||||
"ignore_notifications_modal.not_followers_title": "Пренебрегвате ли известията от хора, които не са ви последвали?",
|
||||
"ignore_notifications_modal.not_following_title": "Пренебрегвате ли известията от хора, които не сте последвали?",
|
||||
"interaction_modal.description.favourite": "Имайки акаунт в Mastodon, може да сложите тази публикации в любими, за да позволите на автора да узнае, че я цените и да я запазите за по-късно.",
|
||||
"interaction_modal.description.follow": "С акаунт в Mastodon може да последвате {name}, за да получавате публикациите от този акаунт в началния си инфоканал.",
|
||||
"interaction_modal.description.reblog": "С акаунт в Mastodon може да подсилите тази публикация, за да я споделите с последователите си.",
|
||||
"interaction_modal.description.reply": "С акаунт в Mastodon може да добавите отговор към тази публикация.",
|
||||
"interaction_modal.description.vote": "Имайки акаунт в Mastodon, можете да гласувате в тази анкета.",
|
||||
"interaction_modal.login.action": "Към началото",
|
||||
"interaction_modal.login.prompt": "Домейнът на сървъра ви, примерно, mastodon.social",
|
||||
"interaction_modal.no_account_yet": "Още не е в Мастодон?",
|
||||
|
@ -380,6 +412,7 @@
|
|||
"interaction_modal.title.follow": "Последване на {name}",
|
||||
"interaction_modal.title.reblog": "Подсилване на публикацията на {name}",
|
||||
"interaction_modal.title.reply": "Отговаряне на публикацията на {name}",
|
||||
"interaction_modal.title.vote": "Гласувайте в анкетата на {name}",
|
||||
"intervals.full.days": "{number, plural, one {# ден} other {# дни}}",
|
||||
"intervals.full.hours": "{number, plural, one {# час} other {# часа}}",
|
||||
"intervals.full.minutes": "{number, plural, one {# минута} other {# минути}}",
|
||||
|
@ -420,27 +453,32 @@
|
|||
"lightbox.close": "Затваряне",
|
||||
"lightbox.next": "Напред",
|
||||
"lightbox.previous": "Назад",
|
||||
"lightbox.zoom_in": "Увеличение до действителната големина",
|
||||
"lightbox.zoom_out": "Увеличение до побиране",
|
||||
"limited_account_hint.action": "Показване на профила въпреки това",
|
||||
"limited_account_hint.title": "Този профил е бил скрит от модераторите на {domain}.",
|
||||
"link_preview.author": "От {name}",
|
||||
"link_preview.more_from_author": "Още от {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} публикация} other {{counter} публикации}}",
|
||||
"lists.account.add": "Добавяне към списък",
|
||||
"lists.account.remove": "Премахване от списъка",
|
||||
"lists.create_list": "Създаване на списък",
|
||||
"lists.delete": "Изтриване на списъка",
|
||||
"lists.done": "Готово",
|
||||
"lists.edit": "Промяна на списъка",
|
||||
"lists.edit.submit": "Промяна на заглавие",
|
||||
"lists.exclusive": "Скриване на тези публикации от началото",
|
||||
"lists.new.create": "Добавяне на списък",
|
||||
"lists.new.title_placeholder": "Ново заглавие на списъка",
|
||||
"lists.find_users_to_add": "Намерете потребители за добавяне",
|
||||
"lists.list_members": "Списък членуващи",
|
||||
"lists.list_name": "Име на списък",
|
||||
"lists.no_lists_yet": "Още няма списъци.",
|
||||
"lists.no_members_yet": "Още няма членуващи.",
|
||||
"lists.no_results_found": "Няма намерени резултати.",
|
||||
"lists.remove_member": "Премахване",
|
||||
"lists.replies_policy.followed": "Някой последван потребител",
|
||||
"lists.replies_policy.list": "Членуващите в списъка",
|
||||
"lists.replies_policy.none": "Никого",
|
||||
"lists.replies_policy.title": "Показване на отговори на:",
|
||||
"lists.search": "Търсене измежду последваните",
|
||||
"lists.subheading": "Вашите списъци",
|
||||
"lists.save": "Запазване",
|
||||
"lists.search_placeholder": "Търсене сред, които сте последвали",
|
||||
"load_pending": "{count, plural, one {# нов елемент} other {# нови елемента}}",
|
||||
"loading_indicator.label": "Зареждане…",
|
||||
"media_gallery.hide": "Скриване",
|
||||
"moved_to_account_banner.text": "Вашият акаунт {disabledAccount} сега е изключен, защото се преместихте в {movedToAccount}.",
|
||||
"mute_modal.hide_from_notifications": "Скриване от известията",
|
||||
"mute_modal.hide_options": "Скриване на възможностите",
|
||||
|
@ -489,6 +527,7 @@
|
|||
"notification.favourite": "{name} направи любима публикацията ви",
|
||||
"notification.favourite.name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> направиха любима ваша публикация",
|
||||
"notification.follow": "{name} ви последва",
|
||||
"notification.follow.name_and_others": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> ви последваха",
|
||||
"notification.follow_request": "{name} поиска да ви последва",
|
||||
"notification.follow_request.name_and_others": "{name} и {count, plural, one {# друг} other {# други}} поискаха да ви последват",
|
||||
"notification.label.mention": "Споменаване",
|
||||
|
@ -496,6 +535,7 @@
|
|||
"notification.label.private_reply": "Личен отговор",
|
||||
"notification.label.reply": "Отговор",
|
||||
"notification.mention": "Споменаване",
|
||||
"notification.mentioned_you": "{name} ви спомена",
|
||||
"notification.moderation-warning.learn_more": "Научете повече",
|
||||
"notification.moderation_warning": "Получихте предупреждение за модериране",
|
||||
"notification.moderation_warning.action_delete_statuses": "Някои от публикациите ви са премахнати.",
|
||||
|
@ -566,6 +606,7 @@
|
|||
"notifications.permission_required": "Известията на работния плот ги няма, щото няма дадено нужното позволение.",
|
||||
"notifications.policy.accept": "Приемам",
|
||||
"notifications.policy.accept_hint": "Показване в известия",
|
||||
"notifications.policy.drop_hint": "Изпращане в празнотата, за да не се видим никога пак",
|
||||
"notifications.policy.filter": "Филтър",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Ограничено от модераторите на сървъра",
|
||||
"notifications.policy.filter_limited_accounts_title": "Модерирани акаунти",
|
||||
|
@ -611,7 +652,7 @@
|
|||
"onboarding.steps.follow_people.title": "Персонализиране на началния ви инфоканал",
|
||||
"onboarding.steps.publish_status.body": "Поздравете целия свят.",
|
||||
"onboarding.steps.publish_status.title": "Направете първата си публикация",
|
||||
"onboarding.steps.setup_profile.body": "Други са по-вероятно да взаимодействат с вас с попълнения профил.",
|
||||
"onboarding.steps.setup_profile.body": "Подсилете взаимодействията си, имайки изчерпателен профил.",
|
||||
"onboarding.steps.setup_profile.title": "Пригодете профила си",
|
||||
"onboarding.steps.share_profile.body": "Позволете на приятелите си да знаят как да ви намират в Mastodon!",
|
||||
"onboarding.steps.share_profile.title": "Споделяне на профила ви",
|
||||
|
@ -752,6 +793,7 @@
|
|||
"status.bookmark": "Отмятане",
|
||||
"status.cancel_reblog_private": "Край на подсилването",
|
||||
"status.cannot_reblog": "Публикацията не може да се подсилва",
|
||||
"status.continued_thread": "Продължена нишка",
|
||||
"status.copy": "Копиране на връзката към публикация",
|
||||
"status.delete": "Изтриване",
|
||||
"status.detailed_status": "Подробен изглед на разговора",
|
||||
|
@ -760,6 +802,7 @@
|
|||
"status.edit": "Редактиране",
|
||||
"status.edited": "Последно редактирано на {date}",
|
||||
"status.edited_x_times": "Редактирано {count, plural,one {{count} път} other {{count} пъти}}",
|
||||
"status.embed": "Вземане на кода за вграждане",
|
||||
"status.favourite": "Любимо",
|
||||
"status.favourites": "{count, plural, one {любимо} other {любими}}",
|
||||
"status.filter": "Филтриране на публ.",
|
||||
|
@ -784,6 +827,7 @@
|
|||
"status.reblogs.empty": "Още никого не е подсилвал публикацията. Подсилващият ще се покаже тук.",
|
||||
"status.redraft": "Изтриване и преработване",
|
||||
"status.remove_bookmark": "Премахване на отметката",
|
||||
"status.replied_in_thread": "Отговорено в нишката",
|
||||
"status.replied_to": "В отговор до {name}",
|
||||
"status.reply": "Отговор",
|
||||
"status.replyAll": "Отговор на нишка",
|
||||
|
@ -821,6 +865,11 @@
|
|||
"upload_error.poll": "Качването на файлове не е позволено с анкети.",
|
||||
"upload_form.audio_description": "Опишете за хора, които са глухи или трудно чуват",
|
||||
"upload_form.description": "Опишете за хора, които са слепи или имат слабо зрение",
|
||||
"upload_form.drag_and_drop.instructions": "Натиснете интервал или enter, за да подберете мултимедийно прикачване. Провлачвайки, ползвайте клавишите със стрелки, за да премествате мултимедията във всяка дадена посока. Натиснете пак интервал или enter, за да се стовари мултимедийното прикачване в новото си положение или натиснете Esc за отмяна.",
|
||||
"upload_form.drag_and_drop.on_drag_cancel": "Провлачването е отменено. Мултимедийното прикачване {item} е спуснато.",
|
||||
"upload_form.drag_and_drop.on_drag_end": "Мултимедийното прикачване {item} е спуснато.",
|
||||
"upload_form.drag_and_drop.on_drag_over": "Мултимедийното прикачване {item} е преместено.",
|
||||
"upload_form.drag_and_drop.on_drag_start": "Избрано мултимедийно прикачване {item}.",
|
||||
"upload_form.edit": "Редактиране",
|
||||
"upload_form.thumbnail": "Промяна на миниобраза",
|
||||
"upload_form.video_description": "Опишете за хора, които са глухи или трудно чуват, слепи или имат слабо зрение",
|
||||
|
|
|
@ -202,7 +202,6 @@
|
|||
"empty_column.hashtag": "এই হেসটাগে এখনো কিছু নেই।",
|
||||
"empty_column.home": "আপনার বাড়ির সময়রেখা এখনো খালি! {public} এ ঘুরে আসুন অথবা অনুসন্ধান বেবহার করে শুরু করতে পারেন এবং অন্য ব্যবহারকারীদের সাথে সাক্ষাৎ করতে পারেন।",
|
||||
"empty_column.list": "এই তালিকাতে এখনো কিছু নেই. যখন এই তালিকায় থাকা ব্যবহারকারী নতুন কিছু লিখবে, সেগুলো এখানে পাওয়া যাবে।",
|
||||
"empty_column.lists": "আপনার এখনো কোনো তালিকা তৈরী নেই। যদি বা যখন তৈরী করেন, সেগুলো এখানে পাওয়া যাবে।",
|
||||
"empty_column.mutes": "আপনি এখনো কোনো ব্যবহারকারীকে নিঃশব্দ করেননি।",
|
||||
"empty_column.notifications": "আপনার এখনো কোনো প্রজ্ঞাপন নেই। কথোপকথন শুরু করতে, অন্যদের সাথে মেলামেশা করতে পারেন।",
|
||||
"empty_column.public": "এখানে এখনো কিছু নেই! প্রকাশ্য ভাবে কিছু লিখুন বা অন্য সার্ভার থেকে কাওকে অনুসরণ করে এই জায়গা ভরে ফেলুন",
|
||||
|
@ -277,16 +276,9 @@
|
|||
"lightbox.next": "পরবর্তী",
|
||||
"lightbox.previous": "পূর্ববর্তী",
|
||||
"link_preview.author": "{name} এর লিখা",
|
||||
"lists.account.add": "তালিকাতে যুক্ত করতে",
|
||||
"lists.account.remove": "তালিকা থেকে বাদ দিতে",
|
||||
"lists.delete": "তালিকা মুছে ফেলতে",
|
||||
"lists.edit": "তালিকা সম্পাদনা করতে",
|
||||
"lists.edit.submit": "শিরোনাম সম্পাদনা করতে",
|
||||
"lists.new.create": "তালিকাতে যুক্ত করতে",
|
||||
"lists.new.title_placeholder": "তালিকার নতুন শিরোনাম দিতে",
|
||||
"lists.replies_policy.none": "কেউ না",
|
||||
"lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
|
||||
"lists.subheading": "আপনার তালিকা",
|
||||
"load_pending": "{count, plural, one {# নতুন জিনিস} other {# নতুন জিনিস}}",
|
||||
"navigation_bar.about": "পরিচিতি",
|
||||
"navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী",
|
||||
|
|
|
@ -82,6 +82,8 @@
|
|||
"alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.",
|
||||
"alert.unexpected.title": "Hopala !",
|
||||
"announcement.announcement": "Kemennad",
|
||||
"annual_report.summary.followers.followers": "heulier",
|
||||
"annual_report.summary.highlighted_post.possessive": "{name}",
|
||||
"attachments_list.unprocessed": "(ket meret)",
|
||||
"audio.hide": "Kuzhat ar c'hleved",
|
||||
"block_modal.show_less": "Diskouez nebeutoc'h",
|
||||
|
@ -221,7 +223,6 @@
|
|||
"empty_column.hashtag": "N'eus netra en hashtag-mañ c'hoazh.",
|
||||
"empty_column.home": "Goullo eo ho red-amzer degemer! Kit da weladenniñ {public} pe implijit ar c'hlask evit kregiñ ganti ha kejañ gant implijer·ien·ezed all.",
|
||||
"empty_column.list": "Goullo eo al listenn-mañ evit c'hoazh. Pa vo embannet toudoù nevez gant e izili e teuint war wel amañ.",
|
||||
"empty_column.lists": "N'ho peus roll ebet c'hoazh. Pa vo krouet unan ganeoc'h e vo diskouezet amañ.",
|
||||
"empty_column.mutes": "N'ho peus kuzhet implijer ebet c'hoazh.",
|
||||
"empty_column.notifications": "N'ho peus kemenn ebet c'hoazh. Grit gant implijer·ezed·ien all evit loc'hañ ar gomz.",
|
||||
"empty_column.public": "N'eus netra amañ! Skrivit un dra bennak foran pe heuilhit implijer·ien·ezed eus dafariadoù all evit leuniañ",
|
||||
|
@ -344,19 +345,11 @@
|
|||
"limited_account_hint.action": "Diskouez an aelad memes tra",
|
||||
"limited_account_hint.title": "Kuzhet eo bet ar profil-mañ gant an evezhierien eus {domain}.",
|
||||
"link_preview.author": "Gant {name}",
|
||||
"lists.account.add": "Ouzhpennañ d'al listenn",
|
||||
"lists.account.remove": "Lemel kuit eus al listenn",
|
||||
"lists.delete": "Dilemel al listenn",
|
||||
"lists.edit": "Kemmañ al listenn",
|
||||
"lists.edit.submit": "Cheñch an titl",
|
||||
"lists.new.create": "Ouzhpennañ ul listenn",
|
||||
"lists.new.title_placeholder": "Titl nevez al listenn",
|
||||
"lists.replies_policy.followed": "Pep implijer.ez heuliet",
|
||||
"lists.replies_policy.list": "Izili ar roll",
|
||||
"lists.replies_policy.none": "Den ebet",
|
||||
"lists.replies_policy.title": "Diskouez ar respontoù:",
|
||||
"lists.search": "Klask e-touez tud heuliet ganeoc'h",
|
||||
"lists.subheading": "Ho listennoù",
|
||||
"load_pending": "{count, plural, one {# dra nevez} other {# dra nevez}}",
|
||||
"loading_indicator.label": "O kargañ…",
|
||||
"navigation_bar.about": "Diwar-benn",
|
||||
|
|
|
@ -87,6 +87,19 @@
|
|||
"alert.unexpected.title": "Vaja!",
|
||||
"alt_text_badge.title": "Text alternatiu",
|
||||
"announcement.announcement": "Anunci",
|
||||
"annual_report.summary.archetype.oracle": "L'Oracle",
|
||||
"annual_report.summary.followers.followers": "seguidors",
|
||||
"annual_report.summary.followers.total": "{count} en total",
|
||||
"annual_report.summary.here_it_is": "El repàs del vostre {year}:",
|
||||
"annual_report.summary.highlighted_post.by_favourites": "la publicació més afavorida",
|
||||
"annual_report.summary.highlighted_post.by_reblogs": "la publicació més impulsada",
|
||||
"annual_report.summary.highlighted_post.by_replies": "la publicació amb més respostes",
|
||||
"annual_report.summary.highlighted_post.possessive": "de {name}",
|
||||
"annual_report.summary.most_used_app.most_used_app": "l'aplicació més utilitzada",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "l'etiqueta més utilitzada",
|
||||
"annual_report.summary.most_used_hashtag.none": "Cap",
|
||||
"annual_report.summary.new_posts.new_posts": "publicacions noves",
|
||||
"annual_report.summary.thanks": "Gràcies per formar part de Mastodon!",
|
||||
"attachments_list.unprocessed": "(sense processar)",
|
||||
"audio.hide": "Amaga l'àudio",
|
||||
"block_modal.remote_users_caveat": "Li demanarem al servidor {domain} que respecti la vostra decisió, tot i que no podem garantir-ho, ja que alguns servidors gestionen de forma diferent els blocatges. És possible que els usuaris no autenticats puguin veure les publicacions públiques.",
|
||||
|
@ -273,7 +286,6 @@
|
|||
"empty_column.hashtag": "Encara no hi ha res en aquesta etiqueta.",
|
||||
"empty_column.home": "La teva línia de temps és buida! Segueix més gent per a emplenar-la. {suggestions}",
|
||||
"empty_column.list": "Encara no hi ha res en aquesta llista. Quan els membres facin nous tuts, apareixeran aquí.",
|
||||
"empty_column.lists": "Encara no tens cap llista. Quan en facis una, apareixerà aquí.",
|
||||
"empty_column.mutes": "Encara no has silenciat cap usuari.",
|
||||
"empty_column.notification_requests": "Tot net, ja no hi ha res aquí! Quan rebeu notificacions noves, segons la vostra configuració, apareixeran aquí.",
|
||||
"empty_column.notifications": "Encara no tens notificacions. Quan altre gent interactuï amb tu, les veuràs aquí.",
|
||||
|
@ -446,20 +458,11 @@
|
|||
"link_preview.author": "Per {name}",
|
||||
"link_preview.more_from_author": "Més de {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} publicació} other {{counter} publicacions}}",
|
||||
"lists.account.add": "Afegeix a la llista",
|
||||
"lists.account.remove": "Elimina de la llista",
|
||||
"lists.delete": "Elimina la llista",
|
||||
"lists.edit": "Edita la llista",
|
||||
"lists.edit.submit": "Canvia el títol",
|
||||
"lists.exclusive": "Amaga aquests tuts a Inici",
|
||||
"lists.new.create": "Afegeix una llista",
|
||||
"lists.new.title_placeholder": "Nou títol de la llista",
|
||||
"lists.replies_policy.followed": "Qualsevol usuari que segueixis",
|
||||
"lists.replies_policy.list": "Membres de la llista",
|
||||
"lists.replies_policy.none": "Ningú",
|
||||
"lists.replies_policy.title": "Mostra respostes a:",
|
||||
"lists.search": "Cerca entre les persones que segueixes",
|
||||
"lists.subheading": "Les teves llistes",
|
||||
"load_pending": "{count, plural, one {# element nou} other {# elements nous}}",
|
||||
"loading_indicator.label": "Es carrega…",
|
||||
"media_gallery.hide": "Amaga",
|
||||
|
|
|
@ -221,7 +221,6 @@
|
|||
"empty_column.hashtag": "هێشتا هیچ شتێک لەم هاشتاگەدا نییە.",
|
||||
"empty_column.home": "تایم لاینی ماڵەوەت بەتاڵە! سەردانی {public} بکە یان گەڕان بەکاربێنە بۆ دەستپێکردن و بینینی بەکارهێنەرانی تر.",
|
||||
"empty_column.list": "هێشتا هیچ شتێک لەم لیستەدا نییە. کاتێک ئەندامانی ئەم لیستە دەنگی نوێ بڵاودەکەن، لێرە دەردەکەون.",
|
||||
"empty_column.lists": "تۆ هێشتا هیچ لیستت دروست نەکردووە، کاتێک دانەیەک دروست دەکەیت، لێرە پیشان دەدرێت.",
|
||||
"empty_column.mutes": "تۆ هێشتا هیچ بەکارهێنەرێکت بێدەنگ نەکردووە.",
|
||||
"empty_column.notifications": "تۆ هێشتا هیچ ئاگانامێکت نیە. چالاکی لەگەڵ کەسانی دیکە بکە بۆ دەستپێکردنی گفتوگۆکە.",
|
||||
"empty_column.public": "لێرە هیچ نییە! شتێک بە ئاشکرا بنووسە(بەگشتی)، یان بە دەستی شوێن بەکارهێنەران بکەوە لە ڕاژەکانی ترەوە بۆ پڕکردنەوەی",
|
||||
|
@ -338,19 +337,11 @@
|
|||
"lightbox.previous": "پێشوو",
|
||||
"limited_account_hint.action": "بەهەر حاڵ پڕۆفایلی پیشان بدە",
|
||||
"limited_account_hint.title": "ئەم پرۆفایلە لەلایەن بەڕێوەبەرانی {domain} شاراوەتەوە.",
|
||||
"lists.account.add": "زیادکردن بۆ لیست",
|
||||
"lists.account.remove": "لابردن لە لیست",
|
||||
"lists.delete": "سڕینەوەی لیست",
|
||||
"lists.edit": "دەستکاری لیست",
|
||||
"lists.edit.submit": "گۆڕینی ناونیشان",
|
||||
"lists.new.create": "زیادکردنی لیست",
|
||||
"lists.new.title_placeholder": "ناونیشانی لیستی نوێ",
|
||||
"lists.replies_policy.followed": "هەر بەکارهێنەرێکی بەدواکەوتوو",
|
||||
"lists.replies_policy.list": "ئەندامانی لیستەکە",
|
||||
"lists.replies_policy.none": "هیچکەس",
|
||||
"lists.replies_policy.title": "پیشاندانی وەڵامەکان بۆ:",
|
||||
"lists.search": "بگەڕێ لەناو ئەو کەسانەی کە شوێنیان کەوتویت",
|
||||
"lists.subheading": "لیستەکانت",
|
||||
"load_pending": "{count, plural, one {# بەڕگەی نوێ} other {# بەڕگەی نوێ}}",
|
||||
"moved_to_account_banner.text": "ئەکاونتەکەت {disabledAccount} لە ئێستادا لەکارخراوە چونکە تۆ چوویتە {movedToAccount}.",
|
||||
"navigation_bar.about": "دەربارە",
|
||||
|
|
|
@ -132,7 +132,6 @@
|
|||
"empty_column.hashtag": "Ùn c'hè ancu nunda quì.",
|
||||
"empty_column.home": "A vostr'accolta hè viota! Pudete andà nant'à {public} o pruvà a ricerca per truvà parsone da siguità.",
|
||||
"empty_column.list": "Ùn c'hè ancu nunda quì. Quandu membri di sta lista manderanu novi statuti, i vidarete quì.",
|
||||
"empty_column.lists": "Ùn avete manc'una lista. Quandu farete una, sarà mustrata quì.",
|
||||
"empty_column.mutes": "Per avà ùn avete manc'un utilizatore piattatu.",
|
||||
"empty_column.notifications": "Ùn avete ancu nisuna nutificazione. Interact with others to start the conversation.",
|
||||
"empty_column.public": "Ùn c'hè nunda quì! Scrivete qualcosa in pubblicu o seguitate utilizatori d'altri servori per empie a linea pubblica",
|
||||
|
@ -198,19 +197,11 @@
|
|||
"lightbox.close": "Chjudà",
|
||||
"lightbox.next": "Siguente",
|
||||
"lightbox.previous": "Pricidente",
|
||||
"lists.account.add": "Aghjunghje à a lista",
|
||||
"lists.account.remove": "Toglie di a lista",
|
||||
"lists.delete": "Toglie a lista",
|
||||
"lists.edit": "Mudificà a lista",
|
||||
"lists.edit.submit": "Cambià u titulu",
|
||||
"lists.new.create": "Aghjunghje",
|
||||
"lists.new.title_placeholder": "Titulu di a lista",
|
||||
"lists.replies_policy.followed": "Tutti i vostri abbunamenti",
|
||||
"lists.replies_policy.list": "Membri di a lista",
|
||||
"lists.replies_policy.none": "Nimu",
|
||||
"lists.replies_policy.title": "Vede e risposte à:",
|
||||
"lists.search": "Circà indè i vostr'abbunamenti",
|
||||
"lists.subheading": "E vo liste",
|
||||
"load_pending": "{count, plural, one {# entrata nova} other {# entrate nove}}",
|
||||
"navigation_bar.blocks": "Utilizatori bluccati",
|
||||
"navigation_bar.bookmarks": "Segnalibri",
|
||||
|
|
|
@ -267,7 +267,6 @@
|
|||
"empty_column.hashtag": "Pod tímto hashtagem zde zatím nic není.",
|
||||
"empty_column.home": "Vaše domovská časová osa je prázdná! Naplňte ji sledováním dalších lidí.",
|
||||
"empty_column.list": "V tomto seznamu zatím nic není. Až nějaký člen z tohoto seznamu zveřejní nový příspěvek, objeví se zde.",
|
||||
"empty_column.lists": "Zatím nemáte žádné seznamy. Až nějaký vytvoříte, zobrazí se zde.",
|
||||
"empty_column.mutes": "Zatím jste neskryli žádného uživatele.",
|
||||
"empty_column.notification_requests": "Vyčištěno! Nic tu není. Jakmile obdržíš nové notifikace, objeví se zde podle tvého nastavení.",
|
||||
"empty_column.notifications": "Zatím nemáte žádná oznámení. Až s vámi někdo bude interagovat, uvidíte to zde.",
|
||||
|
@ -415,20 +414,11 @@
|
|||
"link_preview.author": "Podle {name}",
|
||||
"link_preview.more_from_author": "Více od {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}",
|
||||
"lists.account.add": "Přidat do seznamu",
|
||||
"lists.account.remove": "Odebrat ze seznamu",
|
||||
"lists.delete": "Smazat seznam",
|
||||
"lists.edit": "Upravit seznam",
|
||||
"lists.edit.submit": "Změnit název",
|
||||
"lists.exclusive": "Skrýt tyto příspěvky z domovské stránky",
|
||||
"lists.new.create": "Přidat seznam",
|
||||
"lists.new.title_placeholder": "Název nového seznamu",
|
||||
"lists.replies_policy.followed": "Sledovaným uživatelům",
|
||||
"lists.replies_policy.list": "Členům seznamu",
|
||||
"lists.replies_policy.none": "Nikomu",
|
||||
"lists.replies_policy.title": "Odpovědi zobrazovat:",
|
||||
"lists.search": "Hledejte mezi lidmi, které sledujete",
|
||||
"lists.subheading": "Vaše seznamy",
|
||||
"load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}",
|
||||
"loading_indicator.label": "Načítání…",
|
||||
"moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.",
|
||||
|
|
|
@ -87,11 +87,30 @@
|
|||
"alert.unexpected.title": "Wps!",
|
||||
"alt_text_badge.title": "Testun Amgen",
|
||||
"announcement.announcement": "Cyhoeddiad",
|
||||
"annual_report.summary.archetype.booster": "Y hyrwyddwr",
|
||||
"annual_report.summary.archetype.lurker": "Yr arsylwr",
|
||||
"annual_report.summary.archetype.oracle": "Yr oracl",
|
||||
"annual_report.summary.archetype.pollster": "Yr arholwr",
|
||||
"annual_report.summary.archetype.replier": "Y sbardunwr",
|
||||
"annual_report.summary.followers.followers": "dilynwyr",
|
||||
"annual_report.summary.followers.total": "{count} cyfanswm",
|
||||
"annual_report.summary.here_it_is": "Dyma eich {year} yn gryno:",
|
||||
"annual_report.summary.highlighted_post.by_favourites": "postiad wedi'i ffefrynu fwyaf",
|
||||
"annual_report.summary.highlighted_post.by_reblogs": "postiad wedi'i hybu fwyaf",
|
||||
"annual_report.summary.highlighted_post.by_replies": "postiad gyda'r ymatebion mwyaf",
|
||||
"annual_report.summary.highlighted_post.possessive": "{name}",
|
||||
"annual_report.summary.most_used_app.most_used_app": "ap a ddefnyddiwyd fwyaf",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "hashnod a ddefnyddiwyd fwyaf",
|
||||
"annual_report.summary.most_used_hashtag.none": "Dim",
|
||||
"annual_report.summary.new_posts.new_posts": "postiadau newydd",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Rydych chi yn y </topLabel><percentage></percentage><bottomLabel>mwyaf o ddefnyddwyr Mastodon.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "Ni fyddwn yn dweud wrth Bernie.",
|
||||
"annual_report.summary.thanks": "Diolch am fod yn rhan o Mastodon!",
|
||||
"attachments_list.unprocessed": "(heb eu prosesu)",
|
||||
"audio.hide": "Cuddio sain",
|
||||
"block_modal.remote_users_caveat": "Byddwn yn gofyn i'r gweinydd {domain} barchu eich penderfyniad. Fodd bynnag, nid yw cydymffurfiad wedi'i warantu gan y gall rhai gweinyddwyr drin rhwystro mewn ffyrdd gwahanol. Mae'n bosibl y bydd postiadau cyhoeddus yn dal i fod yn weladwy i ddefnyddwyr nad ydynt wedi mewngofnodi.",
|
||||
"block_modal.show_less": "Dangos llai",
|
||||
"block_modal.show_more": "Dangos mwy",
|
||||
"block_modal.show_more": "Dangos rhagor",
|
||||
"block_modal.they_cant_mention": "Nid ydynt yn gallu eich crybwyll na'ch dilyn.",
|
||||
"block_modal.they_cant_see_posts": "Nid ydynt yn gallu gweld eich postiadau ac ni fyddwch yn gweld eu rhai hwy.",
|
||||
"block_modal.they_will_know": "Gallant weld eu bod wedi'u rhwystro.",
|
||||
|
@ -163,9 +182,9 @@
|
|||
"compose_form.poll.switch_to_single": "Newid pleidlais i gyfyngu i un dewis",
|
||||
"compose_form.poll.type": "Arddull",
|
||||
"compose_form.publish": "Postiad",
|
||||
"compose_form.publish_form": "Cyhoeddi",
|
||||
"compose_form.publish_form": "Postiad newydd",
|
||||
"compose_form.reply": "Ateb",
|
||||
"compose_form.save_changes": "Diweddariad",
|
||||
"compose_form.save_changes": "Diweddaru",
|
||||
"compose_form.spoiler.marked": "Dileu rhybudd cynnwys",
|
||||
"compose_form.spoiler.unmarked": "Ychwanegu rhybudd cynnwys",
|
||||
"compose_form.spoiler_placeholder": "Rhybudd cynnwys (dewisol)",
|
||||
|
@ -273,7 +292,6 @@
|
|||
"empty_column.hashtag": "Nid oes dim ar yr hashnod hwn eto.",
|
||||
"empty_column.home": "Mae eich ffrwd gartref yn wag! Dilynwch fwy o bobl i'w llenwi.",
|
||||
"empty_column.list": "Does dim yn y rhestr yma eto. Pan fydd aelodau'r rhestr yn cyhoeddi postiad newydd, mi fydd yn ymddangos yma.",
|
||||
"empty_column.lists": "Nid oes gennych unrhyw restrau eto. Pan fyddwch yn creu un, mi fydd yn ymddangos yma.",
|
||||
"empty_column.mutes": "Nid ydych wedi tewi unrhyw ddefnyddwyr eto.",
|
||||
"empty_column.notification_requests": "Dim i boeni amdano! Does dim byd yma. Pan fyddwch yn derbyn hysbysiadau newydd, byddan nhw'n ymddangos yma yn ôl eich gosodiadau.",
|
||||
"empty_column.notifications": "Nid oes gennych unrhyw hysbysiadau eto. Rhyngweithiwch ag eraill i ddechrau'r sgwrs.",
|
||||
|
@ -446,20 +464,11 @@
|
|||
"link_preview.author": "Gan {name}",
|
||||
"link_preview.more_from_author": "Mwy gan {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} postiad } two {{counter} bostiad } few {{counter} postiad} many {{counter} postiad} other {{counter} postiad}}",
|
||||
"lists.account.add": "Ychwanegu at restr",
|
||||
"lists.account.remove": "Tynnu o'r rhestr",
|
||||
"lists.delete": "Dileu rhestr",
|
||||
"lists.edit": "Golygu rhestr",
|
||||
"lists.edit.submit": "Newid teitl",
|
||||
"lists.exclusive": "Cuddio'r postiadau hyn o'r ffrwd gartref",
|
||||
"lists.new.create": "Ychwanegu rhestr",
|
||||
"lists.new.title_placeholder": "Teitl rhestr newydd",
|
||||
"lists.replies_policy.followed": "Unrhyw ddefnyddiwr sy'n cael ei ddilyn",
|
||||
"lists.replies_policy.list": "Aelodau'r rhestr",
|
||||
"lists.replies_policy.none": "Neb",
|
||||
"lists.replies_policy.title": "Dangos atebion i:",
|
||||
"lists.search": "Chwilio ymysg pobl rydych yn eu dilyn",
|
||||
"lists.subheading": "Eich rhestrau",
|
||||
"load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
|
||||
"loading_indicator.label": "Yn llwytho…",
|
||||
"media_gallery.hide": "Cuddio",
|
||||
|
@ -508,6 +517,8 @@
|
|||
"notification.admin.report_statuses_other": "Adroddodd {name} {target}",
|
||||
"notification.admin.sign_up": "Cofrestrodd {name}",
|
||||
"notification.admin.sign_up.name_and_others": "Cofrestrodd {name} {count, plural, one {ac # arall} other {a # arall}}",
|
||||
"notification.annual_report.message": "Mae eich #Wrapstodon {year} yn aros i chi! Gwelwch eich uchafbwyntiau ac amseroedd i'w cofio o'r flwyddyn hon ar Mastodon!",
|
||||
"notification.annual_report.view": "Gweld #Wrapstodon",
|
||||
"notification.favourite": "Ffafriodd {name} eich postiad",
|
||||
"notification.favourite.name_and_others_with_link": "Ffafriodd {name} a <a>{count, plural, one {# arall} other {# arall}}</a> eich postiad",
|
||||
"notification.follow": "Dilynodd {name} chi",
|
||||
|
|
|
@ -87,6 +87,25 @@
|
|||
"alert.unexpected.title": "Ups!",
|
||||
"alt_text_badge.title": "Alt text",
|
||||
"announcement.announcement": "Bekendtgørelse",
|
||||
"annual_report.summary.archetype.booster": "Cool-hunter",
|
||||
"annual_report.summary.archetype.lurker": "Lurker",
|
||||
"annual_report.summary.archetype.oracle": "Oracle",
|
||||
"annual_report.summary.archetype.pollster": "Pollster",
|
||||
"annual_report.summary.archetype.replier": "Social butterfly",
|
||||
"annual_report.summary.followers.followers": "følgere",
|
||||
"annual_report.summary.followers.total": "{count} i alt",
|
||||
"annual_report.summary.here_it_is": "Her er {year} i sammendrag:",
|
||||
"annual_report.summary.highlighted_post.by_favourites": "mest favoritmarkerede indlæg",
|
||||
"annual_report.summary.highlighted_post.by_reblogs": "mest boostede indlæg",
|
||||
"annual_report.summary.highlighted_post.by_replies": "indlæg med flest svar",
|
||||
"annual_report.summary.highlighted_post.possessive": "{name}s",
|
||||
"annual_report.summary.most_used_app.most_used_app": "mest benyttede app",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "mest benyttede hashtag",
|
||||
"annual_report.summary.most_used_hashtag.none": "Intet",
|
||||
"annual_report.summary.new_posts.new_posts": "nye indlæg",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Det betyder, at man er i top</topLabel><percentage></percentage><bottomLabel>af Mastodon-brugere.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "Vi fortæller det ikke til Bernie.",
|
||||
"annual_report.summary.thanks": "Tak for at være en del af Mastodon!",
|
||||
"attachments_list.unprocessed": "(ubehandlet)",
|
||||
"audio.hide": "Skjul lyd",
|
||||
"block_modal.remote_users_caveat": "Serveren {domain} vil blive bedt om at respektere din beslutning. Overholdelse er dog ikke garanteret, da nogle servere kan håndtere blokke forskelligt. Offentlige indlæg kan stadig være synlige for ikke-indloggede brugere.",
|
||||
|
@ -121,13 +140,16 @@
|
|||
"column.blocks": "Blokerede brugere",
|
||||
"column.bookmarks": "Bogmærker",
|
||||
"column.community": "Lokal tidslinje",
|
||||
"column.create_list": "Opret liste",
|
||||
"column.direct": "Private omtaler",
|
||||
"column.directory": "Tjek profiler",
|
||||
"column.domain_blocks": "Blokerede domæner",
|
||||
"column.edit_list": "Redigér liste",
|
||||
"column.favourites": "Favoritter",
|
||||
"column.firehose": "Live feeds",
|
||||
"column.follow_requests": "Følgeanmodninger",
|
||||
"column.home": "Hjem",
|
||||
"column.list_members": "Håndtér listemedlemmer",
|
||||
"column.lists": "Lister",
|
||||
"column.mutes": "Skjulte brugere (mutede)",
|
||||
"column.notifications": "Notifikationer",
|
||||
|
@ -158,6 +180,7 @@
|
|||
"compose_form.poll.duration": "Afstemningens varighed",
|
||||
"compose_form.poll.multiple": "Multivalg",
|
||||
"compose_form.poll.option_placeholder": "Valgmulighed {number}",
|
||||
"compose_form.poll.single": "Enkeltvalg",
|
||||
"compose_form.poll.switch_to_multiple": "Ændr afstemning til flervalgstype",
|
||||
"compose_form.poll.switch_to_single": "Ændr afstemning til enkeltvalgstype",
|
||||
"compose_form.poll.type": "Stil",
|
||||
|
@ -272,7 +295,6 @@
|
|||
"empty_column.hashtag": "Der er intet med dette hashtag endnu.",
|
||||
"empty_column.home": "Din hjemmetidslinje er tom! Følg nogle personer, for at udfylde den. {suggestions}",
|
||||
"empty_column.list": "Der er ikke noget på denne liste endnu. Når medlemmer af listen udgiver nye indlæg vil de fremgå hér.",
|
||||
"empty_column.lists": "Du har endnu ingen lister. Når du opretter én, vil den fremgå hér.",
|
||||
"empty_column.mutes": "Du har endnu ikke skjult (muted) nogle brugere.",
|
||||
"empty_column.notification_requests": "Alt er klar! Der er intet her. Når der modtages nye notifikationer, fremgår de her jf. dine indstillinger.",
|
||||
"empty_column.notifications": "Du har endnu ingen notifikationer. Når andre interagerer med dig, vil det fremgå hér.",
|
||||
|
@ -445,20 +467,31 @@
|
|||
"link_preview.author": "Af {name}",
|
||||
"link_preview.more_from_author": "Mere fra {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}}",
|
||||
"lists.account.add": "Føj til liste",
|
||||
"lists.account.remove": "Fjern fra liste",
|
||||
"lists.add_member": "Tilføj",
|
||||
"lists.add_to_list": "Føj til liste",
|
||||
"lists.add_to_lists": "Føj {name} til lister",
|
||||
"lists.create": "Opret",
|
||||
"lists.create_a_list_to_organize": "Opret en ny liste til organisering af hjemmefeed",
|
||||
"lists.create_list": "Opret liste",
|
||||
"lists.delete": "Slet liste",
|
||||
"lists.done": "Færdig",
|
||||
"lists.edit": "Redigér liste",
|
||||
"lists.edit.submit": "Skift titel",
|
||||
"lists.exclusive": "Skjul disse indlæg hjemmefra",
|
||||
"lists.new.create": "Tilføj liste",
|
||||
"lists.new.title_placeholder": "Ny listetitel",
|
||||
"lists.exclusive": "Skjul medlemmer i Hjem",
|
||||
"lists.exclusive_hint": "Er nogen er på denne liste, skjul personen i hjemme-feeds for at undgå at se vedkommendes indlæg to gange.",
|
||||
"lists.find_users_to_add": "Find brugere at tilføje",
|
||||
"lists.list_members_count": "{count, plural, one {# medlem} other {# medlemmer}}",
|
||||
"lists.list_name": "Listetitel",
|
||||
"lists.new_list_name": "Ny listetitel",
|
||||
"lists.no_lists_yet": "Ingen lister endnu.",
|
||||
"lists.no_members_yet": "Ingen medlemmer endnu.",
|
||||
"lists.no_results_found": "Ingen resultater fundet.",
|
||||
"lists.remove_member": "Fjern",
|
||||
"lists.replies_policy.followed": "Enhver bruger, der følges",
|
||||
"lists.replies_policy.list": "Listemedlemmer",
|
||||
"lists.replies_policy.none": "Ingen",
|
||||
"lists.replies_policy.title": "Vis svar til:",
|
||||
"lists.search": "Søg blandt personer, som følges",
|
||||
"lists.subheading": "Dine lister",
|
||||
"lists.save": "Gem",
|
||||
"lists.search_placeholder": "Søg efter folk, man følger",
|
||||
"lists.show_replies_to": "Medtag svar fra listemedlemmer til",
|
||||
"load_pending": "{count, plural, one {# nyt emne} other {# nye emner}}",
|
||||
"loading_indicator.label": "Indlæser…",
|
||||
"media_gallery.hide": "Skjul",
|
||||
|
@ -507,6 +540,8 @@
|
|||
"notification.admin.report_statuses_other": "{name} anmeldte {target}",
|
||||
"notification.admin.sign_up": "{name} tilmeldte sig",
|
||||
"notification.admin.sign_up.name_and_others": "{name} og {count, plural, one {# anden} other {# andre}} tilmeldte sig",
|
||||
"notification.annual_report.message": "{year} #Wrapstodon venter! Afslør årets højdepunkter og mindeværdige øjeblikke på Mastodon!",
|
||||
"notification.annual_report.view": "Vis #Wrapstodon",
|
||||
"notification.favourite": "{name} favoritmarkerede dit indlæg",
|
||||
"notification.favourite.name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a> gjorde dit indlæg til favorit",
|
||||
"notification.follow": "{name} begyndte at følge dig",
|
||||
|
|
|
@ -87,6 +87,25 @@
|
|||
"alert.unexpected.title": "Oha!",
|
||||
"alt_text_badge.title": "Bildbeschreibung",
|
||||
"announcement.announcement": "Ankündigung",
|
||||
"annual_report.summary.archetype.booster": "Trendjäger*in",
|
||||
"annual_report.summary.archetype.lurker": "Beobachter*in",
|
||||
"annual_report.summary.archetype.oracle": "Orakel",
|
||||
"annual_report.summary.archetype.pollster": "Meinungsforscher*in",
|
||||
"annual_report.summary.archetype.replier": "Geselliger Schmetterling",
|
||||
"annual_report.summary.followers.followers": "Follower",
|
||||
"annual_report.summary.followers.total": "{count} insgesamt",
|
||||
"annual_report.summary.here_it_is": "Dein Jahresrückblick für {year}:",
|
||||
"annual_report.summary.highlighted_post.by_favourites": "am häufigsten favorisierter Beitrag",
|
||||
"annual_report.summary.highlighted_post.by_reblogs": "am häufigsten geteilter Beitrag",
|
||||
"annual_report.summary.highlighted_post.by_replies": "Beitrag mit den meisten Antworten",
|
||||
"annual_report.summary.highlighted_post.possessive": "{name}",
|
||||
"annual_report.summary.most_used_app.most_used_app": "am häufigsten verwendete App",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "am häufigsten verwendeter Hashtag",
|
||||
"annual_report.summary.most_used_hashtag.none": "Keiner",
|
||||
"annual_report.summary.new_posts.new_posts": "neue Beiträge",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Damit gehörst du zu den obersten</topLabel><percentage></percentage><bottomLabel>der Mastodon-Nutzer*innen.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "Wir werden Bernie nichts verraten.",
|
||||
"annual_report.summary.thanks": "Danke, dass du Teil von Mastodon bist!",
|
||||
"attachments_list.unprocessed": "(ausstehend)",
|
||||
"audio.hide": "Audio ausblenden",
|
||||
"block_modal.remote_users_caveat": "Wir werden den Server {domain} bitten, deine Entscheidung zu respektieren. Allerdings kann nicht garantiert werden, dass sie eingehalten wird, weil einige Server Blockierungen unterschiedlich handhaben können. Öffentliche Beiträge können für nicht angemeldete Nutzer*innen weiterhin sichtbar sein.",
|
||||
|
@ -121,13 +140,16 @@
|
|||
"column.blocks": "Blockierte Profile",
|
||||
"column.bookmarks": "Lesezeichen",
|
||||
"column.community": "Lokale Timeline",
|
||||
"column.create_list": "Liste erstellen",
|
||||
"column.direct": "Private Erwähnungen",
|
||||
"column.directory": "Profile durchsuchen",
|
||||
"column.domain_blocks": "Blockierte Domains",
|
||||
"column.edit_list": "Liste bearbeiten",
|
||||
"column.favourites": "Favoriten",
|
||||
"column.firehose": "Live-Feeds",
|
||||
"column.follow_requests": "Follower-Anfragen",
|
||||
"column.home": "Startseite",
|
||||
"column.list_members": "Listenmitglieder verwalten",
|
||||
"column.lists": "Listen",
|
||||
"column.mutes": "Stummgeschaltete Profile",
|
||||
"column.notifications": "Benachrichtigungen",
|
||||
|
@ -273,7 +295,6 @@
|
|||
"empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.",
|
||||
"empty_column.home": "Die Timeline deiner Startseite ist leer! Folge mehr Leuten, um sie zu füllen.",
|
||||
"empty_column.list": "Diese Liste ist derzeit leer. Wenn Konten auf dieser Liste neue Beiträge veröffentlichen, werden sie hier erscheinen.",
|
||||
"empty_column.lists": "Du hast noch keine Listen. Sobald du eine anlegst, wird sie hier erscheinen.",
|
||||
"empty_column.mutes": "Du hast keine Profile stummgeschaltet.",
|
||||
"empty_column.notification_requests": "Alles klar! Hier gibt es nichts. Wenn Sie neue Mitteilungen erhalten, werden diese entsprechend Ihren Einstellungen hier angezeigt.",
|
||||
"empty_column.notifications": "Du hast noch keine Benachrichtigungen. Sobald andere Personen mit dir interagieren, wirst du hier darüber informiert.",
|
||||
|
@ -446,20 +467,32 @@
|
|||
"link_preview.author": "Von {name}",
|
||||
"link_preview.more_from_author": "Mehr von {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
|
||||
"lists.account.add": "Zur Liste hinzufügen",
|
||||
"lists.account.remove": "Von der Liste entfernen",
|
||||
"lists.add_member": "Hinzufügen",
|
||||
"lists.add_to_list": "Zur Liste hinzufügen",
|
||||
"lists.add_to_lists": "{name} zu Listen hinzufügen",
|
||||
"lists.create": "Erstellen",
|
||||
"lists.create_a_list_to_organize": "Erstelle eine neue Liste, um deine Startseite zu organisieren",
|
||||
"lists.create_list": "Liste erstellen",
|
||||
"lists.delete": "Liste löschen",
|
||||
"lists.done": "Fertig",
|
||||
"lists.edit": "Liste bearbeiten",
|
||||
"lists.edit.submit": "Titel ändern",
|
||||
"lists.exclusive": "Diese Beiträge in der Startseite ausblenden",
|
||||
"lists.new.create": "Neue Liste erstellen",
|
||||
"lists.new.title_placeholder": "Titel der neuen Liste",
|
||||
"lists.exclusive": "Mitglieder auf der Startseite ausblenden",
|
||||
"lists.exclusive_hint": "Profile, die sich auf dieser Liste befinden, werden nicht auf deiner Startseite angezeigt, damit deren Beiträge nicht doppelt erscheinen.",
|
||||
"lists.find_users_to_add": "Suche nach Profilen, um sie hinzuzufügen",
|
||||
"lists.list_members": "Listenmitglieder",
|
||||
"lists.list_members_count": "{count, plural, one {# Mitglied} other {# Mitglieder}}",
|
||||
"lists.list_name": "Titel der Liste",
|
||||
"lists.new_list_name": "Neuer Listentitel",
|
||||
"lists.no_lists_yet": "Noch keine Listen vorhanden.",
|
||||
"lists.no_members_yet": "Keine Mitglieder vorhanden.",
|
||||
"lists.no_results_found": "Keine Suchergebnisse.",
|
||||
"lists.remove_member": "Entfernen",
|
||||
"lists.replies_policy.followed": "Alle folgenden Profile",
|
||||
"lists.replies_policy.list": "Mitglieder der Liste",
|
||||
"lists.replies_policy.none": "Niemanden",
|
||||
"lists.replies_policy.title": "Antworten anzeigen für:",
|
||||
"lists.search": "Suche nach Leuten, denen du folgst",
|
||||
"lists.subheading": "Deine Listen",
|
||||
"lists.save": "Speichern",
|
||||
"lists.search_placeholder": "Nach Profilen suchen, denen du folgst",
|
||||
"lists.show_replies_to": "Antworten von Listenmitgliedern anzeigen für …",
|
||||
"load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}",
|
||||
"loading_indicator.label": "Wird geladen …",
|
||||
"media_gallery.hide": "Ausblenden",
|
||||
|
@ -467,12 +500,12 @@
|
|||
"mute_modal.hide_from_notifications": "Benachrichtigungen ausblenden",
|
||||
"mute_modal.hide_options": "Einstellungen ausblenden",
|
||||
"mute_modal.indefinite": "Bis ich die Stummschaltung aufhebe",
|
||||
"mute_modal.show_options": "Einstellungen anzeigen",
|
||||
"mute_modal.show_options": "Optionen anzeigen",
|
||||
"mute_modal.they_can_mention_and_follow": "Das Profil wird dich weiterhin erwähnen und dir folgen können, aber du wirst davon nichts sehen.",
|
||||
"mute_modal.they_wont_know": "Es wird nicht erkennbar sein, dass dieses Profil stummgeschaltet wurde.",
|
||||
"mute_modal.they_wont_know": "Das Profil wird nicht erkennen können, dass du es stummgeschaltet hast.",
|
||||
"mute_modal.title": "Profil stummschalten?",
|
||||
"mute_modal.you_wont_see_mentions": "Du wirst keine Beiträge sehen, die dieses Profil erwähnen.",
|
||||
"mute_modal.you_wont_see_posts": "Deine Beiträge können weiterhin angesehen werden, aber du wirst deren Beiträge nicht mehr sehen.",
|
||||
"mute_modal.you_wont_see_posts": "Deine Beiträge können von diesem stummgeschalteten Profil weiterhin gesehen werden, aber du wirst dessen Beiträge nicht mehr sehen.",
|
||||
"navigation_bar.about": "Über",
|
||||
"navigation_bar.administration": "Administration",
|
||||
"navigation_bar.advanced_interface": "Im erweiterten Webinterface öffnen",
|
||||
|
@ -507,13 +540,15 @@
|
|||
"notification.admin.report_statuses": "{name} meldete {target} wegen {category}",
|
||||
"notification.admin.report_statuses_other": "{name} meldete {target}",
|
||||
"notification.admin.sign_up": "{name} registrierte sich",
|
||||
"notification.admin.sign_up.name_and_others": "{name} und {count, plural, one {# weitere Person} other {# weitere Personen}} registrierten sich",
|
||||
"notification.admin.sign_up.name_and_others": "{name} und {count, plural, one {# weiteres Profil} other {# weitere Profile}} registrierten sich",
|
||||
"notification.annual_report.message": "Dein {year} #Wrapstodon erwartet dich! Lass deine Highlights und unvergesslichen Momente auf Mastodon erneut aufleben!",
|
||||
"notification.annual_report.view": "#Wrapstodon ansehen",
|
||||
"notification.favourite": "{name} favorisierte deinen Beitrag",
|
||||
"notification.favourite.name_and_others_with_link": "{name} und <a>{count, plural, one {# weitere Person} other {# weitere Personen}}</a> favorisierten deinen Beitrag",
|
||||
"notification.favourite.name_and_others_with_link": "{name} und <a>{count, plural, one {# weiteres Profil} other {# weitere Profile}}</a> favorisierten deinen Beitrag",
|
||||
"notification.follow": "{name} folgt dir",
|
||||
"notification.follow.name_and_others": "{name} und <a>{count, plural, one {# weitere Person} other {# weitere Personen}}</a> folgen dir",
|
||||
"notification.follow.name_and_others": "{name} und <a>{count, plural, one {# weiteres Profil} other {# weitere Profile}}</a> folgen dir",
|
||||
"notification.follow_request": "{name} möchte dir folgen",
|
||||
"notification.follow_request.name_and_others": "{name} und {count, plural, one {# weitere Person} other {# weitere Personen}} möchten dir folgen",
|
||||
"notification.follow_request.name_and_others": "{name} und {count, plural, one {# weiteres Profil} other {# weitere Profile}} möchten dir folgen",
|
||||
"notification.label.mention": "Erwähnung",
|
||||
"notification.label.private_mention": "Private Erwähnung",
|
||||
"notification.label.private_reply": "Private Antwort",
|
||||
|
@ -532,7 +567,7 @@
|
|||
"notification.own_poll": "Deine Umfrage ist beendet",
|
||||
"notification.poll": "Eine Umfrage, an der du teilgenommen hast, ist beendet",
|
||||
"notification.reblog": "{name} teilte deinen Beitrag",
|
||||
"notification.reblog.name_and_others_with_link": "{name} und <a>{count, plural, one {# weitere Person} other {# weitere Personen}}</a> teilten deinen Beitrag",
|
||||
"notification.reblog.name_and_others_with_link": "{name} und <a>{count, plural, one {# weiteres Profil} other {# weitere Profile}}</a> teilten deinen Beitrag",
|
||||
"notification.relationships_severance_event": "Verbindungen mit {name} verloren",
|
||||
"notification.relationships_severance_event.account_suspension": "Ein Admin von {from} hat {target} gesperrt. Du wirst von diesem Profil keine Updates mehr erhalten und auch nicht mit ihm interagieren können.",
|
||||
"notification.relationships_severance_event.domain_block": "Ein Admin von {from} hat {target} blockiert – darunter {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst.",
|
||||
|
@ -667,7 +702,7 @@
|
|||
"poll_button.remove_poll": "Umfrage entfernen",
|
||||
"privacy.change": "Sichtbarkeit anpassen",
|
||||
"privacy.direct.long": "Alle in diesem Beitrag erwähnten Profile",
|
||||
"privacy.direct.short": "Bestimmte Profile",
|
||||
"privacy.direct.short": "Ausgewählte Profile",
|
||||
"privacy.private.long": "Nur deine Follower",
|
||||
"privacy.private.short": "Follower",
|
||||
"privacy.public.long": "Alle in und außerhalb von Mastodon",
|
||||
|
|
|
@ -87,6 +87,24 @@
|
|||
"alert.unexpected.title": "Ουπς!",
|
||||
"alt_text_badge.title": "Εναλλακτικό κείμενο",
|
||||
"announcement.announcement": "Ανακοίνωση",
|
||||
"annual_report.summary.archetype.booster": "Ο κυνηγός των φοβερών",
|
||||
"annual_report.summary.archetype.lurker": "Ο διακριτικός",
|
||||
"annual_report.summary.archetype.oracle": "Η Πυθία",
|
||||
"annual_report.summary.archetype.pollster": "Ο δημοσκόπος",
|
||||
"annual_report.summary.archetype.replier": "Η κοινωνική πεταλούδα",
|
||||
"annual_report.summary.followers.followers": "ακόλουθοι",
|
||||
"annual_report.summary.followers.total": "{count} συνολικά",
|
||||
"annual_report.summary.here_it_is": "Εδώ είναι το {year} σου σε ανασκόπηση:",
|
||||
"annual_report.summary.highlighted_post.by_favourites": "πιο αγαπημένη ανάρτηση",
|
||||
"annual_report.summary.highlighted_post.by_reblogs": "πιο ενισχυμένη ανάρτηση",
|
||||
"annual_report.summary.highlighted_post.by_replies": "ανάρτηση με τις περισσότερες απαντήσεις",
|
||||
"annual_report.summary.highlighted_post.possessive": "του χρήστη {name}",
|
||||
"annual_report.summary.most_used_app.most_used_app": "πιο χρησιμοποιημένη εφαρμογή",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "πιο χρησιμοποιημένη ετικέτα",
|
||||
"annual_report.summary.new_posts.new_posts": "νέες αναρτήσεις",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Αυτό σε βάζει στην κορυφή του </topLabel><percentage></percentage><bottomLabel>των χρηστών του Mastodon.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "Δεν θα το πούμε στον Bernie.",
|
||||
"annual_report.summary.thanks": "Ευχαριστούμε που συμμετέχεις στο Mastodon!",
|
||||
"attachments_list.unprocessed": "(μη επεξεργασμένο)",
|
||||
"audio.hide": "Απόκρυψη αρχείου ήχου",
|
||||
"block_modal.remote_users_caveat": "Θα ζητήσουμε από τον διακομιστή {domain} να σεβαστεί την απόφασή σου. Ωστόσο, η συμμόρφωση δεν είναι εγγυημένη δεδομένου ότι ορισμένοι διακομιστές ενδέχεται να χειρίζονται τους αποκλεισμούς διαφορετικά. Οι δημόσιες αναρτήσεις ενδέχεται να είναι ορατές σε μη συνδεδεμένους χρήστες.",
|
||||
|
@ -158,6 +176,7 @@
|
|||
"compose_form.poll.duration": "Διάρκεια δημοσκόπησης",
|
||||
"compose_form.poll.multiple": "Πολλαπλή επιλογή",
|
||||
"compose_form.poll.option_placeholder": "Επιλογή {number}",
|
||||
"compose_form.poll.single": "Μονή επιλογή",
|
||||
"compose_form.poll.switch_to_multiple": "Ενημέρωση δημοσκόπησης με πολλαπλές επιλογές",
|
||||
"compose_form.poll.switch_to_single": "Ενημέρωση δημοσκόπησης με μοναδική επιλογή",
|
||||
"compose_form.poll.type": "Στυλ",
|
||||
|
@ -196,6 +215,7 @@
|
|||
"confirmations.unfollow.title": "Άρση ακολούθησης;",
|
||||
"content_warning.hide": "Απόκρυψη ανάρτησης",
|
||||
"content_warning.show": "Εμφάνιση ούτως ή άλλως",
|
||||
"content_warning.show_more": "Εμφάνιση περισσότερων",
|
||||
"conversation.delete": "Διαγραφή συζήτησης",
|
||||
"conversation.mark_as_read": "Σήμανση ως αναγνωσμένο",
|
||||
"conversation.open": "Προβολή συνομιλίας",
|
||||
|
@ -271,7 +291,6 @@
|
|||
"empty_column.hashtag": "Δεν υπάρχει ακόμα κάτι για αυτή την ετικέτα.",
|
||||
"empty_column.home": "Η τοπική σου ροή είναι κενή! Πήγαινε στο {public} ή κάνε αναζήτηση για να ξεκινήσεις και να γνωρίσεις άλλους χρήστες.",
|
||||
"empty_column.list": "Δεν υπάρχει τίποτα σε αυτή τη λίστα ακόμα. Όταν τα μέλη της δημοσιεύσουν νέες καταστάσεις, θα εμφανιστούν εδώ.",
|
||||
"empty_column.lists": "Δεν έχεις καμία λίστα ακόμα. Μόλις φτιάξεις μια, θα εμφανιστεί εδώ.",
|
||||
"empty_column.mutes": "Δεν έχεις κανένα χρήστη σε σίγαση ακόμα.",
|
||||
"empty_column.notification_requests": "Όλα καθαρά! Δεν υπάρχει τίποτα εδώ. Όταν λαμβάνεις νέες ειδοποιήσεις, αυτές θα εμφανίζονται εδώ σύμφωνα με τις ρυθμίσεις σου.",
|
||||
"empty_column.notifications": "Δεν έχεις ειδοποιήσεις ακόμα. Όταν άλλα άτομα αλληλεπιδράσουν μαζί σου, θα το δεις εδώ.",
|
||||
|
@ -304,6 +323,7 @@
|
|||
"filter_modal.select_filter.subtitle": "Χρησιμοποιήστε μια υπάρχουσα κατηγορία ή δημιουργήστε μια νέα",
|
||||
"filter_modal.select_filter.title": "Φιλτράρισμα αυτής της ανάρτησης",
|
||||
"filter_modal.title.status": "Φιλτράρισμα μιας ανάρτησης",
|
||||
"filter_warning.matches_filter": "Ταιριάζει με το φίλτρο “<span>{title}</span>”",
|
||||
"filtered_notifications_banner.pending_requests": "Από {count, plural, =0 {κανένα} one {ένα άτομο} other {# άτομα}} που μπορεί να ξέρεις",
|
||||
"filtered_notifications_banner.title": "Φιλτραρισμένες ειδοποιήσεις",
|
||||
"firehose.all": "Όλα",
|
||||
|
@ -383,9 +403,10 @@
|
|||
"interaction_modal.description.follow": "Με έναν λογαριασμό Mastodon, μπορείς να ακολουθήσεις τον/την {name} ώστε να λαμβάνεις τις αναρτήσεις του/της στη δική σου ροή.",
|
||||
"interaction_modal.description.reblog": "Με ένα λογαριασμό Mastodon, μπορείς να ενισχύσεις αυτή την ανάρτηση για να τη μοιραστείς με τους δικούς σου ακολούθους.",
|
||||
"interaction_modal.description.reply": "Με ένα λογαριασμό Mastodon, μπορείς να απαντήσεις σε αυτή την ανάρτηση.",
|
||||
"interaction_modal.login.action": "Take me home\nΠήγαινέ με στην αρχική σελίδα",
|
||||
"interaction_modal.description.vote": "Με ένα λογαριασμό Mastodon, μπορείς να απαντήσεις σ' αυτή την ανάρτηση.",
|
||||
"interaction_modal.login.action": "Πήγαινέ με στην αρχική σελίδα",
|
||||
"interaction_modal.login.prompt": "Τομέας του οικιακού σου διακομιστή, πχ. mastodon.social",
|
||||
"interaction_modal.no_account_yet": "Not on Mastodon?\nΔεν είστε στο Mastodon;",
|
||||
"interaction_modal.no_account_yet": "Δεν είστε στο Mastodon;",
|
||||
"interaction_modal.on_another_server": "Σε διαφορετικό διακομιστή",
|
||||
"interaction_modal.on_this_server": "Σε αυτόν τον διακομιστή",
|
||||
"interaction_modal.sign_in": "Δεν είσαι συνδεδεμένος σε αυτόν το διακομιστή. Πού φιλοξενείται ο λογαριασμός σου;",
|
||||
|
@ -394,6 +415,7 @@
|
|||
"interaction_modal.title.follow": "Ακολούθησε {name}",
|
||||
"interaction_modal.title.reblog": "Ενίσχυσε την ανάρτηση του {name}",
|
||||
"interaction_modal.title.reply": "Απάντηση στην ανάρτηση του {name}",
|
||||
"interaction_modal.title.vote": "Ψήφισε στη δημοσκόπηση του χρήστη {name}",
|
||||
"intervals.full.days": "{number, plural, one {# μέρα} other {# μέρες}}",
|
||||
"intervals.full.hours": "{number, plural, one {# ώρα} other {# ώρες}}",
|
||||
"intervals.full.minutes": "{number, plural, one {# λεπτό} other {# λεπτά}}",
|
||||
|
@ -441,20 +463,11 @@
|
|||
"link_preview.author": "Από {name}",
|
||||
"link_preview.more_from_author": "Περισσότερα από {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
|
||||
"lists.account.add": "Πρόσθεσε στη λίστα",
|
||||
"lists.account.remove": "Βγάλε από τη λίστα",
|
||||
"lists.delete": "Διαγραφή λίστας",
|
||||
"lists.edit": "Επεξεργασία λίστας",
|
||||
"lists.edit.submit": "Αλλαγή τίτλου",
|
||||
"lists.exclusive": "Απόκρυψη αυτών των αναρτήσεων από την αρχική",
|
||||
"lists.new.create": "Προσθήκη λίστας",
|
||||
"lists.new.title_placeholder": "Τίτλος νέας λίστα",
|
||||
"lists.replies_policy.followed": "Οποιοσδήποτε χρήστης που ακολουθείς",
|
||||
"lists.replies_policy.list": "Μέλη της λίστας",
|
||||
"lists.replies_policy.none": "Κανένας",
|
||||
"lists.replies_policy.title": "Εμφάνιση απαντήσεων σε:",
|
||||
"lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς",
|
||||
"lists.subheading": "Οι λίστες σου",
|
||||
"load_pending": "{count, plural, one {# νέο στοιχείο} other {# νέα στοιχεία}}",
|
||||
"loading_indicator.label": "Φόρτωση…",
|
||||
"media_gallery.hide": "Απόκρυψη",
|
||||
|
@ -503,9 +516,12 @@
|
|||
"notification.admin.report_statuses_other": "Ο χρήστης {name} ανέφερε τον χρήστη {target}",
|
||||
"notification.admin.sign_up": "{name} έχει εγγραφεί",
|
||||
"notification.admin.sign_up.name_and_others": "{name} και {count, plural, one {# ακόμη} other {# ακόμη}} έχουν εγγραφεί",
|
||||
"notification.annual_report.message": "Το #Wrapstodon {year} σε περιμένει! Αποκάλυψε τα στιγμιότυπα της χρονιάς και αξέχαστες στιγμές σου στο Mastodon!",
|
||||
"notification.annual_report.view": "Προβολή #Wrapstodon",
|
||||
"notification.favourite": "{name} favorited your post\n{name} προτίμησε την ανάρτηση σου",
|
||||
"notification.favourite.name_and_others_with_link": "{name} και <a>{count, plural, one {# ακόμη} other {# ακόμη}}</a> αγάπησαν την ανάρτησή σου",
|
||||
"notification.follow": "Ο/Η {name} σε ακολούθησε",
|
||||
"notification.follow.name_and_others": "Ο χρήστης {name} και <a>{count, plural, one {# ακόμη} other {# ακόμη}}</a> σε ακολούθησαν",
|
||||
"notification.follow_request": "Ο/H {name} ζήτησε να σε ακολουθήσει",
|
||||
"notification.follow_request.name_and_others": "{name} και {count, plural, one {# άλλος} other {# άλλοι}} ζήτησαν να σε ακολουθήσουν",
|
||||
"notification.label.mention": "Επισήμανση",
|
||||
|
@ -513,6 +529,7 @@
|
|||
"notification.label.private_reply": "Ιδιωτική απάντηση",
|
||||
"notification.label.reply": "Απάντηση",
|
||||
"notification.mention": "Επισήμανση",
|
||||
"notification.mentioned_you": "Ο χρήστης {name} σε επισήμανε",
|
||||
"notification.moderation-warning.learn_more": "Μάθε περισσότερα",
|
||||
"notification.moderation_warning": "Έχετε λάβει μία προειδοποίηση συντονισμού",
|
||||
"notification.moderation_warning.action_delete_statuses": "Ορισμένες από τις αναρτήσεις σου έχουν αφαιρεθεί.",
|
||||
|
@ -563,6 +580,7 @@
|
|||
"notifications.column_settings.filter_bar.category": "Μπάρα γρήγορου φίλτρου",
|
||||
"notifications.column_settings.follow": "Νέοι ακόλουθοι:",
|
||||
"notifications.column_settings.follow_request": "Νέο αίτημα ακολούθησης:",
|
||||
"notifications.column_settings.group": "Ομάδα",
|
||||
"notifications.column_settings.mention": "Επισημάνσεις:",
|
||||
"notifications.column_settings.poll": "Αποτελέσματα δημοσκόπησης:",
|
||||
"notifications.column_settings.push": "Ειδοποιήσεις Push",
|
||||
|
|
|
@ -87,6 +87,25 @@
|
|||
"alert.unexpected.title": "Oops!",
|
||||
"alt_text_badge.title": "Alt text",
|
||||
"announcement.announcement": "Announcement",
|
||||
"annual_report.summary.archetype.booster": "The cool-hunter",
|
||||
"annual_report.summary.archetype.lurker": "The lurker",
|
||||
"annual_report.summary.archetype.oracle": "The oracle",
|
||||
"annual_report.summary.archetype.pollster": "The pollster",
|
||||
"annual_report.summary.archetype.replier": "The social butterfly",
|
||||
"annual_report.summary.followers.followers": "followers",
|
||||
"annual_report.summary.followers.total": "{count} total",
|
||||
"annual_report.summary.here_it_is": "Here is your {year} in review:",
|
||||
"annual_report.summary.highlighted_post.by_favourites": "most favourited post",
|
||||
"annual_report.summary.highlighted_post.by_reblogs": "most boosted post",
|
||||
"annual_report.summary.highlighted_post.by_replies": "post with the most replies",
|
||||
"annual_report.summary.highlighted_post.possessive": "{name}'s",
|
||||
"annual_report.summary.most_used_app.most_used_app": "most used app",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "most used hashtag",
|
||||
"annual_report.summary.most_used_hashtag.none": "None",
|
||||
"annual_report.summary.new_posts.new_posts": "new posts",
|
||||
"annual_report.summary.percentile.text": "<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of Mastodon users.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.",
|
||||
"annual_report.summary.thanks": "Thanks for being part of Mastodon!",
|
||||
"attachments_list.unprocessed": "(unprocessed)",
|
||||
"audio.hide": "Hide audio",
|
||||
"block_modal.remote_users_caveat": "We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.",
|
||||
|
@ -158,6 +177,7 @@
|
|||
"compose_form.poll.duration": "Poll duration",
|
||||
"compose_form.poll.multiple": "Multiple choice",
|
||||
"compose_form.poll.option_placeholder": "Option {number}",
|
||||
"compose_form.poll.single": "Single choice",
|
||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
||||
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
||||
"compose_form.poll.type": "Style",
|
||||
|
@ -196,6 +216,7 @@
|
|||
"confirmations.unfollow.title": "Unfollow user?",
|
||||
"content_warning.hide": "Hide post",
|
||||
"content_warning.show": "Show anyway",
|
||||
"content_warning.show_more": "Show more",
|
||||
"conversation.delete": "Delete conversation",
|
||||
"conversation.mark_as_read": "Mark as read",
|
||||
"conversation.open": "View conversation",
|
||||
|
@ -271,7 +292,6 @@
|
|||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
||||
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up.",
|
||||
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
|
||||
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
||||
"empty_column.mutes": "You haven't muted any users yet.",
|
||||
"empty_column.notification_requests": "All clear! There is nothing here. When you receive new notifications, they will appear here according to your settings.",
|
||||
"empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.",
|
||||
|
@ -304,6 +324,7 @@
|
|||
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
|
||||
"filter_modal.select_filter.title": "Filter this post",
|
||||
"filter_modal.title.status": "Filter a post",
|
||||
"filter_warning.matches_filter": "Matches filter \"<span>{title}</span>\"",
|
||||
"filtered_notifications_banner.pending_requests": "From {count, plural, =0 {no one} one {one person} other {# people}} you may know",
|
||||
"filtered_notifications_banner.title": "Filtered notifications",
|
||||
"firehose.all": "All",
|
||||
|
@ -383,6 +404,7 @@
|
|||
"interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
|
||||
"interaction_modal.description.reblog": "With an account on Mastodon, you can boost this post to share it with your own followers.",
|
||||
"interaction_modal.description.reply": "With an account on Mastodon, you can respond to this post.",
|
||||
"interaction_modal.description.vote": "With an account on Mastodon, you can vote in this poll.",
|
||||
"interaction_modal.login.action": "Take me home",
|
||||
"interaction_modal.login.prompt": "Domain of your home server, e.g. mastodon.social",
|
||||
"interaction_modal.no_account_yet": "Not on Mastodon?",
|
||||
|
@ -394,6 +416,7 @@
|
|||
"interaction_modal.title.follow": "Follow {name}",
|
||||
"interaction_modal.title.reblog": "Boost {name}'s post",
|
||||
"interaction_modal.title.reply": "Reply to {name}'s post",
|
||||
"interaction_modal.title.vote": "Vote in {name}'s poll",
|
||||
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
|
||||
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
|
||||
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
|
||||
|
@ -441,20 +464,11 @@
|
|||
"link_preview.author": "By {name}",
|
||||
"link_preview.more_from_author": "More from {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} post} other {{counter} posts}}",
|
||||
"lists.account.add": "Add to list",
|
||||
"lists.account.remove": "Remove from list",
|
||||
"lists.delete": "Delete list",
|
||||
"lists.edit": "Edit list",
|
||||
"lists.edit.submit": "Change title",
|
||||
"lists.exclusive": "Hide these posts from home",
|
||||
"lists.new.create": "Add list",
|
||||
"lists.new.title_placeholder": "New list title",
|
||||
"lists.replies_policy.followed": "Any followed user",
|
||||
"lists.replies_policy.list": "Members of the list",
|
||||
"lists.replies_policy.none": "No one",
|
||||
"lists.replies_policy.title": "Show replies to:",
|
||||
"lists.search": "Search among people you follow",
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading…",
|
||||
"media_gallery.hide": "Hide",
|
||||
|
@ -503,9 +517,11 @@
|
|||
"notification.admin.report_statuses_other": "{name} reported {target}",
|
||||
"notification.admin.sign_up": "{name} signed up",
|
||||
"notification.admin.sign_up.name_and_others": "{name} and {count, plural, one {# other} other {# others}} signed up",
|
||||
"notification.annual_report.message": "Your {year} #Wrapstodon awaits! Unveil your year's highlights and memorable moments on Mastodon!",
|
||||
"notification.favourite": "{name} favourited your post",
|
||||
"notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favourited your post",
|
||||
"notification.follow": "{name} followed you",
|
||||
"notification.follow.name_and_others": "{name} and <a>{count, plural, one {# other} other {# others}}</a> followed you",
|
||||
"notification.follow_request": "{name} has requested to follow you",
|
||||
"notification.follow_request.name_and_others": "{name} and {count, plural, one {# other} other {# others}} has requested to follow you",
|
||||
"notification.label.mention": "Mention",
|
||||
|
@ -513,6 +529,7 @@
|
|||
"notification.label.private_reply": "Private reply",
|
||||
"notification.label.reply": "Reply",
|
||||
"notification.mention": "Mention",
|
||||
"notification.mentioned_you": "{name} mentioned you",
|
||||
"notification.moderation-warning.learn_more": "Learn more",
|
||||
"notification.moderation_warning": "You have received a moderation warning",
|
||||
"notification.moderation_warning.action_delete_statuses": "Some of your posts have been removed.",
|
||||
|
@ -563,6 +580,7 @@
|
|||
"notifications.column_settings.filter_bar.category": "Quick filter bar",
|
||||
"notifications.column_settings.follow": "New followers:",
|
||||
"notifications.column_settings.follow_request": "New follow requests:",
|
||||
"notifications.column_settings.group": "Group",
|
||||
"notifications.column_settings.mention": "Mentions:",
|
||||
"notifications.column_settings.poll": "Poll results:",
|
||||
"notifications.column_settings.push": "Push notifications",
|
||||
|
|
|
@ -87,6 +87,25 @@
|
|||
"alert.unexpected.title": "Oops!",
|
||||
"alt_text_badge.title": "Alt text",
|
||||
"announcement.announcement": "Announcement",
|
||||
"annual_report.summary.archetype.booster": "The cool-hunter",
|
||||
"annual_report.summary.archetype.lurker": "The lurker",
|
||||
"annual_report.summary.archetype.oracle": "The oracle",
|
||||
"annual_report.summary.archetype.pollster": "The pollster",
|
||||
"annual_report.summary.archetype.replier": "The social butterfly",
|
||||
"annual_report.summary.followers.followers": "followers",
|
||||
"annual_report.summary.followers.total": "{count} total",
|
||||
"annual_report.summary.here_it_is": "Here is your {year} in review:",
|
||||
"annual_report.summary.highlighted_post.by_favourites": "most favourited post",
|
||||
"annual_report.summary.highlighted_post.by_reblogs": "most boosted post",
|
||||
"annual_report.summary.highlighted_post.by_replies": "post with the most replies",
|
||||
"annual_report.summary.highlighted_post.possessive": "{name}'s",
|
||||
"annual_report.summary.most_used_app.most_used_app": "most used app",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "most used hashtag",
|
||||
"annual_report.summary.most_used_hashtag.none": "None",
|
||||
"annual_report.summary.new_posts.new_posts": "new posts",
|
||||
"annual_report.summary.percentile.text": "<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of Mastodon users.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.",
|
||||
"annual_report.summary.thanks": "Thanks for being part of Mastodon!",
|
||||
"attachments_list.unprocessed": "(unprocessed)",
|
||||
"audio.hide": "Hide audio",
|
||||
"block_modal.remote_users_caveat": "We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.",
|
||||
|
@ -121,13 +140,16 @@
|
|||
"column.blocks": "Blocked users",
|
||||
"column.bookmarks": "Bookmarks",
|
||||
"column.community": "Local timeline",
|
||||
"column.create_list": "Create list",
|
||||
"column.direct": "Private mentions",
|
||||
"column.directory": "Browse profiles",
|
||||
"column.domain_blocks": "Blocked domains",
|
||||
"column.edit_list": "Edit list",
|
||||
"column.favourites": "Favorites",
|
||||
"column.firehose": "Live feeds",
|
||||
"column.follow_requests": "Follow requests",
|
||||
"column.home": "Home",
|
||||
"column.list_members": "Manage list members",
|
||||
"column.lists": "Lists",
|
||||
"column.mutes": "Muted users",
|
||||
"column.notifications": "Notifications",
|
||||
|
@ -273,7 +295,6 @@
|
|||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
||||
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up.",
|
||||
"empty_column.list": "There is nothing in this list yet. When members of this list publish new posts, they will appear here.",
|
||||
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
||||
"empty_column.mutes": "You haven't muted any users yet.",
|
||||
"empty_column.notification_requests": "All clear! There is nothing here. When you receive new notifications, they will appear here according to your settings.",
|
||||
"empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.",
|
||||
|
@ -446,20 +467,32 @@
|
|||
"link_preview.author": "By {name}",
|
||||
"link_preview.more_from_author": "More from {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} post} other {{counter} posts}}",
|
||||
"lists.account.add": "Add to list",
|
||||
"lists.account.remove": "Remove from list",
|
||||
"lists.add_member": "Add",
|
||||
"lists.add_to_list": "Add to list",
|
||||
"lists.add_to_lists": "Add {name} to lists",
|
||||
"lists.create": "Create",
|
||||
"lists.create_a_list_to_organize": "Create a new list to organize your Home feed",
|
||||
"lists.create_list": "Create list",
|
||||
"lists.delete": "Delete list",
|
||||
"lists.done": "Done",
|
||||
"lists.edit": "Edit list",
|
||||
"lists.edit.submit": "Change title",
|
||||
"lists.exclusive": "Hide these posts from home",
|
||||
"lists.new.create": "Add list",
|
||||
"lists.new.title_placeholder": "New list title",
|
||||
"lists.exclusive": "Hide members in Home",
|
||||
"lists.exclusive_hint": "If someone is on this list, hide them in your Home feed to avoid seeing their posts twice.",
|
||||
"lists.find_users_to_add": "Find users to add",
|
||||
"lists.list_members": "List members",
|
||||
"lists.list_members_count": "{count, plural, one {# member} other {# members}}",
|
||||
"lists.list_name": "List name",
|
||||
"lists.new_list_name": "New list name",
|
||||
"lists.no_lists_yet": "No lists yet.",
|
||||
"lists.no_members_yet": "No members yet.",
|
||||
"lists.no_results_found": "No results found.",
|
||||
"lists.remove_member": "Remove",
|
||||
"lists.replies_policy.followed": "Any followed user",
|
||||
"lists.replies_policy.list": "Members of the list",
|
||||
"lists.replies_policy.none": "No one",
|
||||
"lists.replies_policy.title": "Show replies to:",
|
||||
"lists.search": "Search among people you follow",
|
||||
"lists.subheading": "Your lists",
|
||||
"lists.save": "Save",
|
||||
"lists.search_placeholder": "Search people you follow",
|
||||
"lists.show_replies_to": "Include replies from list members to",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading…",
|
||||
"media_gallery.hide": "Hide",
|
||||
|
@ -508,6 +541,8 @@
|
|||
"notification.admin.report_statuses_other": "{name} reported {target}",
|
||||
"notification.admin.sign_up": "{name} signed up",
|
||||
"notification.admin.sign_up.name_and_others": "{name} and {count, plural, one {# other} other {# others}} signed up",
|
||||
"notification.annual_report.message": "Your {year} #Wrapstodon awaits! Unveil your year's highlights and memorable moments on Mastodon!",
|
||||
"notification.annual_report.view": "View #Wrapstodon",
|
||||
"notification.favourite": "{name} favorited your post",
|
||||
"notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your post",
|
||||
"notification.follow": "{name} followed you",
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
"account.languages": "Ŝanĝi la abonitajn lingvojn",
|
||||
"account.link_verified_on": "Propreco de tiu ligilo estis konfirmita je {date}",
|
||||
"account.locked_info": "Tiu konto estas privatigita. La posedanto mane akceptas tiun, kiu povas sekvi rin.",
|
||||
"account.media": "Plurmedioj",
|
||||
"account.media": "Plurmedio",
|
||||
"account.mention": "Mencii @{name}",
|
||||
"account.moved_to": "{name} indikis, ke ria nova konto estas nun:",
|
||||
"account.mute": "Silentigi @{name}",
|
||||
|
@ -87,6 +87,13 @@
|
|||
"alert.unexpected.title": "Aj!",
|
||||
"alt_text_badge.title": "Alt-teksto",
|
||||
"announcement.announcement": "Anonco",
|
||||
"annual_report.summary.archetype.replier": "La plej societema",
|
||||
"annual_report.summary.followers.followers": "sekvantoj",
|
||||
"annual_report.summary.highlighted_post.by_replies": "afiŝo kun la plej multaj respondoj",
|
||||
"annual_report.summary.most_used_app.most_used_app": "plej uzita apo",
|
||||
"annual_report.summary.most_used_hashtag.none": "Nenio",
|
||||
"annual_report.summary.new_posts.new_posts": "novaj afiŝoj",
|
||||
"annual_report.summary.thanks": "Dankon pro esti parto de Mastodon!",
|
||||
"attachments_list.unprocessed": "(neprilaborita)",
|
||||
"audio.hide": "Kaŝi aŭdion",
|
||||
"block_modal.remote_users_caveat": "Ni petos al la servilo {domain} respekti vian elekton. Tamen, plenumo ne estas garantiita ĉar iuj serviloj eble manipulas blokojn malsame. Publikaj afiŝoj eble ankoraŭ estas videbla por ne-ensalutintaj uzantoj.",
|
||||
|
@ -142,7 +149,7 @@
|
|||
"column_header.unpin": "Malfiksi",
|
||||
"column_subheading.settings": "Agordoj",
|
||||
"community.column_settings.local_only": "Nur loka",
|
||||
"community.column_settings.media_only": "Nur plurmedioj",
|
||||
"community.column_settings.media_only": "Nur plurmedio",
|
||||
"community.column_settings.remote_only": "Nur fora",
|
||||
"compose.language.change": "Ŝanĝi lingvon",
|
||||
"compose.language.search": "Serĉi lingvojn...",
|
||||
|
@ -214,7 +221,7 @@
|
|||
"dismissable_banner.community_timeline": "Jen la plej novaj publikaj afiŝoj de uzantoj, kies kontojn gastigas {domain}.",
|
||||
"dismissable_banner.dismiss": "Eksigi",
|
||||
"dismissable_banner.explore_links": "Tiuj novaĵoj estas aktuale priparolataj de uzantoj en tiu ĉi kaj aliaj serviloj, sur la malcentrigita reto.",
|
||||
"dismissable_banner.explore_statuses": "Ĉi tiuj estas afiŝoj de la tuta socia reto, kiuj populariĝas hodiaŭ. Pli novaj afiŝoj kun pli da diskonigoj kaj plej ŝatataj estas rangigitaj pli alte.",
|
||||
"dismissable_banner.explore_statuses": "Jen afiŝoj en la socia reto kiuj populariĝis hodiaŭ. Novaj afiŝoj kun pli da diskonigoj kaj stelumoj aperas pli alte.",
|
||||
"dismissable_banner.explore_tags": "Ĉi tiuj kradvostoj populariĝas en ĉi tiu kaj aliaj serviloj en la malcentraliza reto nun.",
|
||||
"dismissable_banner.public_timeline": "Ĉi tiuj estas la plej lastatempaj publikaj afiŝoj de homoj en la socia reto, kiujn homoj sur {domain} sekvas.",
|
||||
"domain_block_modal.block": "Bloki servilon",
|
||||
|
@ -273,7 +280,6 @@
|
|||
"empty_column.hashtag": "Ankoraŭ estas nenio per ĉi tiu kradvorto.",
|
||||
"empty_column.home": "Via hejma tempolinio estas malplena! Vizitu {public} aŭ uzu la serĉilon por renkonti aliajn uzantojn.",
|
||||
"empty_column.list": "Ankoraŭ estas nenio en ĉi tiu listo. Kiam membroj de ĉi tiu listo afiŝos novajn afiŝojn, ili aperos ĉi tie.",
|
||||
"empty_column.lists": "Vi ankoraŭ ne havas liston. Kiam vi kreos iun, ĝi aperos ĉi tie.",
|
||||
"empty_column.mutes": "Vi ne ankoraŭ silentigis iun uzanton.",
|
||||
"empty_column.notification_requests": "Ĉio klara! Estas nenio tie ĉi. Kiam vi ricevas novajn sciigojn, ili aperos ĉi tie laŭ viaj agordoj.",
|
||||
"empty_column.notifications": "Vi ankoraŭ ne havas sciigojn. Interagu kun aliaj por komenci konversacion.",
|
||||
|
@ -333,7 +339,7 @@
|
|||
"followed_tags": "Sekvataj kradvortoj",
|
||||
"footer.about": "Pri",
|
||||
"footer.directory": "Profilujo",
|
||||
"footer.get_app": "Akiru la Programon",
|
||||
"footer.get_app": "Akiri la apon",
|
||||
"footer.invite": "Inviti homojn",
|
||||
"footer.keyboard_shortcuts": "Fulmoklavoj",
|
||||
"footer.privacy_policy": "Politiko de privateco",
|
||||
|
@ -382,7 +388,7 @@
|
|||
"ignore_notifications_modal.not_followers_title": "Ĉu ignori sciigojn de homoj, kiuj ne sekvas vin?",
|
||||
"ignore_notifications_modal.not_following_title": "Ĉu ignori sciigojn de homoj, kiujn vi ne sekvas?",
|
||||
"ignore_notifications_modal.private_mentions_title": "Ĉu ignori sciigojn de nepetitaj privataj mencioj?",
|
||||
"interaction_modal.description.favourite": "Per konto ĉe Mastodon, vi povas stelumiti ĉi tiun afiŝon por sciigi la afiŝanton ke vi aprezigas ŝin kaj konservas por la estonteco.",
|
||||
"interaction_modal.description.favourite": "Per konto ĉe Mastodon, vi povas stelumi ĉi tiun afiŝon por sciigi la afiŝanton ke vi sâtas kaj konservas ĝin por poste.",
|
||||
"interaction_modal.description.follow": "Kun konto ĉe Mastodon, vi povas sekvi {name} por ricevi iliajn afiŝojn en via hejma fluo.",
|
||||
"interaction_modal.description.reblog": "Kun konto ĉe Mastodon, vi povas diskonigi ĉi tiun afiŝon, por ke viaj propraj sekvantoj vidu ĝin.",
|
||||
"interaction_modal.description.reply": "Kun konto ĉe Mastodon, vi povos respondi al ĉi tiu afiŝo.",
|
||||
|
@ -446,20 +452,11 @@
|
|||
"link_preview.author": "De {name}",
|
||||
"link_preview.more_from_author": "Pli de {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} afiŝo} other {{counter} afiŝoj}}",
|
||||
"lists.account.add": "Aldoni al la listo",
|
||||
"lists.account.remove": "Forigi de la listo",
|
||||
"lists.delete": "Forigi la liston",
|
||||
"lists.edit": "Redakti la liston",
|
||||
"lists.edit.submit": "Ŝanĝi titolon",
|
||||
"lists.exclusive": "Kaŝi ĉi tiujn afiŝojn de hejmo",
|
||||
"lists.new.create": "Aldoni liston",
|
||||
"lists.new.title_placeholder": "Titolo de la nova listo",
|
||||
"lists.replies_policy.followed": "Iu sekvanta uzanto",
|
||||
"lists.replies_policy.list": "Membroj de la listo",
|
||||
"lists.replies_policy.none": "Neniu",
|
||||
"lists.replies_policy.title": "Montri respondojn al:",
|
||||
"lists.search": "Serĉi inter la homoj, kiujn vi sekvas",
|
||||
"lists.subheading": "Viaj listoj",
|
||||
"load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
|
||||
"loading_indicator.label": "Ŝargado…",
|
||||
"media_gallery.hide": "Kaŝi",
|
||||
|
@ -508,6 +505,7 @@
|
|||
"notification.admin.report_statuses_other": "{name} raportis {target}",
|
||||
"notification.admin.sign_up": "{name} kreis konton",
|
||||
"notification.admin.sign_up.name_and_others": "{name} kaj {count, plural, one {# alia} other {# aliaj}} kreis konton",
|
||||
"notification.annual_report.view": "Vidu #Wrapstodon",
|
||||
"notification.favourite": "{name} stelumis vian afiŝon",
|
||||
"notification.favourite.name_and_others_with_link": "{name} kaj <a>{count, plural, one {# alia} other {# aliaj}}</a> ŝatis vian afiŝon",
|
||||
"notification.follow": "{name} eksekvis vin",
|
||||
|
@ -640,7 +638,7 @@
|
|||
"onboarding.start.lead": "Vi nun estas parto de Mastodon, unika, malcentralizita socia amaskomunikilara platformo, kie vi—ne algoritmo—zorgas vian propran sperton. Ni komencu vin sur ĉi tiu nova socia limo:",
|
||||
"onboarding.start.skip": "Ĉu vi ne bezonas helpon por komenci?",
|
||||
"onboarding.start.title": "Vi atingas ĝin!",
|
||||
"onboarding.steps.follow_people.body": "Sekvi interesajn homojn estas pri kio Mastodonto temas.",
|
||||
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
|
||||
"onboarding.steps.follow_people.title": "Agordu vian hejman fluon",
|
||||
"onboarding.steps.publish_status.body": "Salutu la mondon per teksto, fotoj, filmetoj aŭ balotenketoj {emoji}",
|
||||
"onboarding.steps.publish_status.title": "Fari vian unuan afiŝon",
|
||||
|
@ -774,9 +772,9 @@
|
|||
"server_banner.is_one_of_many": "{domain} estas unu el la multaj sendependaj Mastodon-serviloj, kiujn vi povas uzi por partopreni en la fediverso.",
|
||||
"server_banner.server_stats": "Statistikoj de la servilo:",
|
||||
"sign_in_banner.create_account": "Krei konton",
|
||||
"sign_in_banner.follow_anyone": "Sekvi iun ajn tra la fediverso kaj vidi ĉion en kronologia ordo. Neniuj algoritmoj, reklamoj aŭ klakbetoj videblas.",
|
||||
"sign_in_banner.mastodon_is": "Mastodonto estas la plej bona maniero por resti flank-al-flanke kun kio okazas.",
|
||||
"sign_in_banner.sign_in": "Saluti",
|
||||
"sign_in_banner.follow_anyone": "Sekvu iun ajn tra la fediverso kaj vidu ĉion laŭ templinio. Nul algoritmo, reklamo aŭ kliklogilo ĉeestas.",
|
||||
"sign_in_banner.mastodon_is": "Mastodon estas la plej bona maniero resti ĝisdata pri aktualaĵoj.",
|
||||
"sign_in_banner.sign_in": "Ensaluti",
|
||||
"sign_in_banner.sso_redirect": "Ensalutu aŭ Registriĝi",
|
||||
"status.admin_account": "Malfermi fasadon de moderigado por @{name}",
|
||||
"status.admin_domain": "Malfermu moderigan interfacon por {domain}",
|
||||
|
|
|
@ -87,6 +87,25 @@
|
|||
"alert.unexpected.title": "¡Epa!",
|
||||
"alt_text_badge.title": "Texto alternativo",
|
||||
"announcement.announcement": "Anuncio",
|
||||
"annual_report.summary.archetype.booster": "Corrió la voz",
|
||||
"annual_report.summary.archetype.lurker": "El acechador",
|
||||
"annual_report.summary.archetype.oracle": "El oráculo",
|
||||
"annual_report.summary.archetype.pollster": "Estuvo consultando",
|
||||
"annual_report.summary.archetype.replier": "Respondió un montón",
|
||||
"annual_report.summary.followers.followers": "seguidores",
|
||||
"annual_report.summary.followers.total": "{count} en total",
|
||||
"annual_report.summary.here_it_is": "Acá está tu resumen de {year}:",
|
||||
"annual_report.summary.highlighted_post.by_favourites": "el mensaje más veces marcado como favorito",
|
||||
"annual_report.summary.highlighted_post.by_reblogs": "el mensaje que más adhesiones recibió",
|
||||
"annual_report.summary.highlighted_post.by_replies": "el mensaje que más respuestas recibió",
|
||||
"annual_report.summary.highlighted_post.possessive": "{name}",
|
||||
"annual_report.summary.most_used_app.most_used_app": "la aplicación más usada",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "la etiqueta más usada",
|
||||
"annual_report.summary.most_used_hashtag.none": "Ninguna",
|
||||
"annual_report.summary.new_posts.new_posts": "nuevos mensajes",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Eso te pone en la cima</topLabel><percentage></percentage><bottomLabel>de los usuarios de Mastodon.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "No se lo diremos a Bernie.",
|
||||
"annual_report.summary.thanks": "¡Gracias por ser parte de Mastodon!",
|
||||
"attachments_list.unprocessed": "[sin procesar]",
|
||||
"audio.hide": "Ocultar audio",
|
||||
"block_modal.remote_users_caveat": "Le pediremos al servidor {domain} que respete tu decisión. Sin embargo, el cumplimiento no está garantizado, ya que algunos servidores pueden manejar los bloqueos de forma diferente. Los mensajes públicos todavía podrían estar visibles para los usuarios no conectados.",
|
||||
|
@ -273,7 +292,6 @@
|
|||
"empty_column.hashtag": "Todavía no hay nada con esta etiqueta.",
|
||||
"empty_column.home": "¡Tu línea temporal principal está vacía! Seguí a más cuentas para llenarla.",
|
||||
"empty_column.list": "Todavía no hay nada en esta lista. Cuando miembros de esta lista envíen nuevos mensaje, se mostrarán acá.",
|
||||
"empty_column.lists": "Todavía no tenés ninguna lista. Cuando creés una, se mostrará acá.",
|
||||
"empty_column.mutes": "Todavía no silenciaste a ningún usuario.",
|
||||
"empty_column.notification_requests": "¡Todo limpio! No hay nada acá. Cuando recibás nuevas notificaciones, aparecerán acá, acorde a tu configuración.",
|
||||
"empty_column.notifications": "Todavía no tenés ninguna notificación. Cuando otras cuentas interactúen con vos, vas a ver la notificación acá.",
|
||||
|
@ -446,20 +464,11 @@
|
|||
"link_preview.author": "Por {name}",
|
||||
"link_preview.more_from_author": "Más de {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}",
|
||||
"lists.account.add": "Agregar a lista",
|
||||
"lists.account.remove": "Quitar de lista",
|
||||
"lists.delete": "Eliminar lista",
|
||||
"lists.edit": "Editar lista",
|
||||
"lists.edit.submit": "Cambiar título",
|
||||
"lists.exclusive": "Ocultar estos mensajes del inicio",
|
||||
"lists.new.create": "Agregar lista",
|
||||
"lists.new.title_placeholder": "Título de nueva lista",
|
||||
"lists.replies_policy.followed": "Cualquier cuenta seguida",
|
||||
"lists.replies_policy.list": "Miembros de la lista",
|
||||
"lists.replies_policy.none": "Nadie",
|
||||
"lists.replies_policy.title": "Mostrar respuestas a:",
|
||||
"lists.search": "Buscar entre la gente que seguís",
|
||||
"lists.subheading": "Tus listas",
|
||||
"load_pending": "{count, plural, one {# elemento nuevo} other {# elementos nuevos}}",
|
||||
"loading_indicator.label": "Cargando…",
|
||||
"media_gallery.hide": "Ocultar",
|
||||
|
@ -508,6 +517,8 @@
|
|||
"notification.admin.report_statuses_other": "{name} denunció a {target}",
|
||||
"notification.admin.sign_up": "Se registró {name}",
|
||||
"notification.admin.sign_up.name_and_others": "Se registraron {name} y {count, plural, one {# cuenta más} other {# cuentas más}}",
|
||||
"notification.annual_report.message": "¡Tu #Wrapstodon {year} te espera! ¡Desvela los momentos más destacados y memorables de tu año en Mastodon!",
|
||||
"notification.annual_report.view": "Ver #Wrapstodon",
|
||||
"notification.favourite": "{name} marcó tu mensaje como favorito",
|
||||
"notification.favourite.name_and_others_with_link": "{name} y <a>{count, plural, one {# cuenta más} other {# cuentas más}}</a> marcaron tu mensaje como favorito",
|
||||
"notification.follow": "{name} te empezó a seguir",
|
||||
|
|
|
@ -87,6 +87,25 @@
|
|||
"alert.unexpected.title": "¡Ups!",
|
||||
"alt_text_badge.title": "Texto alternativo",
|
||||
"announcement.announcement": "Anuncio",
|
||||
"annual_report.summary.archetype.booster": "El cazador de tendencias",
|
||||
"annual_report.summary.archetype.lurker": "El acechador",
|
||||
"annual_report.summary.archetype.oracle": "El oraculo",
|
||||
"annual_report.summary.archetype.pollster": "El encuestador",
|
||||
"annual_report.summary.archetype.replier": "La mariposa sociable",
|
||||
"annual_report.summary.followers.followers": "seguidores",
|
||||
"annual_report.summary.followers.total": "{count} en total",
|
||||
"annual_report.summary.here_it_is": "Aquí está tu resumen de {year}:",
|
||||
"annual_report.summary.highlighted_post.by_favourites": "publicación con más favoritos",
|
||||
"annual_report.summary.highlighted_post.by_reblogs": "publicación más impulsada",
|
||||
"annual_report.summary.highlighted_post.by_replies": "publicación con más respuestas",
|
||||
"annual_report.summary.highlighted_post.possessive": "de {name}",
|
||||
"annual_report.summary.most_used_app.most_used_app": "aplicación más usada",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "etiqueta más usada",
|
||||
"annual_report.summary.most_used_hashtag.none": "Ninguna",
|
||||
"annual_report.summary.new_posts.new_posts": "nuevas publicaciones",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Eso te pone en el top</topLabel><percentage></percentage><bottomLabel>de usuarios de Mastodon.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "No se lo diremos a Bernie.",
|
||||
"annual_report.summary.thanks": "¡Gracias por ser parte de Mastodon!",
|
||||
"attachments_list.unprocessed": "(sin procesar)",
|
||||
"audio.hide": "Ocultar audio",
|
||||
"block_modal.remote_users_caveat": "Le pediremos al servidor {domain} que respete tu decisión. Sin embargo, el cumplimiento no está garantizado ya que algunos servidores pueden manejar bloques de forma diferente. Las publicaciones públicas pueden ser todavía visibles para los usuarios no conectados.",
|
||||
|
@ -273,7 +292,6 @@
|
|||
"empty_column.hashtag": "No hay nada en esta etiqueta aún.",
|
||||
"empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
|
||||
"empty_column.list": "No hay nada en esta lista aún. Cuando miembros de esta lista publiquen nuevos estatus, estos aparecerán qui.",
|
||||
"empty_column.lists": "No tienes ninguna lista. cuando crees una, se mostrará aquí.",
|
||||
"empty_column.mutes": "Aún no has silenciado a ningún usuario.",
|
||||
"empty_column.notification_requests": "¡Todo limpio! No hay nada aquí. Cuando recibas nuevas notificaciones, aparecerán aquí conforme a tu configuración.",
|
||||
"empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
|
||||
|
@ -446,20 +464,11 @@
|
|||
"link_preview.author": "Por {name}",
|
||||
"link_preview.more_from_author": "Más de {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
|
||||
"lists.account.add": "Añadir a lista",
|
||||
"lists.account.remove": "Quitar de lista",
|
||||
"lists.delete": "Borrar lista",
|
||||
"lists.edit": "Editar lista",
|
||||
"lists.edit.submit": "Cambiar título",
|
||||
"lists.exclusive": "Ocultar estas publicaciones en inicio",
|
||||
"lists.new.create": "Añadir lista",
|
||||
"lists.new.title_placeholder": "Título de la nueva lista",
|
||||
"lists.replies_policy.followed": "Cualquier usuario seguido",
|
||||
"lists.replies_policy.list": "Miembros de la lista",
|
||||
"lists.replies_policy.none": "Nadie",
|
||||
"lists.replies_policy.title": "Mostrar respuestas a:",
|
||||
"lists.search": "Buscar entre la gente a la que sigues",
|
||||
"lists.subheading": "Tus listas",
|
||||
"load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
|
||||
"loading_indicator.label": "Cargando…",
|
||||
"media_gallery.hide": "Ocultar",
|
||||
|
@ -508,6 +517,8 @@
|
|||
"notification.admin.report_statuses_other": "{name} reportó {target}",
|
||||
"notification.admin.sign_up": "{name} se unio",
|
||||
"notification.admin.sign_up.name_and_others": "{name} y {count, plural, one {# otro} other {# otros}} se registraron",
|
||||
"notification.annual_report.message": "¡Tu #Wrapstodon {year} te espera! ¡Desvela los momentos más destacados y memorables de tu año en Mastodon!",
|
||||
"notification.annual_report.view": "Ver #Wrapstodon",
|
||||
"notification.favourite": "{name} marcó como favorita tu publicación",
|
||||
"notification.favourite.name_and_others_with_link": "{name} y <a>{count, plural, one {# otro} other {# otros}}</a> marcaron tu publicación como favorita",
|
||||
"notification.follow": "{name} te empezó a seguir",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue