Merge tag 'v2.9.2' into instance_only_statuses

This commit is contained in:
Renato "Lond" Cerqueira 2019-06-25 19:44:59 +02:00
commit f0a512c7ca
534 changed files with 12629 additions and 7877 deletions

View File

@ -174,8 +174,7 @@ jobs:
steps: steps:
- *attach_workspace - *attach_workspace
- run: bundle exec i18n-tasks check-normalized - run: bundle exec i18n-tasks check-normalized
- run: bundle exec i18n-tasks unused - run: bundle exec i18n-tasks unused -l en
- run: bundle exec i18n-tasks missing -t plural
- run: bundle exec i18n-tasks check-consistent-interpolations - run: bundle exec i18n-tasks check-consistent-interpolations
workflows: workflows:

View File

@ -30,8 +30,8 @@ plugins:
channel: eslint-5 channel: eslint-5
rubocop: rubocop:
enabled: true enabled: true
channel: rubocop-0-54 channel: rubocop-0-71
scss-lint: sass-lint:
enabled: true enabled: true
exclude_patterns: exclude_patterns:
- spec/ - spec/

10
.dependabot/config.yml Normal file
View File

@ -0,0 +1,10 @@
version: 1
update_configs:
- package_manager: "ruby:bundler"
directory: "/"
update_schedule: "weekly"
- package_manager: "javascript"
directory: "/"
update_schedule: "weekly"

View File

@ -10,6 +10,7 @@ DB_NAME=postgres
DB_PASS= DB_PASS=
DB_PORT=5432 DB_PORT=5432
# Optional ElasticSearch configuration # Optional ElasticSearch configuration
# You may also set ES_PREFIX to share the same cluster between multiple Mastodon servers (falls back to REDIS_NAMESPACE if not set)
# ES_ENABLED=true # ES_ENABLED=true
# ES_HOST=es # ES_HOST=es
# ES_PORT=9200 # ES_PORT=9200

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
patreon: mastodon
open_collective: mastodon

View File

@ -1,3 +1,6 @@
require:
- rubocop-rails
AllCops: AllCops:
TargetRubyVersion: 2.3 TargetRubyVersion: 2.3
Exclude: Exclude:
@ -82,6 +85,9 @@ Rails/Exit:
- 'lib/mastodon/*' - 'lib/mastodon/*'
- 'lib/cli.rb' - 'lib/cli.rb'
Rails/HelperInstanceVariable:
Enabled: false
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Enabled: false Enabled: false

37
.sass-lint.yml Normal file
View File

@ -0,0 +1,37 @@
# Linter Documentation:
# https://github.com/sasstools/sass-lint/tree/v1.13.1/docs/options
files:
include: app/javascript/styles/**/*.scss
ignore:
- app/javascript/styles/mastodon/reset.scss
rules:
# Disallows
no-color-literals: 0
no-css-comments: 0
no-duplicate-properties: 0
no-ids: 0
no-important: 0
no-mergeable-selectors: 0
no-misspelled-properties: 0
no-qualifying-elements: 0
no-transition-all: 0
no-vendor-prefixes: 0
# Nesting
force-element-nesting: 0
force-attribute-nesting: 0
force-pseudo-nesting: 0
# Name Formats
class-name-format: 0
leading-zero: 0
# Style Guide
attribute-quotes: 0
hex-length: 0
indentation: 0
nesting-depth: 0
property-sort-order: 0
quotes: 0

View File

@ -1,264 +0,0 @@
# Linter Documentation:
# https://github.com/brigade/scss-lint/blob/v0.42.2/lib/scss_lint/linter/README.md
scss_files: 'app/javascript/styles/**/*.scss'
exclude:
- app/javascript/styles/reset.scss
linters:
# Reports when you use improper spacing around ! (the "bang") in !default,
# !global, !important, and !optional flags.
BangFormat:
enabled: false
# Whether or not to prefer `border: 0` over `border: none`.
BorderZero:
enabled: false
# Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes).
ChainedClasses:
enabled: false
# Prefer hexadecimal color codes over color keywords.
# (e.g. `color: green` is a color keyword)
ColorKeyword:
enabled: false
# Prefer color literals (keywords or hexadecimal codes) to be used only in
# variable declarations. They should be referred to via variables everywhere
# else.
ColorVariable:
enabled: true
# Which form of comments to prefer in CSS.
Comment:
enabled: false
# Reports @debug statements (which you probably left behind accidentally).
DebugStatement:
enabled: false
# Rule sets should be ordered as follows:
# - @extend declarations
# - @include declarations without inner @content
# - properties, @include declarations with inner @content
# - nested rule sets.
DeclarationOrder:
enabled: false
# `scss-lint:disable` control comments should be preceded by a comment
# explaining why these linters are being disabled for this file.
# See https://github.com/brigade/scss-lint#disabling-linters-via-source for
# more information.
DisableLinterReason:
enabled: true
# Reports when you define the same property twice in a single rule set.
DuplicateProperty:
enabled: false
# Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks:
enabled: true
# Reports when you have an empty rule set.
EmptyRule:
enabled: true
# Reports when you have an @extend directive.
ExtendDirective:
enabled: false
# Files should always have a final newline. This results in better diffs
# when adding lines to the file, since SCM systems such as git won't
# think that you touched the last line.
FinalNewline:
enabled: false
# HEX colors should use three-character values where possible.
HexLength:
enabled: false
# HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
HexNotation:
enabled: true
# Avoid using ID selectors.
IdSelector:
enabled: false
# The basenames of @imported SCSS partials should not begin with an
# underscore and should not include the filename extension.
ImportPath:
enabled: false
# Avoid using !important in properties. It is usually indicative of a
# misunderstanding of CSS specificity and can lead to brittle code.
ImportantRule:
enabled: false
# Indentation should always be done in increments of 2 spaces.
Indentation:
enabled: true
width: 2
# Don't write leading zeros for numeric values with a decimal point.
LeadingZero:
enabled: false
# Reports when you define the same selector twice in a single sheet.
MergeableSelector:
enabled: false
# Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores.
NameFormat:
enabled: false
# Avoid nesting selectors too deeply.
NestingDepth:
enabled: false
# Always use placeholder selectors in @extend.
PlaceholderInExtend:
enabled: false
# Sort properties in a strict order.
PropertySortOrder:
enabled: false
# Reports when you use an unknown or disabled CSS property
# (ignoring vendor-prefixed properties).
PropertySpelling:
enabled: false
# Configure which units are allowed for property values.
PropertyUnits:
enabled: false
# Pseudo-elements, like ::before, and ::first-letter, should be declared
# with two colons. Pseudo-classes, like :hover and :first-child, should
# be declared with one colon.
PseudoElement:
enabled: true
# Avoid qualifying elements in selectors (also known as "tag-qualifying").
QualifyingElement:
enabled: false
# Don't write selectors with a depth of applicability greater than 3.
SelectorDepth:
enabled: false
# Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case.
SelectorFormat:
enabled: false
convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it.
Shorthand:
enabled: true
# Each property should have its own line, except in the special case of
# single line rulesets.
SingleLinePerProperty:
enabled: true
allow_single_line_rule_sets: true
# Split selectors onto separate lines after each comma, and have each
# individual selector occupy a single line.
SingleLinePerSelector:
enabled: true
# Commas in lists should be followed by a space.
SpaceAfterComma:
enabled: false
# Properties should be formatted with a single space separating the colon
# from the property's value.
SpaceAfterPropertyColon:
enabled: true
# Properties should be formatted with no space between the name and the
# colon.
SpaceAfterPropertyName:
enabled: true
# Variables should be formatted with a single space separating the colon
# from the variable's value.
SpaceAfterVariableColon:
enabled: true
# Variables should be formatted with no space between the name and the
# colon.
SpaceAfterVariableName:
enabled: false
# Operators should be formatted with a single space on both sides of an
# infix operator.
SpaceAroundOperator:
enabled: true
# Opening braces should be preceded by a single space.
SpaceBeforeBrace:
enabled: true
# Parentheses should not be padded with spaces.
SpaceBetweenParens:
enabled: false
# Enforces that string literals should be written with a consistent form
# of quotes (single or double).
StringQuotes:
enabled: false
# Property values, @extend, @include, and @import directives, and variable
# declarations should always end with a semicolon.
TrailingSemicolon:
enabled: true
# Reports lines containing trailing whitespace.
TrailingWhitespace:
enabled: true
# Don't write trailing zeros for numeric values with a decimal point.
TrailingZero:
enabled: false
# Don't use the `all` keyword to specify transition properties.
TransitionAll:
enabled: false
# Numeric values should not contain unnecessary fractional portions.
UnnecessaryMantissa:
enabled: false
# Do not use parent selector references (&) when they would otherwise
# be unnecessary.
UnnecessaryParentReference:
enabled: false
# URLs should be valid and not contain protocols or domain names.
UrlFormat:
enabled: true
# URLs should always be enclosed within quotes.
UrlQuotes:
enabled: true
# Properties, like color and font, are easier to read and maintain
# when defined using variables rather than literals.
VariableForProperty:
enabled: false
# Avoid vendor prefixes. Or rather: don't write them yourself.
VendorPrefix:
enabled: false
# Omit length units on zero values, e.g. `0px` vs. `0`.
ZeroUnit:
enabled: true

View File

@ -43,4 +43,4 @@ Gruntfile.js
# for specific ignore # for specific ignore
!.svgo.yml !.svgo.yml
!sass-lint/**/*.yml

View File

@ -3,6 +3,99 @@ Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [2.9.2] - 2019-06-22
### Added
- Add `short_description` and `approval_required` to `GET /api/v1/instance` ([Gargron](https://github.com/tootsuite/mastodon/pull/11146))
### Changed
- Change camera icon to paperclip icon in upload form ([koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/11149))
### Fixed
- Fix audio-only OGG and WebM files not being processed as such ([Gargron](https://github.com/tootsuite/mastodon/pull/11151))
- Fix audio not being downloaded from remote servers ([Gargron](https://github.com/tootsuite/mastodon/pull/11145))
## [2.9.1] - 2019-06-22
### Added
- Add moderation API ([Gargron](https://github.com/tootsuite/mastodon/pull/9387))
- Add audio uploads ([Gargron](https://github.com/tootsuite/mastodon/pull/11123), [Gargron](https://github.com/tootsuite/mastodon/pull/11141))
### Changed
- Change domain blocks to automatically support subdomains ([Gargron](https://github.com/tootsuite/mastodon/pull/11138))
- Change Nanobox configuration to bring it up to date ([danhunsaker](https://github.com/tootsuite/mastodon/pull/11083))
### Removed
- Remove expensive counters from federation page in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11139))
### Fixed
- Fix converted media being saved with original extension and mime type ([Gargron](https://github.com/tootsuite/mastodon/pull/11130))
- Fix layout of identity proofs settings ([acid-chicken](https://github.com/tootsuite/mastodon/pull/11126))
- Fix active scope only returning suspended users ([ThibG](https://github.com/tootsuite/mastodon/pull/11111))
- Fix sanitizer making block level elements unreadable ([Gargron](https://github.com/tootsuite/mastodon/pull/10836))
- Fix label for site theme not being translated in admin UI ([palindromordnilap](https://github.com/tootsuite/mastodon/pull/11121))
- Fix statuses not being filtered irreversibly in web UI under some circumstances ([ThibG](https://github.com/tootsuite/mastodon/pull/11113))
- Fix scrolling behaviour in compose form ([ThibG](https://github.com/tootsuite/mastodon/pull/11093))
## [2.9.0] - 2019-06-13
### Added
- **Add single-column mode in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/10807), [Gargron](https://github.com/tootsuite/mastodon/pull/10848), [Gargron](https://github.com/tootsuite/mastodon/pull/11003), [Gargron](https://github.com/tootsuite/mastodon/pull/10961), [Hanage999](https://github.com/tootsuite/mastodon/pull/10915), [noellabo](https://github.com/tootsuite/mastodon/pull/10917), [abcang](https://github.com/tootsuite/mastodon/pull/10859), [Gargron](https://github.com/tootsuite/mastodon/pull/10820), [Gargron](https://github.com/tootsuite/mastodon/pull/10835), [Gargron](https://github.com/tootsuite/mastodon/pull/10809), [Gargron](https://github.com/tootsuite/mastodon/pull/10963), [noellabo](https://github.com/tootsuite/mastodon/pull/10883), [Hanage999](https://github.com/tootsuite/mastodon/pull/10839))
- Add waiting time to the list of pending accounts in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10985))
- Add a keyboard shortcut to hide/show media in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10647), [Gargron](https://github.com/tootsuite/mastodon/pull/10838), [ThibG](https://github.com/tootsuite/mastodon/pull/10872))
- Add `account_id` param to `GET /api/v1/notifications` ([pwoolcoc](https://github.com/tootsuite/mastodon/pull/10796))
- Add confirmation modal for unboosting toots in web UI ([aurelien-reeves](https://github.com/tootsuite/mastodon/pull/10287))
- Add emoji suggestions to content warning and poll option fields in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10555))
- Add `source` attribute to response of `DELETE /api/v1/statuses/:id` ([ThibG](https://github.com/tootsuite/mastodon/pull/10669))
- Add some caching for HTML versions of public status pages ([ThibG](https://github.com/tootsuite/mastodon/pull/10701))
- Add button to conveniently copy OAuth code ([ThibG](https://github.com/tootsuite/mastodon/pull/11065))
### Changed
- **Change default layout to single column in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/10847))
- **Change light theme** ([Gargron](https://github.com/tootsuite/mastodon/pull/10992), [Gargron](https://github.com/tootsuite/mastodon/pull/10996), [yuzulabo](https://github.com/tootsuite/mastodon/pull/10754), [Gargron](https://github.com/tootsuite/mastodon/pull/10845))
- **Change preferences page into appearance, notifications, and other** ([Gargron](https://github.com/tootsuite/mastodon/pull/10977), [Gargron](https://github.com/tootsuite/mastodon/pull/10988))
- Change priority of delete activity forwards for replies and reblogs ([Gargron](https://github.com/tootsuite/mastodon/pull/11002))
- Change Mastodon logo to use primary text color of the given theme ([Gargron](https://github.com/tootsuite/mastodon/pull/10994))
- Change reblogs counter to be updated when boosted privately ([Gargron](https://github.com/tootsuite/mastodon/pull/10964))
- Change bio limit from 160 to 500 characters ([trwnh](https://github.com/tootsuite/mastodon/pull/10790))
- Change API rate limiting to reduce allowed unauthenticated requests ([ThibG](https://github.com/tootsuite/mastodon/pull/10860), [hinaloe](https://github.com/tootsuite/mastodon/pull/10868), [mayaeh](https://github.com/tootsuite/mastodon/pull/10867))
- Change help text of `tootctl emoji import` command to specify a gzipped TAR archive is required ([dariusk](https://github.com/tootsuite/mastodon/pull/11000))
- Change web UI to hide poll options behind content warnings ([ThibG](https://github.com/tootsuite/mastodon/pull/10983))
- Change silencing to ensure local effects and remote effects are the same for silenced local users ([ThibG](https://github.com/tootsuite/mastodon/pull/10575))
- Change `tootctl domains purge` to remove custom emoji as well ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10721))
- Change Docker image to keep `apt` working ([SuperSandro2000](https://github.com/tootsuite/mastodon/pull/10830))
### Removed
- Remove `dist-upgrade` from Docker image ([SuperSandro2000](https://github.com/tootsuite/mastodon/pull/10822))
### Fixed
- Fix RTL layout not being RTL within the columns area in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10990))
- Fix display of alternative text when a media attachment is not available in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10981))
- Fix not being able to directly switch between list timelines in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10973))
- Fix media sensitivity not being maintained in delete & redraft in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10980))
- Fix emoji picker being always displayed in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/10979), [yuzulabo](https://github.com/tootsuite/mastodon/pull/10801), [wcpaez](https://github.com/tootsuite/mastodon/pull/10978))
- Fix potential private status leak through caching ([ThibG](https://github.com/tootsuite/mastodon/pull/10969))
- Fix refreshing featured toots when the new collection is empty in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10971))
- Fix undoing domain block also undoing individual moderation on users from before the domain block ([ThibG](https://github.com/tootsuite/mastodon/pull/10660))
- Fix time not being local in the audit log ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10751))
- Fix statuses removed by moderation re-appearing on subsequent fetches ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10732))
- Fix misattribution of inlined announces if `attributedTo` isn't present in ActivityPub ([ThibG](https://github.com/tootsuite/mastodon/pull/10967))
- Fix `GET /api/v1/polls/:id` not requiring authentication for non-public polls ([Gargron](https://github.com/tootsuite/mastodon/pull/10960))
- Fix handling of blank poll options in ActivityPub ([ThibG](https://github.com/tootsuite/mastodon/pull/10946))
- Fix avatar preview aspect ratio on edit profile page ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10931))
- Fix web push notifications not being sent for polls ([ThibG](https://github.com/tootsuite/mastodon/pull/10864))
- Fix cut off letters in last paragraph of statuses in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/10821))
- Fix list not being automatically unpinned when it returns 404 in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11045))
- Fix login sometimes redirecting to paths that are not pages ([Gargron](https://github.com/tootsuite/mastodon/pull/11019))
## [2.8.4] - 2019-05-24 ## [2.8.4] - 2019-05-24
### Fixed ### Fixed

View File

@ -18,9 +18,9 @@ Bug reports and feature suggestions can be submitted to [GitHub Issues](https://
## Translations ## Translations
You can submit translations via [Weblate](https://weblate.joinmastodon.org/). They are periodically merged into the codebase. You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). They are periodically merged into the codebase.
[![Mastodon translation statistics by language](https://weblate.joinmastodon.org/widgets/mastodon/-/multi-auto.svg)](https://weblate.joinmastodon.org/) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
## Pull requests ## Pull requests

View File

@ -7,7 +7,6 @@ SHELL ["bash", "-c"]
ENV NODE_VER="8.15.0" ENV NODE_VER="8.15.0"
RUN echo "Etc/UTC" > /etc/localtime && \ RUN echo "Etc/UTC" > /etc/localtime && \
apt update && \ apt update && \
apt -y dist-upgrade && \
apt -y install wget make gcc g++ python && \ apt -y install wget make gcc g++ python && \
cd ~ && \ cd ~ && \
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \ wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \
@ -80,13 +79,12 @@ ARG GID=991
RUN apt update && \ RUN apt update && \
echo "Etc/UTC" > /etc/localtime && \ echo "Etc/UTC" > /etc/localtime && \
ln -s /opt/jemalloc/lib/* /usr/lib/ && \ ln -s /opt/jemalloc/lib/* /usr/lib/ && \
apt -y dist-upgrade && \
apt install -y whois wget && \ apt install -y whois wget && \
addgroup --gid $GID mastodon && \ addgroup --gid $GID mastodon && \
useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \ useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd
# Install masto runtime deps # Install mastodon runtime deps
RUN apt -y --no-install-recommends install \ RUN apt -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg \ libssl1.1 libpq5 imagemagick ffmpeg \
libicu60 libprotobuf10 libidn11 libyaml-0-2 \ libicu60 libprotobuf10 libidn11 libyaml-0-2 \
@ -95,7 +93,7 @@ RUN apt -y --no-install-recommends install \
ln -s /opt/mastodon /mastodon && \ ln -s /opt/mastodon /mastodon && \
gem install bundler && \ gem install bundler && \
rm -rf /var/cache && \ rm -rf /var/cache && \
rm -rf /var/lib/apt rm -rf /var/lib/apt/lists/*
# Add tini # Add tini
ENV TINI_VERSION="0.18.0" ENV TINI_VERSION="0.18.0"
@ -104,11 +102,11 @@ ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tin
RUN echo "$TINI_SUM tini" | sha256sum -c - RUN echo "$TINI_SUM tini" | sha256sum -c -
RUN chmod +x /tini RUN chmod +x /tini
# Copy over masto source, and dependencies from building, and set permissions # Copy over mastodon source, and dependencies from building, and set permissions
COPY --chown=mastodon:mastodon . /opt/mastodon COPY --chown=mastodon:mastodon . /opt/mastodon
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
# Run masto services in prod mode # Run mastodon services in prod mode
ENV RAILS_ENV="production" ENV RAILS_ENV="production"
ENV NODE_ENV="production" ENV NODE_ENV="production"

22
Gemfile
View File

@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.2' gem 'pghero', '~> 2.2'
gem 'dotenv-rails', '~> 2.7' gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.36', require: false gem 'aws-sdk-s3', '~> 1.42', require: false
gem 'fog-core', '<= 2.1.0' gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0' gem 'paperclip', '~> 6.0'
@ -53,7 +53,7 @@ gem 'htmlentities', '~> 4.3'
gem 'http', '~> 3.3' gem 'http', '~> 3.3'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2' gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2'
gem 'httplog', '~> 1.2' gem 'httplog', '~> 1.3'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1' gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
@ -62,7 +62,7 @@ gem 'nokogiri', '~> 1.10'
gem 'nsa', '~> 0.2' gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.7' gem 'oj', '~> 3.7'
gem 'ostatus2', '~> 2.0' gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.10' gem 'ox', '~> 2.11'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.0' gem 'pundit', '~> 2.0'
gem 'premailer-rails' gem 'premailer-rails'
@ -82,9 +82,9 @@ gem 'simple-navigation', '~> 4.0'
gem 'simple_form', '~> 4.1' gem 'simple_form', '~> 4.1'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.1.3' gem 'stoplight', '~> 2.1.3'
gem 'strong_migrations', '~> 0.3' gem 'strong_migrations', '~> 0.4'
gem 'tty-command', '~> 0.8', require: false gem 'tty-command', '~> 0.8', require: false
gem 'tty-prompt', '~> 0.18', require: false gem 'tty-prompt', '~> 0.19', require: false
gem 'twitter-text', '~> 1.14' gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2019' gem 'tzinfo-data', '~> 1.2019'
gem 'webpacker', '~> 4.0' gem 'webpacker', '~> 4.0'
@ -96,7 +96,7 @@ gem 'rdf-normalize', '~> 0.3'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.20' gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.3' gem 'fuubar', '~> 2.4'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.7' gem 'pry-byebug', '~> 3.7'
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
@ -108,15 +108,15 @@ group :production, :test do
end end
group :test do group :test do
gem 'capybara', '~> 3.18' gem 'capybara', '~> 3.24'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.9' gem 'faker', '~> 1.9'
gem 'microformats', '~> 4.1' gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.16', require: false gem 'simplecov', '~> 0.16', require: false
gem 'webmock', '~> 3.5' gem 'webmock', '~> 3.6'
gem 'parallel_tests', '~> 2.28' gem 'parallel_tests', '~> 2.29'
end end
group :development do group :development do
@ -128,10 +128,10 @@ group :development do
gem 'letter_opener', '~> 1.7' gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.3' gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 0.68', require: false gem 'rubocop', '~> 0.71', require: false
gem 'rubocop-rails', '~> 2.0', require: false
gem 'brakeman', '~> 4.5', require: false gem 'brakeman', '~> 4.5', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.58', require: false
gem 'capistrano', '~> 3.11' gem 'capistrano', '~> 3.11'
gem 'capistrano-rails', '~> 1.4' gem 'capistrano-rails', '~> 1.4'

View File

@ -75,20 +75,20 @@ GEM
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-eventstream (1.0.2) aws-eventstream (1.0.3)
aws-partitions (1.151.0) aws-partitions (1.175.0)
aws-sdk-core (3.48.4) aws-sdk-core (3.55.0)
aws-eventstream (~> 1.0, >= 1.0.2) aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1.0) aws-partitions (~> 1.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.17.0) aws-sdk-kms (1.21.0)
aws-sdk-core (~> 3, >= 3.48.2) aws-sdk-core (~> 3, >= 3.53.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.36.1) aws-sdk-s3 (1.42.0)
aws-sdk-core (~> 3, >= 3.48.2) aws-sdk-core (~> 3, >= 3.53.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.0) aws-sigv4 (1.1.0)
aws-eventstream (~> 1.0, >= 1.0.2) aws-eventstream (~> 1.0, >= 1.0.2)
bcrypt (3.1.12) bcrypt (3.1.12)
@ -103,7 +103,7 @@ GEM
ffi (~> 1.10.0) ffi (~> 1.10.0)
bootsnap (1.4.4) bootsnap (1.4.4)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.5.0) brakeman (4.5.1)
browser (2.5.3) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
bullet (6.0.0) bullet (6.0.0)
@ -129,13 +129,13 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (3.18.0) capybara (3.24.0)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.8)
rack (>= 1.6.0) rack (>= 1.6.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (~> 1.2) regexp_parser (~> 1.5)
xpath (~> 3.2) xpath (~> 3.2)
case_transform (0.2) case_transform (0.2)
activesupport activesupport
@ -231,7 +231,7 @@ GEM
fugit (1.1.6) fugit (1.1.6)
et-orbi (~> 1.1, >= 1.1.6) et-orbi (~> 1.1, >= 1.1.6)
raabro (~> 1.1) raabro (~> 1.1)
fuubar (2.3.2) fuubar (2.4.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
get_process_mem (0.2.3) get_process_mem (0.2.3)
@ -253,7 +253,7 @@ GEM
railties (>= 4.0.1) railties (>= 4.0.1)
hamster (3.0.0) hamster (3.0.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
hashdiff (0.3.7) hashdiff (0.4.0)
hashie (3.6.0) hashie (3.6.0)
heapy (0.1.4) heapy (0.1.4)
highline (2.0.1) highline (2.0.1)
@ -269,7 +269,7 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.1.1) http-form_data (2.1.1)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httplog (1.2.2) httplog (1.3.1)
rack (>= 1.0) rack (>= 1.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.6.0) i18n (1.6.0)
@ -320,7 +320,7 @@ GEM
letter_opener (~> 1.0) letter_opener (~> 1.0)
railties (>= 3.2) railties (>= 3.2)
link_header (0.0.8) link_header (0.0.8)
lograge (0.11.0) lograge (0.11.2)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
@ -351,7 +351,7 @@ GEM
msgpack (1.2.10) msgpack (1.2.10)
multi_json (1.13.1) multi_json (1.13.1)
multipart-post (2.0.0) multipart-post (2.0.0)
necromancer (0.4.0) necromancer (0.5.0)
net-ldap (0.16.1) net-ldap (0.16.1)
net-scp (1.2.1) net-scp (1.2.1)
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
@ -382,7 +382,7 @@ GEM
addressable (~> 2.5) addressable (~> 2.5)
http (~> 3.0) http (~> 3.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
ox (2.10.0) ox (2.11.0)
paperclip (6.0.0) paperclip (6.0.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@ -393,7 +393,7 @@ GEM
av (~> 0.9.0) av (~> 0.9.0)
paperclip (>= 2.5.2) paperclip (>= 2.5.2)
parallel (1.17.0) parallel (1.17.0)
parallel_tests (2.28.0) parallel_tests (2.29.0)
parallel parallel
parser (2.6.3.0) parser (2.6.3.0)
ast (~> 2.4.0) ast (~> 2.4.0)
@ -401,7 +401,7 @@ GEM
equatable (~> 0.5.0) equatable (~> 0.5.0)
tty-color (~> 0.4.0) tty-color (~> 0.4.0)
pg (1.1.4) pg (1.1.4)
pghero (2.2.0) pghero (2.2.1)
activerecord activerecord
pkg-config (1.3.7) pkg-config (1.3.7)
premailer (1.11.1) premailer (1.11.1)
@ -420,7 +420,7 @@ GEM
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (3.0.3) public_suffix (3.1.0)
puma (3.12.1) puma (3.12.1)
pundit (2.0.1) pundit (2.0.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -470,15 +470,12 @@ GEM
thor (>= 0.19.0, < 2.0) thor (>= 0.19.0, < 2.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (12.3.2) rake (12.3.2)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
rdf (3.0.9) rdf (3.0.9)
hamster (~> 3.0) hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.3) rdf-normalize (0.3.3)
rdf (>= 2.2, < 4.0) rdf (>= 2.2, < 4.0)
redis (4.1.0) redis (4.1.2)
redis-actionpack (5.0.2) redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
@ -497,7 +494,7 @@ GEM
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-store (1.5.0) redis-store (1.5.0)
redis (>= 2.2, < 5) redis (>= 2.2, < 5)
regexp_parser (1.4.0) regexp_parser (1.5.1)
request_store (1.4.1) request_store (1.4.1)
rack (>= 1.4) rack (>= 1.4)
responders (2.4.1) responders (2.4.1)
@ -527,31 +524,26 @@ GEM
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.8.0) rspec-support (3.8.0)
rubocop (0.68.1) rubocop (0.71.0)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1) parser (>= 2.6)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.6) unicode-display_width (>= 1.4.0, < 1.7)
ruby-progressbar (1.10.0) rubocop-rails (2.0.1)
rack (>= 1.1)
rubocop (>= 0.70.0)
ruby-progressbar (1.10.1)
ruby-saml (1.9.0) ruby-saml (1.9.0)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
rufus-scheduler (3.5.2) rufus-scheduler (3.5.2)
fugit (~> 1.1, >= 1.1.5) fugit (~> 1.1, >= 1.1.5)
safe_yaml (1.0.4) safe_yaml (1.0.5)
sanitize (5.0.0) sanitize (5.0.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.8.0) nokogiri (>= 1.8.0)
nokogumbo (~> 2.0) nokogumbo (~> 2.0)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
scss_lint (0.58.0)
rake (>= 0.9, < 13)
sass (~> 3.5, >= 3.5.5)
sidekiq (5.2.7) sidekiq (5.2.7)
connection_pool (~> 2.2, >= 2.2.2) connection_pool (~> 2.2, >= 2.2.2)
rack (>= 1.5.0) rack (>= 1.5.0)
@ -593,8 +585,8 @@ GEM
stoplight (2.1.3) stoplight (2.1.3)
streamio-ffmpeg (3.0.2) streamio-ffmpeg (3.0.2)
multi_json (~> 1.8) multi_json (~> 1.8)
strong_migrations (0.3.1) strong_migrations (0.4.0)
activerecord (>= 3.2.0) activerecord (>= 5)
temple (0.8.1) temple (0.8.1)
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
@ -603,22 +595,19 @@ GEM
thor (0.20.3) thor (0.20.3)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.9) tilt (2.0.9)
timers (4.2.0)
tty-color (0.4.3) tty-color (0.4.3)
tty-command (0.8.2) tty-command (0.8.2)
pastel (~> 0.7.0) pastel (~> 0.7.0)
tty-cursor (0.6.0) tty-cursor (0.7.0)
tty-prompt (0.18.1) tty-prompt (0.19.0)
necromancer (~> 0.4.0) necromancer (~> 0.5.0)
pastel (~> 0.7.0) pastel (~> 0.7.0)
timers (~> 4.0) tty-reader (~> 0.6.0)
tty-cursor (~> 0.6.0) tty-reader (0.6.0)
tty-reader (~> 0.5.0) tty-cursor (~> 0.7)
tty-reader (0.5.0) tty-screen (~> 0.7)
tty-cursor (~> 0.6.0)
tty-screen (~> 0.6.4)
wisper (~> 2.0.0) wisper (~> 2.0.0)
tty-screen (0.6.5) tty-screen (0.7.0)
twitter-text (1.14.7) twitter-text (1.14.7)
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (1.2.5) tzinfo (1.2.5)
@ -628,15 +617,15 @@ GEM
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.5) unf_ext (0.0.7.5)
unicode-display_width (1.5.0) unicode-display_width (1.6.0)
uniform_notifier (1.12.1) uniform_notifier (1.12.1)
warden (1.2.8) warden (1.2.8)
rack (>= 2.0.6) rack (>= 2.0.6)
webmock (3.5.1) webmock (3.6.0)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff (>= 0.4.0, < 2.0.0)
webpacker (4.0.2) webpacker (4.0.7)
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
@ -658,7 +647,7 @@ DEPENDENCIES
active_record_query_trace (~> 1.6) active_record_query_trace (~> 1.6)
addressable (~> 2.6) addressable (~> 2.6)
annotate (~> 2.7) annotate (~> 2.7)
aws-sdk-s3 (~> 1.36) aws-sdk-s3 (~> 1.42)
better_errors (~> 2.5) better_errors (~> 2.5)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
blurhash (~> 0.1) blurhash (~> 0.1)
@ -671,7 +660,7 @@ DEPENDENCIES
capistrano-rails (~> 1.4) capistrano-rails (~> 1.4)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 3.18) capybara (~> 3.24)
charlock_holmes (~> 0.7.6) charlock_holmes (~> 0.7.6)
chewy (~> 5.0) chewy (~> 5.0)
cld3 (~> 3.2.4) cld3 (~> 3.2.4)
@ -689,7 +678,7 @@ DEPENDENCIES
fastimage fastimage
fog-core (<= 2.1.0) fog-core (<= 2.1.0)
fog-openstack (~> 0.3) fog-openstack (~> 0.3)
fuubar (~> 2.3) fuubar (~> 2.4)
goldfinger (~> 2.1) goldfinger (~> 2.1)
hamlit-rails (~> 0.2) hamlit-rails (~> 0.2)
hiredis (~> 0.6) hiredis (~> 0.6)
@ -697,7 +686,7 @@ DEPENDENCIES
http (~> 3.3) http (~> 3.3)
http_accept_language (~> 2.1) http_accept_language (~> 2.1)
http_parser.rb (~> 0.6)! http_parser.rb (~> 0.6)!
httplog (~> 1.2) httplog (~> 1.3)
i18n-tasks (~> 0.9) i18n-tasks (~> 0.9)
idn-ruby idn-ruby
iso-639 iso-639
@ -721,10 +710,10 @@ DEPENDENCIES
omniauth-cas (~> 1.1) omniauth-cas (~> 1.1)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
ostatus2 (~> 2.0) ostatus2 (~> 2.0)
ox (~> 2.10) ox (~> 2.11)
paperclip (~> 6.0) paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6) paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.28) parallel_tests (~> 2.29)
pg (~> 1.1) pg (~> 1.1)
pghero (~> 2.2) pghero (~> 2.2)
pkg-config (~> 1.3) pkg-config (~> 1.3)
@ -748,9 +737,9 @@ DEPENDENCIES
rqrcode (~> 0.10) rqrcode (~> 0.10)
rspec-rails (~> 3.8) rspec-rails (~> 3.8)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rubocop (~> 0.68) rubocop (~> 0.71)
rubocop-rails (~> 2.0)
sanitize (~> 5.0) sanitize (~> 5.0)
scss_lint (~> 0.58)
sidekiq (~> 5.2) sidekiq (~> 5.2)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.0) sidekiq-scheduler (~> 3.0)
@ -762,13 +751,13 @@ DEPENDENCIES
stackprof stackprof
stoplight (~> 2.1.3) stoplight (~> 2.1.3)
streamio-ffmpeg (~> 3.0) streamio-ffmpeg (~> 3.0)
strong_migrations (~> 0.3) strong_migrations (~> 0.4)
thor (~> 0.20) thor (~> 0.20)
tty-command (~> 0.8) tty-command (~> 0.8)
tty-prompt (~> 0.18) tty-prompt (~> 0.19)
twitter-text (~> 1.14) twitter-text (~> 1.14)
tzinfo-data (~> 1.2019) tzinfo-data (~> 1.2019)
webmock (~> 3.5) webmock (~> 3.6)
webpacker (~> 4.0) webpacker (~> 4.0)
webpush webpush

View File

@ -4,13 +4,13 @@
[![GitHub release](https://img.shields.io/github/release/tootsuite/mastodon.svg)][releases] [![GitHub release](https://img.shields.io/github/release/tootsuite/mastodon.svg)][releases]
[![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci] [![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate] [![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate]
[![Translation status](https://weblate.joinmastodon.org/widgets/mastodon/-/svg-badge.svg)][weblate] [![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
[![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker] [![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker]
[releases]: https://github.com/tootsuite/mastodon/releases [releases]: https://github.com/tootsuite/mastodon/releases
[circleci]: https://circleci.com/gh/tootsuite/mastodon [circleci]: https://circleci.com/gh/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon [code_climate]: https://codeclimate.com/github/tootsuite/mastodon
[weblate]: https://weblate.joinmastodon.org/engage/mastodon/ [crowdin]: https://crowdin.com/project/mastodon
[docker]: https://hub.docker.com/r/tootsuite/mastodon/ [docker]: https://hub.docker.com/r/tootsuite/mastodon/
Mastodon is a **free, open-source social network server** based on ActivityPub. Follow friends and discover new ones. Publish anything you want: links, pictures, text, video. All servers of Mastodon are interoperable as a federated network, i.e. users on one server can seamlessly communicate with users from another one. This includes non-Mastodon software that also implements ActivityPub! Mastodon is a **free, open-source social network server** based on ActivityPub. Follow friends and discover new ones. Publish anything you want: links, pictures, text, video. All servers of Mastodon are interoperable as a federated network, i.e. users on one server can seamlessly communicate with users from another one. This includes non-Mastodon software that also implements ActivityPub!

View File

@ -46,8 +46,6 @@ class AccountsController < ApplicationController
end end
format.json do format.json do
mark_cacheable!
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
end end

View File

@ -9,8 +9,6 @@ class ActivityPub::CollectionsController < Api::BaseController
before_action :set_cache_headers before_action :set_cache_headers
def show def show
skip_session!
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new( ActiveModelSerializers::SerializableResource.new(
collection_presenter, collection_presenter,

View File

@ -10,10 +10,7 @@ class ActivityPub::OutboxesController < Api::BaseController
before_action :set_cache_headers before_action :set_cache_headers
def show def show
unless page_requested? expires_in 1.minute, public: true unless page_requested?
skip_session!
expires_in 1.minute, public: true
end
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end

View File

@ -48,13 +48,13 @@ module Admin
def approve def approve
authorize @account.user, :approve? authorize @account.user, :approve?
@account.user.approve! @account.user.approve!
redirect_to admin_accounts_path(pending: '1') redirect_to admin_pending_accounts_path
end end
def reject def reject
authorize @account.user, :reject? authorize @account.user, :reject?
SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
redirect_to admin_accounts_path(pending: '1') redirect_to admin_pending_accounts_path
end end
def unsilence def unsilence
@ -127,6 +127,7 @@ module Admin
:by_domain, :by_domain,
:active, :active,
:pending, :pending,
:disabled,
:silenced, :silenced,
:suspended, :suspended,
:username, :username,

View File

@ -13,7 +13,7 @@ module Admin
authorize :domain_block, :create? authorize :domain_block, :create?
@domain_block = DomainBlock.new(resource_params) @domain_block = DomainBlock.new(resource_params)
existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
@domain_block.save @domain_block.save
@ -41,7 +41,7 @@ module Admin
def destroy def destroy
authorize @domain_block, :destroy? authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?) UnblockDomainService.new.call(@domain_block)
log_action :destroy, @domain_block log_action :destroy, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg') redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg')
end end
@ -53,11 +53,7 @@ module Admin
end end
def resource_params def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :retroactive) params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports)
end
def retroactive_unblock?
ActiveRecord::Type.lookup(:boolean).cast(resource_params[:retroactive])
end end
end end
end end

View File

@ -18,7 +18,7 @@ module Admin
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
@available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url) @available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size) @media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
@domain_block = DomainBlock.find_by(domain: params[:id]) @domain_block = DomainBlock.rule_for(params[:id])
end end
private private

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
class Api::V1::Admin::AccountActionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }
before_action :require_staff!
before_action :set_account
def create
account_action = Admin::AccountAction.new(resource_params)
account_action.target_account = @account
account_action.current_account = current_account
account_action.save!
render_empty
end
private
def set_account
@account = Account.find(params[:account_id])
end
def resource_params
params.permit(
:type,
:report_id,
:warning_preset_id,
:text,
:send_email_notification
)
end
end

View File

@ -0,0 +1,128 @@
# frozen_string_literal: true
class Api::V1::Admin::AccountsController < Api::BaseController
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
before_action :require_staff!
before_action :set_accounts, only: :index
before_action :set_account, except: :index
before_action :require_local_account!, only: [:enable, :approve, :reject]
after_action :insert_pagination_headers, only: :index
FILTER_PARAMS = %i(
local
remote
by_domain
active
pending
disabled
silenced
suspended
username
display_name
email
ip
staff
).freeze
PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze
def index
authorize :account, :index?
render json: @accounts, each_serializer: REST::Admin::AccountSerializer
end
def show
authorize @account, :show?
render json: @account, serializer: REST::Admin::AccountSerializer
end
def enable
authorize @account.user, :enable?
@account.user.enable!
log_action :enable, @account.user
render json: @account, serializer: REST::Admin::AccountSerializer
end
def approve
authorize @account.user, :approve?
@account.user.approve!
render json: @account, serializer: REST::Admin::AccountSerializer
end
def reject
authorize @account.user, :reject?
SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
render json: @account, serializer: REST::Admin::AccountSerializer
end
def unsilence
authorize @account, :unsilence?
@account.unsilence!
log_action :unsilence, @account
render json: @account, serializer: REST::Admin::AccountSerializer
end
def unsuspend
authorize @account, :unsuspend?
@account.unsuspend!
log_action :unsuspend, @account
render json: @account, serializer: REST::Admin::AccountSerializer
end
private
def set_accounts
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_account
@account = Account.find(params[:id])
end
def filtered_accounts
AccountFilter.new(filter_params).results
end
def filter_params
params.permit(*FILTER_PARAMS)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.first.id
end
def records_continue?
@accounts.size == limit_param(LIMIT)
end
def pagination_params(core_params)
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
end
def require_local_account!
forbidden unless @account.local? && @account.user.present?
end
end

View File

@ -0,0 +1,108 @@
# frozen_string_literal: true
class Api::V1::Admin::ReportsController < Api::BaseController
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
before_action :require_staff!
before_action :set_reports, only: :index
before_action :set_report, except: :index
after_action :insert_pagination_headers, only: :index
FILTER_PARAMS = %i(
resolved
account_id
target_account_id
).freeze
PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze
def index
authorize :report, :index?
render json: @reports, each_serializer: REST::Admin::ReportSerializer
end
def show
authorize @report, :show?
render json: @report, serializer: REST::Admin::ReportSerializer
end
def assign_to_self
authorize @report, :update?
@report.update!(assigned_account_id: current_account.id)
log_action :assigned_to_self, @report
render json: @report, serializer: REST::Admin::ReportSerializer
end
def unassign
authorize @report, :update?
@report.update!(assigned_account_id: nil)
log_action :unassigned, @report
render json: @report, serializer: REST::Admin::ReportSerializer
end
def reopen
authorize @report, :update?
@report.unresolve!
log_action :reopen, @report
render json: @report, serializer: REST::Admin::ReportSerializer
end
def resolve
authorize @report, :update?
@report.resolve!(current_account)
log_action :resolve, @report
render json: @report, serializer: REST::Admin::ReportSerializer
end
private
def set_reports
@reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_report
@report = Report.find(params[:id])
end
def filtered_reports
ReportFilter.new(filter_params).results
end
def filter_params
params.permit(*FILTER_PARAMS)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty?
end
def pagination_max_id
@reports.last.id
end
def pagination_since_id
@reports.first.id
end
def records_continue?
@reports.size == limit_param(LIMIT)
end
def pagination_params(core_params)
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
end
end

View File

@ -44,7 +44,7 @@ class Api::V1::NotificationsController < Api::BaseController
end end
def browserable_account_notifications def browserable_account_notifications
current_account.notifications.browserable(exclude_types) current_account.notifications.browserable(exclude_types, from_account)
end end
def target_statuses_from_notifications def target_statuses_from_notifications
@ -81,6 +81,10 @@ class Api::V1::NotificationsController < Api::BaseController
val val
end end
def from_account
params[:account_id]
end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit, :exclude_types).permit(:limit, exclude_types: []).merge(core_params) params.slice(:limit, :exclude_types).permit(:limit, exclude_types: []).merge(core_params)
end end

View File

@ -1,13 +1,28 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::PollsController < Api::BaseController class Api::V1::PollsController < Api::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show
before_action :set_poll
before_action :refresh_poll
respond_to :json respond_to :json
def show def show
@poll = Poll.attached.find(params[:id])
ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale?
render json: @poll, serializer: REST::PollSerializer, include_results: true render json: @poll, serializer: REST::PollSerializer, include_results: true
end end
private
def set_poll
@poll = Poll.attached.find(params[:id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
end
def refresh_poll
ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale?
end
end end

View File

@ -51,6 +51,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
def data_params def data_params
return {} if params[:data].blank? return {} if params[:data].blank?
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention]) params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll])
end end
end end

View File

@ -66,7 +66,7 @@ class Api::V1::StatusesController < Api::BaseController
RemovalWorker.perform_async(@status.id) RemovalWorker.perform_async(@status.id)
render_empty render json: @status, serializer: REST::StatusSerializer, source_requested: true
end end
private private

View File

@ -22,6 +22,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
favourite: alerts_enabled, favourite: alerts_enabled,
reblog: alerts_enabled, reblog: alerts_enabled,
mention: alerts_enabled, mention: alerts_enabled,
poll: alerts_enabled,
}, },
} }
@ -57,6 +58,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end end
def data_params def data_params
@data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention]) @data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll])
end end
end end

View File

@ -152,11 +152,6 @@ class ApplicationController < ActionController::Base
end end
def mark_cacheable! def mark_cacheable!
skip_session!
expires_in 0, public: true expires_in 0, public: true
end end
def skip_session!
request.session_options[:skip] = true
end
end end

View File

@ -70,7 +70,6 @@ module AccountControllerConcern
def check_account_suspension def check_account_suspension
if @account.suspended? if @account.suspended?
skip_session!
expires_in(3.minutes, public: true) expires_in(3.minutes, public: true)
gone gone
end end

View File

@ -1,10 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class CustomCssController < ApplicationController class CustomCssController < ApplicationController
skip_before_action :store_current_location
before_action :set_cache_headers before_action :set_cache_headers
def show def show
skip_session!
render plain: Setting.custom_css || '', content_type: 'text/css' render plain: Setting.custom_css || '', content_type: 'text/css'
end end
end end

View File

@ -7,8 +7,6 @@ class EmojisController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.json do format.json do
skip_session!
render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
end end

View File

@ -19,10 +19,7 @@ class FollowerAccountsController < ApplicationController
format.json do format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
if params[:page].blank? expires_in 3.minutes, public: true if params[:page].blank?
skip_session!
expires_in 3.minutes, public: true
end
render json: collection_presenter, render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer, serializer: ActivityPub::CollectionSerializer,

View File

@ -19,10 +19,7 @@ class FollowingAccountsController < ApplicationController
format.json do format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
if params[:page].blank? expires_in 3.minutes, public: true if params[:page].blank?
skip_session!
expires_in 3.minutes, public: true
end
render json: collection_presenter, render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer, serializer: ActivityPub::CollectionSerializer,

View File

@ -58,7 +58,7 @@ class HomeController < ApplicationController
if request.path.start_with?('/web') if request.path.start_with?('/web')
new_user_session_path new_user_session_path
elsif single_user_mode? elsif single_user_mode?
short_account_path(Account.local.where(suspended: false).first) short_account_path(Account.local.without_suspended.first)
else else
about_path about_path
end end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class ManifestsController < ApplicationController class ManifestsController < ApplicationController
skip_before_action :store_current_location
def show def show
render json: InstancePresenter.new, serializer: ManifestSerializer render json: InstancePresenter.new, serializer: ManifestSerializer
end end

View File

@ -3,8 +3,12 @@
class MediaController < ApplicationController class MediaController < ApplicationController
include Authorization include Authorization
skip_before_action :store_current_location
before_action :set_media_attachment before_action :set_media_attachment
before_action :verify_permitted_status! before_action :verify_permitted_status!
before_action :check_playable, only: :player
before_action :allow_iframing, only: :player
content_security_policy only: :player do |p| content_security_policy only: :player do |p|
p.frame_ancestors(false) p.frame_ancestors(false)
@ -16,8 +20,6 @@ class MediaController < ApplicationController
def player def player
@body_classes = 'player' @body_classes = 'player'
response.headers['X-Frame-Options'] = 'ALLOWALL'
raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv?
end end
private private
@ -32,4 +34,12 @@ class MediaController < ApplicationController
# Reraise in order to get a 404 instead of a 403 error code # Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound
end end
def check_playable
not_found unless @media_attachment.larger_media_format?
end
def allow_iframing
response.headers['X-Frame-Options'] = 'ALLOWALL'
end
end end

View File

@ -3,6 +3,8 @@
class MediaProxyController < ApplicationController class MediaProxyController < ApplicationController
include RoutingHelper include RoutingHelper
skip_before_action :store_current_location
def show def show
RedisLock.acquire(lock_options) do |lock| RedisLock.acquire(lock_options) do |lock|
if lock.acquired? if lock.acquired?
@ -37,6 +39,6 @@ class MediaProxyController < ApplicationController
end end
def reject_media? def reject_media?
DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media? DomainBlock.reject_media?(@media_attachment.account.domain)
end end
end end

View File

@ -56,8 +56,4 @@ class Settings::IdentityProofsController < Settings::BaseController
def post_params def post_params
params.require(:account_identity_proof).permit(:post_status, :status_text) params.require(:account_identity_proof).permit(:post_status, :status_text)
end end
def set_body_classes
@body_classes = ''
end
end end

View File

@ -1,32 +0,0 @@
# frozen_string_literal: true
class Settings::NotificationsController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
def show; end
def update
user_settings.update(user_settings_params.to_h)
if current_user.save
redirect_to settings_notifications_path, notice: I18n.t('generic.changes_saved_msg')
else
render :show
end
end
private
def user_settings
UserSettingsDecorator.new(current_user)
end
def user_settings_params
params.require(:user).permit(
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
interactions: %i(must_be_follower must_be_following must_be_following_dm)
)
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Settings::Preferences::AppearanceController < Settings::PreferencesController
private
def after_update_redirect_path
settings_preferences_appearance_path
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Settings::Preferences::NotificationsController < Settings::PreferencesController
private
def after_update_redirect_path
settings_preferences_notifications_path
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Settings::Preferences::OtherController < Settings::PreferencesController
private
def after_update_redirect_path
settings_preferences_other_path
end
end

View File

@ -12,7 +12,7 @@ class Settings::PreferencesController < Settings::BaseController
if current_user.update(user_params) if current_user.update(user_params)
I18n.locale = current_user.locale I18n.locale = current_user.locale
redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg') redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg')
else else
render :show render :show
end end
@ -20,6 +20,10 @@ class Settings::PreferencesController < Settings::BaseController
private private
def after_update_redirect_path
settings_preferences_path
end
def user_settings def user_settings
UserSettingsDecorator.new(current_user) UserSettingsDecorator.new(current_user)
end end
@ -50,8 +54,9 @@ class Settings::PreferencesController < Settings::BaseController
:setting_hide_network, :setting_hide_network,
:setting_aggregate_reblogs, :setting_aggregate_reblogs,
:setting_show_application, :setting_show_application,
:setting_advanced_layout,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
interactions: %i(must_be_follower must_be_following) interactions: %i(must_be_follower must_be_following must_be_following_dm)
) )
end end
end end

View File

@ -27,7 +27,7 @@ class StatusesController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
mark_cacheable! unless user_signed_in? expires_in 10.seconds, public: true if current_account.nil?
@body_classes = 'with-modals' @body_classes = 'with-modals'
@ -38,8 +38,6 @@ class StatusesController < ApplicationController
end end
format.json do format.json do
mark_cacheable! unless @stream_entry.hidden?
render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
end end
@ -48,8 +46,6 @@ class StatusesController < ApplicationController
end end
def activity def activity
skip_session!
render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
end end
@ -58,7 +54,6 @@ class StatusesController < ApplicationController
def embed def embed
raise ActiveRecord::RecordNotFound if @status.hidden? raise ActiveRecord::RecordNotFound if @status.hidden?
skip_session!
expires_in 180, public: true expires_in 180, public: true
response.headers['X-Frame-Options'] = 'ALLOWALL' response.headers['X-Frame-Options'] = 'ALLOWALL'
@autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay]) @autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay])
@ -67,8 +62,6 @@ class StatusesController < ApplicationController
end end
def replies def replies
skip_session!
render json: replies_collection_presenter, render json: replies_collection_presenter,
serializer: ActivityPub::CollectionSerializer, serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter, adapter: ActivityPub::Adapter,

View File

@ -15,14 +15,13 @@ class StreamEntriesController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status' expires_in 5.minutes, public: true unless @stream_entry.hidden?
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity)
end end
format.atom do format.atom do
unless @stream_entry.hidden? || @stream_entry.local_only? expires_in 3.minutes, public: true unless @stream_entry.hidden?
skip_session!
expires_in 3.minutes, public: true
end
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true)) render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
end end
@ -50,7 +49,7 @@ class StreamEntriesController < ApplicationController
def set_stream_entry def set_stream_entry
@stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id]) @stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id])
@type = @stream_entry.activity_type.downcase @type = 'status'
raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil? raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil?
authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only? authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only?

View File

@ -16,24 +16,32 @@ module StreamEntriesHelper
if user_signed_in? if user_signed_in?
if account.id == current_user.account_id if account.id == current_user.account_id
link_to settings_profile_url, class: 'button logo-button' do link_to settings_profile_url, class: 'button logo-button' do
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('settings.edit_profile')]) safe_join([svg_logo, t('settings.edit_profile')])
end end
elsif current_account.following?(account) || current_account.requested?(account) elsif current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')]) safe_join([svg_logo, t('accounts.unfollow')])
end end
elsif !(account.memorial? || account.moved?) elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')]) safe_join([svg_logo, t('accounts.follow')])
end end
end end
elsif !(account.memorial? || account.moved?) elsif !(account.memorial? || account.moved?)
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')]) safe_join([svg_logo, t('accounts.follow')])
end end
end end
end end
def svg_logo
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
end
def svg_logo_full
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
end
def account_badge(account, all: false) def account_badge(account, all: false)
if account.bot? if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" fill="#fff"/></svg> <svg xmlns="http://www.w3.org/2000/svg"><symbol id="mastodon-svg-logo" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" /></symbol></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -8,6 +8,7 @@ const messages = defineMessages({
export const ALERT_SHOW = 'ALERT_SHOW'; export const ALERT_SHOW = 'ALERT_SHOW';
export const ALERT_DISMISS = 'ALERT_DISMISS'; export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR'; export const ALERT_CLEAR = 'ALERT_CLEAR';
export const ALERT_NOOP = 'ALERT_NOOP';
export function dismissAlert(alert) { export function dismissAlert(alert) {
return { return {
@ -36,7 +37,7 @@ export function showAlertForError(error) {
if (status === 404 || status === 410) { if (status === 404 || status === 410) {
// Skip these errors as they are reflected in the UI // Skip these errors as they are reflected in the UI
return {}; return { type: ALERT_NOOP };
} }
let message = statusText; let message = statusText;

View File

@ -64,6 +64,14 @@ const messages = defineMessages({
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
}); });
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
export const ensureComposeIsVisible = (getState, routerHistory) => {
if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
routerHistory.push('/statuses/new');
}
};
export function changeCompose(text) { export function changeCompose(text) {
return { return {
type: COMPOSE_CHANGE, type: COMPOSE_CHANGE,
@ -78,9 +86,7 @@ export function replyCompose(status, routerHistory) {
status: status, status: status,
}); });
if (!getState().getIn(['compose', 'mounted'])) { ensureComposeIsVisible(getState, routerHistory);
routerHistory.push('/statuses/new');
}
}; };
}; };
@ -103,9 +109,7 @@ export function mentionCompose(account, routerHistory) {
account: account, account: account,
}); });
if (!getState().getIn(['compose', 'mounted'])) { ensureComposeIsVisible(getState, routerHistory);
routerHistory.push('/statuses/new');
}
}; };
}; };
@ -116,9 +120,7 @@ export function directCompose(account, routerHistory) {
account: account, account: account,
}); });
if (!getState().getIn(['compose', 'mounted'])) { ensureComposeIsVisible(getState, routerHistory);
routerHistory.push('/statuses/new');
}
}; };
}; };
@ -385,7 +387,7 @@ export function readyComposeSuggestionsAccounts(token, accounts) {
}; };
}; };
export function selectComposeSuggestion(position, token, suggestion) { export function selectComposeSuggestion(position, token, suggestion, path) {
return (dispatch, getState) => { return (dispatch, getState) => {
let completion, startPosition; let completion, startPosition;
@ -407,6 +409,7 @@ export function selectComposeSuggestion(position, token, suggestion) {
position: startPosition, position: startPosition,
token, token,
completion, completion,
path,
}); });
}; };
}; };

View File

@ -48,9 +48,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
let filtered = false; let filtered = false;
if (notification.type === 'mention') { if (notification.type === 'mention') {
const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible')));
const regex = regexFromFilters(filters); const regex = regexFromFilters(filters);
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
if (dropRegex && dropRegex.test(searchIndex)) {
return;
}
filtered = regex && regex.test(searchIndex); filtered = regex && regex.test(searchIndex);
} }

View File

@ -4,6 +4,7 @@ import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer'; import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
import { ensureComposeIsVisible } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
@ -131,14 +132,15 @@ export function fetchStatusFail(id, error, skipLoading) {
}; };
}; };
export function redraft(status) { export function redraft(status, raw_text) {
return { return {
type: REDRAFT, type: REDRAFT,
status, status,
raw_text,
}; };
}; };
export function deleteStatus(id, router, withRedraft = false) { export function deleteStatus(id, routerHistory, withRedraft = false) {
return (dispatch, getState) => { return (dispatch, getState) => {
let status = getState().getIn(['statuses', id]); let status = getState().getIn(['statuses', id]);
@ -148,17 +150,14 @@ export function deleteStatus(id, router, withRedraft = false) {
dispatch(deleteStatusRequest(id)); dispatch(deleteStatusRequest(id));
api(getState).delete(`/api/v1/statuses/${id}`).then(() => { api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
evictStatus(id); evictStatus(id);
dispatch(deleteStatusSuccess(id)); dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id)); dispatch(deleteFromTimelines(id));
if (withRedraft) { if (withRedraft) {
dispatch(redraft(status)); dispatch(redraft(status, response.data.text));
ensureComposeIsVisible(getState, routerHistory);
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new');
}
} }
}).catch(error => { }).catch(error => {
dispatch(deleteStatusFail(id, error)); dispatch(deleteStatusFail(id, error));

View File

@ -0,0 +1,229 @@
import React from 'react';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { isRtl } from '../rtl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import classNames from 'classnames';
import { List as ImmutableList } from 'immutable';
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
let word;
let left = str.slice(0, caretPosition).search(/\S+$/);
let right = str.slice(caretPosition).search(/\s/);
if (right < 0) {
word = str.slice(left);
} else {
word = str.slice(left, right + caretPosition);
}
if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) {
return [null, null];
}
word = word.trim().toLowerCase();
if (word.length > 0) {
return [left + 1, word];
} else {
return [null, null];
}
};
export default class AutosuggestInput extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
suggestions: ImmutablePropTypes.list,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
onSuggestionSelected: PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
autoFocus: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
searchTokens: PropTypes.arrayOf(PropTypes.string),
maxLength: PropTypes.number,
};
static defaultProps = {
autoFocus: true,
searchTokens: ImmutableList(['@', ':', '#']),
};
state = {
suggestionsHidden: true,
focused: false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
};
onChange = (e) => {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens);
if (token !== null && this.state.lastToken !== token) {
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
this.props.onSuggestionsFetchRequested(token);
} else if (token === null) {
this.setState({ lastToken: null });
this.props.onSuggestionsClearRequested();
}
this.props.onChange(e);
}
onKeyDown = (e) => {
const { suggestions, disabled } = this.props;
const { selectedSuggestion, suggestionsHidden } = this.state;
if (disabled) {
e.preventDefault();
return;
}
if (e.which === 229 || e.isComposing) {
// Ignore key events during text composition
// e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
return;
}
switch(e.key) {
case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector('.ui').parentElement.focus();
} else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
}
break;
case 'ArrowDown':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
}
break;
case 'ArrowUp':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
}
break;
case 'Enter':
case 'Tab':
// Select suggestion
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
}
break;
}
if (e.defaultPrevented || !this.props.onKeyDown) {
return;
}
this.props.onKeyDown(e);
}
onBlur = () => {
this.setState({ suggestionsHidden: true, focused: false });
}
onFocus = () => {
this.setState({ focused: true });
}
onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.input.focus();
}
componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false });
}
}
setInput = (c) => {
this.input = c;
}
renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state;
let inner, key;
if (typeof suggestion === 'object') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion[0] === '#') {
inner = suggestion;
key = suggestion;
} else {
inner = <AutosuggestAccountContainer id={suggestion} />;
key = suggestion;
}
return (
<div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
{inner}
</div>
);
}
render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' };
if (isRtl(value)) {
style.direction = 'rtl';
}
return (
<div className='autosuggest-input'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<input
type='text'
ref={this.setInput}
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
style={style}
aria-autocomplete='list'
id={id}
className={className}
maxLength={maxLength}
/>
</label>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map(this.renderSuggestion)}
</div>
</div>
);
}
}

View File

@ -55,7 +55,8 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
}; };
state = { state = {
suggestionsHidden: false, suggestionsHidden: true,
focused: false,
selectedSuggestion: 0, selectedSuggestion: 0,
lastToken: null, lastToken: null,
tokenStart: 0, tokenStart: 0,
@ -134,7 +135,14 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
onBlur = () => { onBlur = () => {
this.setState({ suggestionsHidden: true }); this.setState({ suggestionsHidden: true, focused: false });
}
onFocus = (e) => {
this.setState({ focused: true });
if (this.props.onFocus) {
this.props.onFocus(e);
}
} }
onSuggestionClick = (e) => { onSuggestionClick = (e) => {
@ -145,7 +153,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false }); this.setState({ suggestionsHidden: false });
} }
} }
@ -184,7 +192,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
render () { render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props; const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
const { suggestionsHidden } = this.state; const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' }; const style = { direction: 'ltr' };
@ -192,33 +200,39 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
style.direction = 'rtl'; style.direction = 'rtl';
} }
return ( return [
<div className='autosuggest-textarea'> <div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
<label> <div className='autosuggest-textarea'>
<span style={{ display: 'none' }}>{placeholder}</span> <label>
<span style={{ display: 'none' }}>{placeholder}</span>
<Textarea <Textarea
inputRef={this.setTextarea} inputRef={this.setTextarea}
className='autosuggest-textarea__textarea' className='autosuggest-textarea__textarea'
disabled={disabled} disabled={disabled}
placeholder={placeholder} placeholder={placeholder}
autoFocus={autoFocus} autoFocus={autoFocus}
value={value} value={value}
onChange={this.onChange} onChange={this.onChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
onBlur={this.onBlur} onFocus={this.onFocus}
onPaste={this.onPaste} onBlur={this.onBlur}
style={style} onPaste={this.onPaste}
aria-autocomplete='list' style={style}
/> aria-autocomplete='list'
</label> />
</label>
</div>
{children}
</div>,
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map(this.renderSuggestion)} {suggestions.map(this.renderSuggestion)}
</div> </div>
</div> </div>,
); ];
} }
} }

View File

@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
const formatNumber = num => num > 40 ? '40+' : num;
const IconWithBadge = ({ id, count, className }) => (
<i className='icon-with-badge'>
<Icon id={id} fixedWidth className={className} />
{count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
</i>
);
IconWithBadge.propTypes = {
id: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
className: PropTypes.string,
};
export default IconWithBadge;

View File

@ -157,7 +157,7 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'unknown') { if (attachment.get('type') === 'unknown') {
return ( return (
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}> <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
</a> </a>
</div> </div>
@ -244,6 +244,8 @@ class MediaGallery extends React.PureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
defaultWidth: PropTypes.number, defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func, cacheWidth: PropTypes.func,
visible: PropTypes.bool,
onToggleVisibility: PropTypes.func,
}; };
static defaultProps = { static defaultProps = {
@ -251,18 +253,24 @@ class MediaGallery extends React.PureComponent {
}; };
state = { state = {
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all', visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
width: this.props.defaultWidth, width: this.props.defaultWidth,
}; };
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (!is(nextProps.media, this.props.media)) { if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
this.setState({ visible: !nextProps.sensitive }); this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
} else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
this.setState({ visible: nextProps.visible });
} }
} }
handleOpen = () => { handleOpen = () => {
this.setState({ visible: !this.state.visible }); if (this.props.onToggleVisibility) {
this.props.onToggleVisibility();
} else {
this.setState({ visible: !this.state.visible });
}
} }
handleClick = (index) => { handleClick = (index) => {

View File

@ -16,7 +16,7 @@ import { MediaGallery, Video } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import PollContainer from 'mastodon/containers/poll_container'; import { displayMedia } from '../initial_state';
// We use the component (and not the container) since we do not want // We use the component (and not the container) since we do not want
// to use the progress bar to show download progress // to use the progress bar to show download progress
@ -39,6 +39,18 @@ export const textForScreenReader = (intl, status, rebloggedByText = false) => {
return values.join(', '); return values.join(', ');
}; };
export const defaultMediaVisibility = (status) => {
if (!status) {
return undefined;
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
status = status.get('reblog');
}
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
};
export default @injectIntl export default @injectIntl
class Status extends ImmutablePureComponent { class Status extends ImmutablePureComponent {
@ -85,6 +97,11 @@ class Status extends ImmutablePureComponent {
'hidden', 'hidden',
]; ];
state = {
showMedia: defaultMediaVisibility(this.props.status),
statusId: undefined,
};
// Track height changes we know about to compensate scrolling // Track height changes we know about to compensate scrolling
componentDidMount () { componentDidMount () {
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card'); this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
@ -98,11 +115,24 @@ class Status extends ImmutablePureComponent {
} }
} }
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
showMedia: defaultMediaVisibility(nextProps.status),
statusId: nextProps.status.get('id'),
};
} else {
return null;
}
}
// Compensate height changes // Compensate height changes
componentDidUpdate (prevProps, prevState, snapshot) { componentDidUpdate (prevProps, prevState, snapshot) {
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card'); const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
if (doShowCard && !this.didShowCard) { if (doShowCard && !this.didShowCard) {
this.didShowCard = true; this.didShowCard = true;
if (snapshot !== null && this.props.updateScrollBottom) { if (snapshot !== null && this.props.updateScrollBottom) {
if (this.node && this.node.offsetTop < snapshot.top) { if (this.node && this.node.offsetTop < snapshot.top) {
this.props.updateScrollBottom(snapshot.height - snapshot.top); this.props.updateScrollBottom(snapshot.height - snapshot.top);
@ -122,6 +152,10 @@ class Status extends ImmutablePureComponent {
} }
} }
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
}
handleClick = () => { handleClick = () => {
if (this.props.onClick) { if (this.props.onClick) {
this.props.onClick(); this.props.onClick();
@ -136,6 +170,22 @@ class Status extends ImmutablePureComponent {
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
} }
handleExpandClick = (e) => {
if (this.props.onClick) {
this.props.onClick();
return;
}
if (e.button === 0) {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
}
handleAccountClick = (e) => { handleAccountClick = (e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
const id = e.currentTarget.getAttribute('data-id'); const id = e.currentTarget.getAttribute('data-id');
@ -198,6 +248,10 @@ class Status extends ImmutablePureComponent {
this.props.onToggleHidden(this._properStatus()); this.props.onToggleHidden(this._properStatus());
} }
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
}
_properStatus () { _properStatus () {
const { status } = this.props; const { status } = this.props;
@ -271,9 +325,7 @@ class Status extends ImmutablePureComponent {
status = status.get('reblog'); status = status.get('reblog');
} }
if (status.get('poll')) { if (status.get('media_attachments').size > 0) {
media = <PollContainer pollId={status.get('poll')} />;
} else if (status.get('media_attachments').size > 0) {
if (this.props.muted) { if (this.props.muted) {
media = ( media = (
<AttachmentList <AttachmentList
@ -281,23 +333,25 @@ class Status extends ImmutablePureComponent {
media={status.get('media_attachments')} media={status.get('media_attachments')}
/> />
); );
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { } else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
const video = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
media = ( media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => ( {Component => (
<Component <Component
preview={video.get('preview_url')} preview={attachment.get('preview_url')}
blurhash={video.get('blurhash')} blurhash={attachment.get('blurhash')}
src={video.get('url')} src={attachment.get('url')}
alt={video.get('description')} alt={attachment.get('description')}
width={this.props.cachedMediaWidth} width={this.props.cachedMediaWidth}
height={110} height={110}
inline inline
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/> />
)} )}
</Bundle> </Bundle>
@ -313,6 +367,8 @@ class Status extends ImmutablePureComponent {
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.props.onOpenMedia}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth} defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/> />
)} )}
</Bundle> </Bundle>
@ -348,6 +404,7 @@ class Status extends ImmutablePureComponent {
moveUp: this.handleHotkeyMoveUp, moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown, moveDown: this.handleHotkeyMoveDown,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
}; };
return ( return (
@ -356,7 +413,7 @@ class Status extends ImmutablePureComponent {
{prepend} {prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}> <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
<div className='status__expand' onClick={this.handleClick} role='presentation' /> <div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
<div className='status__info'> <div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>

View File

@ -5,6 +5,7 @@ import { isRtl } from '../rtl';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Permalink from './permalink'; import Permalink from './permalink';
import classnames from 'classnames'; import classnames from 'classnames';
import PollContainer from 'mastodon/containers/poll_container';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top) const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
@ -106,8 +107,12 @@ export default class StatusContent extends React.PureComponent {
const [ startX, startY ] = this.startXY; const [ startX, startY ] = this.startXY;
const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)]; const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
if (e.target.localName === 'button' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) { let element = e.target;
return; while (element) {
if (element.localName === 'button' || element.localName === 'a' || element.localName === 'label') {
return;
}
element = element.parentNode;
} }
if (deltaX + deltaY < 5 && e.button === 0 && this.props.onClick) { if (deltaX + deltaY < 5 && e.button === 0 && this.props.onClick) {
@ -191,6 +196,8 @@ export default class StatusContent extends React.PureComponent {
{mentionsPlaceholder} {mentionsPlaceholder}
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} /> <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
</div> </div>
); );
} else if (this.props.onClick) { } else if (this.props.onClick) {
@ -212,9 +219,13 @@ export default class StatusContent extends React.PureComponent {
output.push(readMoreButton); output.push(readMoreButton);
} }
if (status.get('poll')) {
output.push(<PollContainer pollId={status.get('poll')} />);
}
return output; return output;
} else { } else {
return ( const output = [
<div <div
tabIndex='0' tabIndex='0'
ref={this.setRef} ref={this.setRef}
@ -222,8 +233,14 @@ export default class StatusContent extends React.PureComponent {
style={directionStyle} style={directionStyle}
dangerouslySetInnerHTML={content} dangerouslySetInnerHTML={content}
lang={status.get('language')} lang={status.get('language')}
/> />,
); ];
if (status.get('poll')) {
output.push(<PollContainer pollId={status.get('poll')} />);
}
return output;
} }
} }

View File

@ -69,18 +69,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onModalReblog (status) { onModalReblog (status) {
dispatch(reblog(status));
},
onReblog (status, e) {
if (status.get('reblogged')) { if (status.get('reblogged')) {
dispatch(unreblog(status)); dispatch(unreblog(status));
} else { } else {
if (e.shiftKey || !boostModal) { dispatch(reblog(status));
this.onModalReblog(status); }
} else { },
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
} onReblog (status, e) {
if (e.shiftKey || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
} }
}, },

View File

@ -46,7 +46,7 @@ class ActionBar extends React.PureComponent {
return ( return (
<div className='compose__action-bar'> <div className='compose__action-bar'>
<div className='compose__action-bar-dropdown'> <div className='compose__action-bar-dropdown'>
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' /> <DropdownMenuContainer items={menu} icon='chevron-down' size={16} direction='right' />
</div> </div>
</div> </div>
); );

View File

@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import AutosuggestInput from '../../../components/autosuggest_input';
import PollButtonContainer from '../containers/poll_button_container'; import PollButtonContainer from '../containers/poll_button_container';
import UploadButtonContainer from '../containers/upload_button_container'; import UploadButtonContainer from '../containers/upload_button_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
@ -61,6 +62,7 @@ class ComposeForm extends ImmutablePureComponent {
onPickEmoji: PropTypes.func.isRequired, onPickEmoji: PropTypes.func.isRequired,
showSearch: PropTypes.bool, showSearch: PropTypes.bool,
anyMedia: PropTypes.bool, anyMedia: PropTypes.bool,
singleColumn: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -104,13 +106,23 @@ class ComposeForm extends ImmutablePureComponent {
} }
onSuggestionSelected = (tokenStart, token, value) => { onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value); this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
}
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
} }
handleChangeSpoilerText = (e) => { handleChangeSpoilerText = (e) => {
this.props.onChangeSpoilerText(e.target.value); this.props.onChangeSpoilerText(e.target.value);
} }
handleFocus = () => {
if (this.composeForm && !this.props.singleColumn) {
this.composeForm.scrollIntoView();
}
}
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
// This statement does several things: // This statement does several things:
// - If we're beginning a reply, and, // - If we're beginning a reply, and,
@ -137,7 +149,7 @@ class ComposeForm extends ImmutablePureComponent {
this.autosuggestTextarea.textarea.focus(); this.autosuggestTextarea.textarea.focus();
} else if (this.props.spoiler !== prevProps.spoiler) { } else if (this.props.spoiler !== prevProps.spoiler) {
if (this.props.spoiler) { if (this.props.spoiler) {
this.spoilerText.focus(); this.spoilerText.input.focus();
} else { } else {
this.autosuggestTextarea.textarea.focus(); this.autosuggestTextarea.textarea.focus();
} }
@ -152,6 +164,10 @@ class ComposeForm extends ImmutablePureComponent {
this.spoilerText = c; this.spoilerText = c;
} }
setRef = c => {
this.composeForm = c;
};
handleEmojiPick = (data) => { handleEmojiPick = (data) => {
const { text } = this.props; const { text } = this.props;
const position = this.autosuggestTextarea.textarea.selectionStart; const position = this.autosuggestTextarea.textarea.selectionStart;
@ -174,41 +190,50 @@ class ComposeForm extends ImmutablePureComponent {
} }
return ( return (
<div className='compose-form'> <div className='compose-form' ref={this.setRef}>
<WarningContainer /> <WarningContainer />
<ReplyIndicatorContainer /> <ReplyIndicatorContainer />
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}> <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
<label> <AutosuggestInput
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span> placeholder={intl.formatMessage(messages.spoiler_placeholder)}
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoilerText} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} /> value={this.props.spoilerText}
</label> onChange={this.handleChangeSpoilerText}
</div>
<div className='compose-form__autosuggest-wrapper'>
<AutosuggestTextarea
ref={this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
disabled={!this.props.spoiler}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected} onSuggestionSelected={this.onSpoilerSuggestionSelected}
onPaste={onPaste} searchTokens={[':']}
autoFocus={!showSearch && !isMobile(window.innerWidth)} id='cw-spoiler-input'
className='spoiler-input__input'
/> />
</div>
<AutosuggestTextarea
ref={this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={!showSearch && !isMobile(window.innerWidth)}
>
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
</div> <div className='compose-form__modifiers'>
<UploadFormContainer />
<div className='compose-form__modifiers'> <PollFormContainer />
<UploadFormContainer /> </div>
<PollFormContainer /> </AutosuggestTextarea>
</div>
<div className='compose-form__buttons-wrapper'> <div className='compose-form__buttons-wrapper'>
<div className='compose-form__buttons'> <div className='compose-form__buttons'>

View File

@ -20,7 +20,7 @@ export default class NavigationBar extends ImmutablePureComponent {
<div className='navigation-bar'> <div className='navigation-bar'>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
<Avatar account={this.props.account} size={40} /> <Avatar account={this.props.account} size={48} />
</Permalink> </Permalink>
<div className='navigation-bar__profile'> <div className='navigation-bar__profile'>

View File

@ -5,6 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from 'mastodon/components/icon_button'; import IconButton from 'mastodon/components/icon_button';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import AutosuggestInput from 'mastodon/components/autosuggest_input';
import classNames from 'classnames'; import classNames from 'classnames';
const messages = defineMessages({ const messages = defineMessages({
@ -27,6 +28,10 @@ class Option extends React.PureComponent {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired,
onToggleMultiple: PropTypes.func.isRequired, onToggleMultiple: PropTypes.func.isRequired,
suggestions: ImmutablePropTypes.list,
onClearSuggestions: PropTypes.func.isRequired,
onFetchSuggestions: PropTypes.func.isRequired,
onSuggestionSelected: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -38,12 +43,25 @@ class Option extends React.PureComponent {
this.props.onRemove(this.props.index); this.props.onRemove(this.props.index);
}; };
handleToggleMultiple = e => { handleToggleMultiple = e => {
this.props.onToggleMultiple(); this.props.onToggleMultiple();
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}; };
onSuggestionsClearRequested = () => {
this.props.onClearSuggestions();
}
onSuggestionsFetchRequested = (token) => {
this.props.onFetchSuggestions(token);
}
onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
}
render () { render () {
const { isPollMultiple, title, index, intl } = this.props; const { isPollMultiple, title, index, intl } = this.props;
@ -57,12 +75,16 @@ class Option extends React.PureComponent {
tabIndex='0' tabIndex='0'
/> />
<input <AutosuggestInput
type='text'
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })} placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
maxLength={25} maxLength={25}
value={title} value={title}
onChange={this.handleOptionTitleChange} onChange={this.handleOptionTitleChange}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
searchTokens={[':']}
/> />
</label> </label>
@ -87,6 +109,10 @@ class PollForm extends ImmutablePureComponent {
onAddOption: PropTypes.func.isRequired, onAddOption: PropTypes.func.isRequired,
onRemoveOption: PropTypes.func.isRequired, onRemoveOption: PropTypes.func.isRequired,
onChangeSettings: PropTypes.func.isRequired, onChangeSettings: PropTypes.func.isRequired,
suggestions: ImmutablePropTypes.list,
onClearSuggestions: PropTypes.func.isRequired,
onFetchSuggestions: PropTypes.func.isRequired,
onSuggestionSelected: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -103,7 +129,7 @@ class PollForm extends ImmutablePureComponent {
}; };
render () { render () {
const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props; const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
if (!options) { if (!options) {
return null; return null;
@ -112,7 +138,7 @@ class PollForm extends ImmutablePureComponent {
return ( return (
<div className='compose-form__poll-wrapper'> <div className='compose-form__poll-wrapper'>
<ul> <ul>
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} />)} {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} {...other} />)}
</ul> </ul>
<div className='poll__footer'> <div className='poll__footer'>

View File

@ -129,7 +129,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
// It should not be transformed when mounting because the resulting // It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by // size will be used to determine the coordinate of the menu by
// react-overlays // react-overlays
<div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}> <div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, zIndex: 2 }} role='listbox' ref={this.setRef}>
{items.map(item => ( {items.map(item => (
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}> <div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
<div className='privacy-dropdown__option__icon'> <div className='privacy-dropdown__option__icon'>

View File

@ -7,6 +7,7 @@ import DisplayName from '../../../components/display_name';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { isRtl } from '../../../rtl'; import { isRtl } from '../../../rtl';
import AttachmentList from 'mastodon/components/attachment_list';
const messages = defineMessages({ const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
@ -60,6 +61,13 @@ class ReplyIndicator extends ImmutablePureComponent {
</div> </div>
<div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} /> <div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
{status.get('media_attachments').size > 0 && (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
)}
</div> </div>
); );
} }

View File

@ -21,7 +21,7 @@ class SearchPopout extends React.PureComponent {
const { style } = this.props; const { style } = this.props;
const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />; const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />;
return ( return (
<div style={{ ...style, position: 'absolute', width: 285 }}> <div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}>
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => ( {({ opacity, scaleX, scaleY }) => (
<div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
@ -47,6 +47,10 @@ class SearchPopout extends React.PureComponent {
export default @injectIntl export default @injectIntl
class Search extends React.PureComponent { class Search extends React.PureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = { static propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
submitted: PropTypes.bool, submitted: PropTypes.bool,
@ -54,6 +58,7 @@ class Search extends React.PureComponent {
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired, onClear: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired, onShow: PropTypes.func.isRequired,
openInRoute: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -76,7 +81,12 @@ class Search extends React.PureComponent {
handleKeyUp = (e) => { handleKeyUp = (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
this.props.onSubmit(); this.props.onSubmit();
if (this.props.openInRoute) {
this.context.router.history.push('/search');
}
} else if (e.key === 'Escape') { } else if (e.key === 'Escape') {
document.querySelector('.ui').parentElement.focus(); document.querySelector('.ui').parentElement.focus();
} }

View File

@ -7,9 +7,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
const messages = defineMessages({ const messages = defineMessages({
upload: { id: 'upload_button.label', defaultMessage: 'Add media (JPEG, PNG, GIF, WebM, MP4, MOV)' }, upload: { id: 'upload_button.label', defaultMessage: 'Add media ({formats})' },
}); });
const SUPPORTED_FORMATS = 'JPEG, PNG, GIF, WebM, MP4, MOV, OGG, WAV, MP3, FLAC';
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
@ -60,9 +62,9 @@ class UploadButton extends ImmutablePureComponent {
return ( return (
<div className='compose-form__upload-button'> <div className='compose-form__upload-button'>
<IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> <IconButton icon='paperclip' title={intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
<label> <label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span> <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })}</span>
<input <input
key={resetFileKey} key={resetFileKey}
ref={this.setRef} ref={this.setRef}

View File

@ -46,8 +46,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(fetchComposeSuggestions(token)); dispatch(fetchComposeSuggestions(token));
}, },
onSuggestionSelected (position, token, suggestion) { onSuggestionSelected (position, token, suggestion, path) {
dispatch(selectComposeSuggestion(position, token, suggestion)); dispatch(selectComposeSuggestion(position, token, suggestion, path));
}, },
onChangeSpoilerText (checked) { onChangeSpoilerText (checked) {

View File

@ -1,8 +1,14 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PollForm from '../components/poll_form'; import PollForm from '../components/poll_form';
import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose'; import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose';
import {
clearComposeSuggestions,
fetchComposeSuggestions,
selectComposeSuggestion,
} from '../../../actions/compose';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
suggestions: state.getIn(['compose', 'suggestions']),
options: state.getIn(['compose', 'poll', 'options']), options: state.getIn(['compose', 'poll', 'options']),
expiresIn: state.getIn(['compose', 'poll', 'expires_in']), expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
isMultiple: state.getIn(['compose', 'poll', 'multiple']), isMultiple: state.getIn(['compose', 'poll', 'multiple']),
@ -24,6 +30,19 @@ const mapDispatchToProps = dispatch => ({
onChangeSettings(expiresIn, isMultiple) { onChangeSettings(expiresIn, isMultiple) {
dispatch(changePollSettings(expiresIn, isMultiple)); dispatch(changePollSettings(expiresIn, isMultiple));
}, },
onClearSuggestions () {
dispatch(clearComposeSuggestions());
},
onFetchSuggestions (token) {
dispatch(fetchComposeSuggestions(token));
},
onSuggestionSelected (position, token, accountId, path) {
dispatch(selectComposeSuggestion(position, token, accountId, path));
},
}); });
export default connect(mapStateToProps, mapDispatchToProps)(PollForm); export default connect(mapStateToProps, mapDispatchToProps)(PollForm);

View File

@ -3,7 +3,7 @@ import UploadButton from '../components/upload_button';
import { uploadCompose } from '../../../actions/compose'; import { uploadCompose } from '../../../actions/compose';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')), disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
unavailable: state.getIn(['compose', 'poll']) !== null, unavailable: state.getIn(['compose', 'poll']) !== null,
resetFileKey: state.getIn(['compose', 'resetFileKey']), resetFileKey: state.getIn(['compose', 'resetFileKey']),
}); });

View File

@ -106,12 +106,12 @@ class Compose extends React.PureComponent {
<div className='drawer__pager'> <div className='drawer__pager'>
{!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}> {!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}>
<NavigationContainer onClose={this.onBlur} /> <NavigationContainer onClose={this.onBlur} />
<ComposeFormContainer /> <ComposeFormContainer />
{multiColumn && (
<div className='drawer__inner__mastodon'> <div className='drawer__inner__mastodon'>
<img alt='' draggable='false' src={mascot || elephantUIPlane} /> <img alt='' draggable='false' src={mascot || elephantUIPlane} />
</div> </div>
)}
</div>} </div>}
<Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}> <Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>

View File

@ -56,7 +56,7 @@ class FollowRequests extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />; const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
return ( return (
<Column icon='users' heading={intl.formatMessage(messages.heading)}> <Column icon='user-plus' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim /> <ColumnBackButtonSlim />
<ScrollableList <ScrollableList
scrollKey='follow_requests' scrollKey='follow_requests'

View File

@ -7,12 +7,12 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, invitesEnabled, version, profile_directory, repository, source_url } from '../../initial_state'; import { me, profile_directory } from '../../initial_state';
import { fetchFollowRequests } from '../../actions/accounts'; import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { Link } from 'react-router-dom';
import NavigationBar from '../compose/components/navigation_bar'; import NavigationBar from '../compose/components/navigation_bar';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import LinkFooter from 'mastodon/features/ui/components/link_footer';
const messages = defineMessages({ const messages = defineMessages({
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
@ -55,10 +55,16 @@ const badgeDisplay = (number, limit) => {
} }
}; };
const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
export default @connect(mapStateToProps, mapDispatchToProps) export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl @injectIntl
class GettingStarted extends ImmutablePureComponent { class GettingStarted extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
myAccount: ImmutablePropTypes.map.isRequired, myAccount: ImmutablePropTypes.map.isRequired,
@ -70,7 +76,12 @@ class GettingStarted extends ImmutablePureComponent {
}; };
componentDidMount () { componentDidMount () {
const { myAccount, fetchFollowRequests } = this.props; const { myAccount, fetchFollowRequests, multiColumn } = this.props;
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
this.context.router.history.replace('/timelines/home');
return;
}
if (myAccount.get('locked')) { if (myAccount.get('locked')) {
fetchFollowRequests(); fetchFollowRequests();
@ -123,7 +134,7 @@ class GettingStarted extends ImmutablePureComponent {
height += 48*3; height += 48*3;
if (myAccount.get('locked')) { if (myAccount.get('locked')) {
navItems.push(<ColumnLink key={i++} icon='users' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />); navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
height += 48; height += 48;
} }
@ -155,27 +166,7 @@ class GettingStarted extends ImmutablePureComponent {
{!multiColumn && <div className='flex-spacer' />} {!multiColumn && <div className='flex-spacer' />}
<div className='getting-started__footer'> <LinkFooter withHotkeys={multiColumn} />
<ul>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
{multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
<li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
</ul>
<p>
<FormattedMessage
id='getting_started.open_source_notice'
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
/>
</p>
</div>
</div> </div>
</Column> </Column>
); );

View File

@ -60,6 +60,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
<td><kbd>x</kbd></td> <td><kbd>x</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td> <td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
</tr> </tr>
<tr>
<td><kbd>h</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.toggle_sensitivity' defaultMessage='to show/hide media' /></td>
</tr>
<tr> <tr>
<td><kbd>up</kbd>, <kbd>k</kbd></td> <td><kbd>up</kbd>, <kbd>k</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td> <td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>

View File

@ -75,6 +75,23 @@ class ListTimeline extends React.PureComponent {
this.disconnect = dispatch(connectListStream(id)); this.disconnect = dispatch(connectListStream(id));
} }
componentWillReceiveProps (nextProps) {
const { dispatch } = this.props;
const { id } = nextProps.params;
if (id !== this.props.params.id) {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
dispatch(fetchList(id));
dispatch(expandListTimeline(id));
this.disconnect = dispatch(connectListStream(id));
}
}
componentWillUnmount () { componentWillUnmount () {
if (this.disconnect) { if (this.disconnect) {
this.disconnect(); this.disconnect();
@ -158,8 +175,6 @@ class ListTimeline extends React.PureComponent {
<Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' /> <Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
</button> </button>
</div> </div>
<hr />
</ColumnHeader> </ColumnHeader>
<StatusListContainer <StatusListContainer

View File

@ -65,11 +65,11 @@ class Lists extends ImmutablePureComponent {
<NewListForm /> <NewListForm />
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
<ScrollableList <ScrollableList
scrollKey='lists' scrollKey='lists'
shouldUpdateScroll={shouldUpdateScroll} shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
> >
{lists.map(list => {lists.map(list =>
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />

View File

@ -0,0 +1,17 @@
import React from 'react';
import SearchContainer from 'mastodon/features/compose/containers/search_container';
import SearchResultsContainer from 'mastodon/features/compose/containers/search_results_container';
const Search = () => (
<div className='column search-page'>
<SearchContainer />
<div className='drawer__pager'>
<div className='drawer__inner darker'>
<SearchResultsContainer />
</div>
</div>
</div>
);
export default Search;

View File

@ -13,7 +13,6 @@ import Video from '../../video';
import scheduleIdleTask from '../../ui/util/schedule_idle_task'; import scheduleIdleTask from '../../ui/util/schedule_idle_task';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import PollContainer from 'mastodon/containers/poll_container';
const messages = defineMessages({ const messages = defineMessages({
local_only: { id: 'status.local_only', defaultMessage: 'This post is only visible by other users of your instance' }, local_only: { id: 'status.local_only', defaultMessage: 'This post is only visible by other users of your instance' },
@ -35,6 +34,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
onHeightChange: PropTypes.func, onHeightChange: PropTypes.func,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
compact: PropTypes.bool, compact: PropTypes.bool,
showMedia: PropTypes.bool,
onToggleMediaVisibility: PropTypes.func,
}; };
state = { state = {
@ -112,23 +113,23 @@ export default class DetailedStatus extends ImmutablePureComponent {
outerStyle.height = `${this.state.height}px`; outerStyle.height = `${this.state.height}px`;
} }
if (status.get('poll')) { if (status.get('media_attachments').size > 0) {
media = <PollContainer pollId={status.get('poll')} />; if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
} else if (status.get('media_attachments').size > 0) { const attachment = status.getIn(['media_attachments', 0]);
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const video = status.getIn(['media_attachments', 0]);
media = ( media = (
<Video <Video
preview={video.get('preview_url')} preview={attachment.get('preview_url')}
blurhash={video.get('blurhash')} blurhash={attachment.get('blurhash')}
src={video.get('url')} src={attachment.get('url')}
alt={video.get('description')} alt={attachment.get('description')}
width={300} width={300}
height={150} height={150}
inline inline
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
/> />
); );
} else { } else {
@ -139,6 +140,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
media={status.get('media_attachments')} media={status.get('media_attachments')}
height={300} height={300}
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.props.onOpenMedia}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
/> />
); );
} }

View File

@ -43,7 +43,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state'; import { boostModal, deleteModal } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import { textForScreenReader } from '../../components/status'; import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
@ -131,6 +131,8 @@ class Status extends ImmutablePureComponent {
state = { state = {
fullscreen: false, fullscreen: false,
showMedia: defaultMediaVisibility(this.props.status),
loadedStatusId: undefined,
}; };
componentWillMount () { componentWillMount () {
@ -146,6 +148,14 @@ class Status extends ImmutablePureComponent {
this._scrolledIntoView = false; this._scrolledIntoView = false;
this.props.dispatch(fetchStatus(nextProps.params.statusId)); this.props.dispatch(fetchStatus(nextProps.params.statusId));
} }
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') });
}
}
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
} }
handleFavouriteClick = (status) => { handleFavouriteClick = (status) => {
@ -312,6 +322,10 @@ class Status extends ImmutablePureComponent {
this.handleToggleHidden(this.props.status); this.handleToggleHidden(this.props.status);
} }
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
}
handleMoveUp = id => { handleMoveUp = id => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
@ -432,6 +446,7 @@ class Status extends ImmutablePureComponent {
mention: this.handleHotkeyMention, mention: this.handleHotkeyMention,
openProfile: this.handleHotkeyOpenProfile, openProfile: this.handleHotkeyOpenProfile,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
}; };
return ( return (
@ -455,6 +470,8 @@ class Status extends ImmutablePureComponent {
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
onToggleHidden={this.handleToggleHidden} onToggleHidden={this.handleToggleHidden}
domain={domain} domain={domain}
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
/> />
<ActionBar <ActionBar

View File

@ -9,8 +9,10 @@ import RelativeTimestamp from '../../../components/relative_timestamp';
import DisplayName from '../../../components/display_name'; import DisplayName from '../../../components/display_name';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import AttachmentList from 'mastodon/components/attachment_list';
const messages = defineMessages({ const messages = defineMessages({
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
}); });
@ -51,6 +53,7 @@ class BoostModal extends ImmutablePureComponent {
render () { render () {
const { status, intl } = this.props; const { status, intl } = this.props;
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
return ( return (
<div className='modal-root__modal boost-modal'> <div className='modal-root__modal boost-modal'>
@ -71,12 +74,19 @@ class BoostModal extends ImmutablePureComponent {
</div> </div>
<StatusContent status={status} /> <StatusContent status={status} />
{status.get('media_attachments').size > 0 && (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
)}
</div> </div>
</div> </div>
<div className='boost-modal__action-bar'> <div className='boost-modal__action-bar'>
<div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} /></div> <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} /></div>
<Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} ref={this.setRef} /> <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
</div> </div>
</div> </div>
); );

View File

@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import ReactSwipeableViews from 'react-swipeable-views'; import ReactSwipeableViews from 'react-swipeable-views';
import { links, getIndex, getLink } from './tabs_bar'; import TabsBar, { links, getIndex, getLink } from './tabs_bar';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import BundleContainer from '../containers/bundle_container'; import BundleContainer from '../containers/bundle_container';
@ -14,6 +14,8 @@ import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error'; import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components'; import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import ComposePanel from './compose_panel';
import NavigationPanel from './navigation_panel';
import detectPassiveEvents from 'detect-passive-events'; import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll'; import { scrollRight } from '../../../scroll';
@ -139,7 +141,7 @@ class ColumnsArea extends ImmutablePureComponent {
<ColumnLoading title={title} icon={icon} />; <ColumnLoading title={title} icon={icon} />;
return ( return (
<div className='columns-area' key={index}> <div className='columns-area columns-area--mobile' key={index}>
{view} {view}
</div> </div>
); );
@ -163,17 +165,36 @@ class ColumnsArea extends ImmutablePureComponent {
if (singleColumn) { if (singleColumn) {
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
return columnIndex !== -1 ? [ const content = columnIndex !== -1 ? (
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> <ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
{links.map(this.renderView)} {links.map(this.renderView)}
</ReactSwipeableViews>, </ReactSwipeableViews>
) : (
<div key='content' className='columns-area columns-area--mobile'>{children}</div>
);
floatingActionButton, return (
] : [ <div className='columns-area__panels'>
<div className='columns-area'>{children}</div>, <div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
<ComposePanel />
</div>
</div>
floatingActionButton, <div className='columns-area__panels__main'>
]; <TabsBar key='tabs' />
{content}
</div>
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
<div className='columns-area__panels__pane__inner'>
<NavigationPanel />
</div>
</div>
{floatingActionButton}
</div>
);
} }
return ( return (

View File

@ -0,0 +1,16 @@
import React from 'react';
import SearchContainer from 'mastodon/features/compose/containers/search_container';
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
import LinkFooter from './link_footer';
const ComposePanel = () => (
<div className='compose-panel'>
<SearchContainer openInRoute />
<NavigationContainer />
<ComposeFormContainer singleColumn />
<LinkFooter withHotkeys />
</div>
);
export default ComposePanel;

View File

@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { connect } from 'react-redux';
import { NavLink, withRouter } from 'react-router-dom';
import IconWithBadge from 'mastodon/components/icon_with_badge';
import { me } from 'mastodon/initial_state';
import { List as ImmutableList } from 'immutable';
import { FormattedMessage } from 'react-intl';
const mapStateToProps = state => ({
locked: state.getIn(['accounts', me, 'locked']),
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
});
export default @withRouter
@connect(mapStateToProps)
class FollowRequestsNavLink extends React.Component {
static propTypes = {
dispatch: PropTypes.func.isRequired,
locked: PropTypes.bool,
count: PropTypes.number.isRequired,
};
componentDidMount () {
const { dispatch, locked } = this.props;
if (locked) {
dispatch(fetchFollowRequests());
}
}
render () {
const { locked, count } = this.props;
if (!locked || count === 0) {
return null;
}
return <NavLink className='column-link column-link--transparent' to='/follow_requests'><IconWithBadge className='column-link__icon' id='user-plus' count={count} /><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></NavLink>;
}
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state';
const LinkFooter = ({ withHotkeys }) => (
<div className='getting-started__footer'>
<ul>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
{withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
<li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
</ul>
<p>
<FormattedMessage
id='getting_started.open_source_notice'
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
/>
</p>
</div>
);
LinkFooter.propTypes = {
withHotkeys: PropTypes.bool,
};
export default LinkFooter;

View File

@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { fetchLists } from 'mastodon/actions/lists';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { NavLink, withRouter } from 'react-router-dom';
import Icon from 'mastodon/components/icon';
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'))).take(4);
});
const mapStateToProps = state => ({
lists: getOrderedLists(state),
});
export default @withRouter
@connect(mapStateToProps)
class ListPanel extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
lists: ImmutablePropTypes.list,
};
componentDidMount () {
const { dispatch } = this.props;
dispatch(fetchLists());
}
render () {
const { lists } = this.props;
if (!lists || lists.isEmpty()) {
return null;
}
return (
<div>
<hr />
{lists.map(list => (
<NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/timelines/list/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink>
))}
</div>
);
}
}

View File

@ -0,0 +1,31 @@
import React from 'react';
import { NavLink, withRouter } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import Icon from 'mastodon/components/icon';
import { profile_directory } from 'mastodon/initial_state';
import NotificationsCounterIcon from './notifications_counter_icon';
import FollowRequestsNavLink from './follow_requests_nav_link';
import ListPanel from './list_panel';
const NavigationPanel = () => (
<div className='navigation-panel'>
<NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
<FollowRequestsNavLink />
<NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
<NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
<ListPanel />
<hr />
<a className='column-link column-link--transparent' href='/settings/preferences'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
<a className='column-link column-link--transparent' href='/relationships'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
{!!profile_directory && <a className='column-link column-link--transparent' href='/explore'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></a>}
</div>
);
export default withRouter(NavigationPanel);

View File

@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import IconWithBadge from 'mastodon/components/icon_with_badge';
const mapStateToProps = state => ({
count: state.getIn(['notifications', 'unread']),
id: 'bell',
});
export default connect(mapStateToProps)(IconWithBadge);

View File

@ -5,16 +5,15 @@ import { FormattedMessage, injectIntl } from 'react-intl';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { isUserTouching } from '../../../is_mobile'; import { isUserTouching } from '../../../is_mobile';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import NotificationsCounterIcon from './notifications_counter_icon';
export const links = [ export const links = [
<NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
<NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><Icon id='bell' fixedWidth /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
<NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
<NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
<NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
<NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>, <NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
<NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
]; ];
export function getIndex (path) { export function getIndex (path) {

View File

@ -7,7 +7,6 @@ import { Redirect, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import NotificationsContainer from './containers/notifications_container'; import NotificationsContainer from './containers/notifications_container';
import LoadingBarContainer from './containers/loading_bar_container'; import LoadingBarContainer from './containers/loading_bar_container';
import TabsBar from './components/tabs_bar';
import ModalContainer from './containers/modal_container'; import ModalContainer from './containers/modal_container';
import { isMobile } from '../../is_mobile'; import { isMobile } from '../../is_mobile';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
@ -45,8 +44,9 @@ import {
Mutes, Mutes,
PinnedStatuses, PinnedStatuses,
Lists, Lists,
Search,
} from './util/async-components'; } from './util/async-components';
import { me } from '../../initial_state'; import { me, forceSingleColumn } from '../../initial_state';
import { previewState as previewMediaState } from './components/media_modal'; import { previewState as previewMediaState } from './components/media_modal';
import { previewState as previewVideoState } from './components/video_modal'; import { previewState as previewVideoState } from './components/video_modal';
@ -93,6 +93,7 @@ const keyMap = {
goToMuted: 'g m', goToMuted: 'g m',
goToRequests: 'g r', goToRequests: 'g r',
toggleHidden: 'x', toggleHidden: 'x',
toggleSensitive: 'h',
}; };
class SwitchingColumnsArea extends React.PureComponent { class SwitchingColumnsArea extends React.PureComponent {
@ -141,10 +142,11 @@ class SwitchingColumnsArea extends React.PureComponent {
render () { render () {
const { children } = this.props; const { children } = this.props;
const { mobile } = this.state; const { mobile } = this.state;
const redirect = mobile ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />; const singleColumn = forceSingleColumn || mobile;
const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
return ( return (
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}> <ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
<WrappedSwitch> <WrappedSwitch>
{redirect} {redirect}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
@ -160,7 +162,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/search' component={Compose} content={children} componentParams={{ isSearchPage: true }} /> <WrappedRoute path='/search' component={Search} content={children} />
<WrappedRoute path='/statuses/new' component={Compose} content={children} /> <WrappedRoute path='/statuses/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
@ -479,8 +481,6 @@ class UI extends React.PureComponent {
return ( return (
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused> <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}> <div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
<TabsBar />
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}> <SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}>
{children} {children}
</SwitchingColumnsArea> </SwitchingColumnsArea>

View File

@ -129,3 +129,7 @@ export function ListEditor () {
export function ListAdder () { export function ListAdder () {
return import(/*webpackChunkName: "features/list_adder" */'../../list_adder'); return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
} }
export function Search () {
return import(/*webpackChunkName: "features/search" */'../../search');
}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { fromJS } from 'immutable'; import { fromJS, is } from 'immutable';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
@ -102,6 +102,8 @@ class Video extends React.PureComponent {
detailed: PropTypes.bool, detailed: PropTypes.bool,
inline: PropTypes.bool, inline: PropTypes.bool,
cacheWidth: PropTypes.func, cacheWidth: PropTypes.func,
visible: PropTypes.bool,
onToggleVisibility: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
blurhash: PropTypes.string, blurhash: PropTypes.string,
link: PropTypes.node, link: PropTypes.node,
@ -117,7 +119,7 @@ class Video extends React.PureComponent {
fullscreen: false, fullscreen: false,
hovered: false, hovered: false,
muted: false, muted: false,
revealed: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all', revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
}; };
// hard coded in components.scss // hard coded in components.scss
@ -280,7 +282,16 @@ class Video extends React.PureComponent {
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true); document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
} }
componentDidUpdate (prevProps) { componentWillReceiveProps (nextProps) {
if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
this.setState({ revealed: nextProps.visible });
}
}
componentDidUpdate (prevProps, prevState) {
if (prevState.revealed && !this.state.revealed && this.video) {
this.video.pause();
}
if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) { if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) {
this._decode(); this._decode();
} }
@ -316,11 +327,11 @@ class Video extends React.PureComponent {
} }
toggleReveal = () => { toggleReveal = () => {
if (this.state.revealed) { if (this.props.onToggleVisibility) {
this.video.pause(); this.props.onToggleVisibility();
} else {
this.setState({ revealed: !this.state.revealed });
} }
this.setState({ revealed: !this.state.revealed });
} }
handleLoadedData = () => { handleLoadedData = () => {

View File

@ -19,5 +19,6 @@ export const version = getMeta('version');
export const mascot = getMeta('mascot'); export const mascot = getMeta('mascot');
export const profile_directory = getMeta('profile_directory'); export const profile_directory = getMeta('profile_directory');
export const isStaff = getMeta('is_staff'); export const isStaff = getMeta('is_staff');
export const forceSingleColumn = !getMeta('advanced_layout');
export default initialState; export default initialState;

View File

@ -1,8 +1,8 @@
{ {
"account.add_or_remove_from_list": "أضيف/ي أو أحذف/ي من القائمة", "account.add_or_remove_from_list": "أضفه أو أزله من القائمة",
"account.badges.bot": "روبوت", "account.badges.bot": "روبوت",
"account.block": "حظر @{name}", "account.block": "حظر @{name}",
"account.block_domain": "إخفاء كل شيئ قادم من إسم النطاق {domain}", "account.block_domain": "إخفاء كل شيئ قادم من اسم النطاق {domain}",
"account.blocked": "محظور", "account.blocked": "محظور",
"account.direct": "رسالة خاصة إلى @{name}", "account.direct": "رسالة خاصة إلى @{name}",
"account.domain_blocked": "النطاق مخفي", "account.domain_blocked": "النطاق مخفي",
@ -19,7 +19,7 @@
"account.locked_info": "تم تأمين خصوصية هذا الحساب عبر قفل. صاحب الحساب يُراجِع يدويا طلبات المتابَعة و الاشتراك بحسابه.", "account.locked_info": "تم تأمين خصوصية هذا الحساب عبر قفل. صاحب الحساب يُراجِع يدويا طلبات المتابَعة و الاشتراك بحسابه.",
"account.media": "وسائط", "account.media": "وسائط",
"account.mention": "أُذكُر/ي @{name}", "account.mention": "أُذكُر/ي @{name}",
"account.moved_to": "{name} إنتقل إلى :", "account.moved_to": "{name} انتقل إلى:",
"account.mute": "كتم @{name}", "account.mute": "كتم @{name}",
"account.mute_notifications": "كتم الإخطارات من @{name}", "account.mute_notifications": "كتم الإخطارات من @{name}",
"account.muted": "مكتوم", "account.muted": "مكتوم",
@ -36,7 +36,7 @@
"account.unmute": "إلغاء الكتم عن @{name}", "account.unmute": "إلغاء الكتم عن @{name}",
"account.unmute_notifications": "إلغاء كتم إخطارات @{name}", "account.unmute_notifications": "إلغاء كتم إخطارات @{name}",
"alert.unexpected.message": "لقد طرأ هناك خطأ غير متوقّع.", "alert.unexpected.message": "لقد طرأ هناك خطأ غير متوقّع.",
"alert.unexpected.title": "المعذرة !", "alert.unexpected.title": "المعذرة!",
"boost_modal.combo": "يمكنك/ي ضغط {combo} لتخطّي هذه في المرّة القادمة", "boost_modal.combo": "يمكنك/ي ضغط {combo} لتخطّي هذه في المرّة القادمة",
"bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.", "bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
"bundle_column_error.retry": "إعادة المحاولة", "bundle_column_error.retry": "إعادة المحاولة",
@ -45,7 +45,7 @@
"bundle_modal_error.message": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.", "bundle_modal_error.message": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
"bundle_modal_error.retry": "إعادة المحاولة", "bundle_modal_error.retry": "إعادة المحاولة",
"column.blocks": "الحسابات المحجوبة", "column.blocks": "الحسابات المحجوبة",
"column.community": "التَسَلْسُل الزَمني المحلي", "column.community": "الخيط العام المحلي",
"column.direct": "الرسائل المباشرة", "column.direct": "الرسائل المباشرة",
"column.domain_blocks": "النطاقات المخفية", "column.domain_blocks": "النطاقات المخفية",
"column.favourites": "المفضلة", "column.favourites": "المفضلة",
@ -66,7 +66,7 @@
"column_subheading.settings": "الإعدادات", "column_subheading.settings": "الإعدادات",
"community.column_settings.media_only": "الوسائط فقط", "community.column_settings.media_only": "الوسائط فقط",
"compose_form.direct_message_warning": "لن يَظهر هذا التبويق إلا للمستخدمين المذكورين.", "compose_form.direct_message_warning": "لن يَظهر هذا التبويق إلا للمستخدمين المذكورين.",
"compose_form.direct_message_warning_learn_more": "إقرأ المزيد", "compose_form.direct_message_warning_learn_more": "اقرأ المزيد",
"compose_form.hashtag_warning": "هذا التبويق لن يُدرَج تحت أي وسم كان بما أنه غير مُدرَج. لا يُسمح بالبحث إلّا عن التبويقات العمومية عن طريق الوسوم.", "compose_form.hashtag_warning": "هذا التبويق لن يُدرَج تحت أي وسم كان بما أنه غير مُدرَج. لا يُسمح بالبحث إلّا عن التبويقات العمومية عن طريق الوسوم.",
"compose_form.lock_disclaimer": "حسابك ليس {locked}. يمكن لأي شخص متابعتك و عرض المنشورات.", "compose_form.lock_disclaimer": "حسابك ليس {locked}. يمكن لأي شخص متابعتك و عرض المنشورات.",
"compose_form.lock_disclaimer.lock": "مقفل", "compose_form.lock_disclaimer.lock": "مقفل",
@ -77,21 +77,22 @@
"compose_form.poll.remove_option": "إزالة هذا الخيار", "compose_form.poll.remove_option": "إزالة هذا الخيار",
"compose_form.publish": "بوّق", "compose_form.publish": "بوّق",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "تحديد الوسائط كحساسة",
"compose_form.sensitive.marked": "لقد تم تحديد هذه الصورة كحساسة", "compose_form.sensitive.marked": "لقد تم تحديد هذه الصورة كحساسة",
"compose_form.sensitive.unmarked": "لم يتم تحديد الصورة كحساسة", "compose_form.sensitive.unmarked": "لم يتم تحديد الصورة كحساسة",
"compose_form.spoiler.marked": "إنّ النص مخفي وراء تحذير", "compose_form.spoiler.marked": "إنّ النص مخفي وراء تحذير",
"compose_form.spoiler.unmarked": "النص غير مخفي", "compose_form.spoiler.unmarked": "النص غير مخفي",
"compose_form.spoiler_placeholder": "تنبيه عن المحتوى", "compose_form.spoiler_placeholder": "تنبيه عن المحتوى",
"confirmation_modal.cancel": "إلغاء", "confirmation_modal.cancel": "إلغاء",
"confirmations.block.block_and_report": "Block & Report", "confirmations.block.block_and_report": "احجبه وابلغ عنه",
"confirmations.block.confirm": "حجب", "confirmations.block.confirm": "حجب",
"confirmations.block.message": "هل أنت متأكد أنك تريد حجب {name} ؟", "confirmations.block.message": "هل أنت متأكد أنك تريد حجب {name} ؟",
"confirmations.delete.confirm": "حذف", "confirmations.delete.confirm": "حذف",
"confirmations.delete.message": "هل أنت متأكد أنك تريد حذف هذا المنشور ؟", "confirmations.delete.message": "هل أنت متأكد أنك تريد حذف هذا المنشور ؟",
"confirmations.delete_list.confirm": "Delete", "confirmations.delete_list.confirm": "احذف",
"confirmations.delete_list.message": "هل تود حقا حذف هذه القائمة ؟", "confirmations.delete_list.message": "هل تود حقا حذف هذه القائمة ؟",
"confirmations.domain_block.confirm": "إخفاء إسم النطاق كاملا", "confirmations.domain_block.confirm": "إخفاء اسم النطاق كاملا",
"confirmations.domain_block.message": "متأكد من أنك تود حظر إسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.\nلن تتمكن مِن رؤية محتوى هذا النطاق لا على خيوطك العمومية و لا في إشعاراتك. سوف يتم كذلك إزالة كافة متابعيك المنتمين إلى هذا النطاق.", "confirmations.domain_block.message": "متأكد من أنك تود حظر اسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.\nلن تتمكن مِن رؤية محتوى هذا النطاق لا على خيوطك العمومية و لا في إشعاراتك. سوف يتم كذلك إزالة كافة متابعيك المنتمين إلى هذا النطاق.",
"confirmations.mute.confirm": "أكتم", "confirmations.mute.confirm": "أكتم",
"confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟", "confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
"confirmations.redraft.confirm": "إزالة و إعادة الصياغة", "confirmations.redraft.confirm": "إزالة و إعادة الصياغة",
@ -101,17 +102,17 @@
"confirmations.unfollow.confirm": "إلغاء المتابعة", "confirmations.unfollow.confirm": "إلغاء المتابعة",
"confirmations.unfollow.message": "متأكد من أنك تريد إلغاء متابعة {name} ؟", "confirmations.unfollow.message": "متأكد من أنك تريد إلغاء متابعة {name} ؟",
"embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.", "embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
"embed.preview": "هكذا ما سوف يبدو عليه :", "embed.preview": "هكذا ما سوف يبدو عليه:",
"emoji_button.activity": "الأنشطة", "emoji_button.activity": "الأنشطة",
"emoji_button.custom": "مخصص", "emoji_button.custom": "مخصص",
"emoji_button.flags": "الأعلام", "emoji_button.flags": "الأعلام",
"emoji_button.food": "الطعام والشراب", "emoji_button.food": "الطعام والشراب",
"emoji_button.label": "أدرج إيموجي", "emoji_button.label": "أدرج إيموجي",
"emoji_button.nature": "الطبيعة", "emoji_button.nature": "الطبيعة",
"emoji_button.not_found": "لا إيموجو !! (╯°□°)╯︵ ┻━┻", "emoji_button.not_found": "لا إيموجو!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "أشياء", "emoji_button.objects": "أشياء",
"emoji_button.people": "الناس", "emoji_button.people": "الناس",
"emoji_button.recent": "الشائعة الإستخدام", "emoji_button.recent": "الشائعة الاستخدام",
"emoji_button.search": "ابحث...", "emoji_button.search": "ابحث...",
"emoji_button.search_results": "نتائج البحث", "emoji_button.search_results": "نتائج البحث",
"emoji_button.symbols": "رموز", "emoji_button.symbols": "رموز",
@ -119,7 +120,7 @@
"empty_column.account_timeline": "ليس هناك تبويقات!", "empty_column.account_timeline": "ليس هناك تبويقات!",
"empty_column.account_unavailable": "الملف الشخصي غير متوفر", "empty_column.account_unavailable": "الملف الشخصي غير متوفر",
"empty_column.blocks": "لم تقم بحظر أي مستخدِم بعد.", "empty_column.blocks": "لم تقم بحظر أي مستخدِم بعد.",
"empty_column.community": "الخط الزمني المحلي فارغ. أكتب شيئا ما للعامة كبداية !", "empty_column.community": "الخط العام المحلي فارغ. أكتب شيئا ما للعامة كبداية!",
"empty_column.direct": "لم تتلق أية رسالة خاصة مباشِرة بعد. سوف يتم عرض الرسائل المباشرة هنا إن قمت بإرسال واحدة أو تلقيت البعض منها.", "empty_column.direct": "لم تتلق أية رسالة خاصة مباشِرة بعد. سوف يتم عرض الرسائل المباشرة هنا إن قمت بإرسال واحدة أو تلقيت البعض منها.",
"empty_column.domain_blocks": "ليس هناك نطاقات مخفية بعد.", "empty_column.domain_blocks": "ليس هناك نطاقات مخفية بعد.",
"empty_column.favourited_statuses": "ليس لديك أية تبويقات مفضلة بعد. عندما ستقوم بالإعجاب بواحد، سيظهر هنا.", "empty_column.favourited_statuses": "ليس لديك أية تبويقات مفضلة بعد. عندما ستقوم بالإعجاب بواحد، سيظهر هنا.",
@ -132,7 +133,7 @@
"empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قائمتك هنا إن قمت بإنشاء واحدة.", "empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قائمتك هنا إن قمت بإنشاء واحدة.",
"empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.", "empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.",
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.", "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
"empty_column.public": "لا يوجد أي شيء هنا ! قم بنشر شيء ما للعامة، أو إتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات", "empty_column.public": "لا يوجد أي شيء هنا! قم بنشر شيء ما للعامة، أو اتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات",
"federation.change": "Adjust status federation", "federation.change": "Adjust status federation",
"federation.federated.long": "Allow toot to reach other instances", "federation.federated.long": "Allow toot to reach other instances",
"federation.federated.short": "Federated", "federation.federated.short": "Federated",
@ -143,7 +144,7 @@
"getting_started.developers": "المُطوِّرون", "getting_started.developers": "المُطوِّرون",
"getting_started.directory": "دليل المستخدِمين والمستخدِمات", "getting_started.directory": "دليل المستخدِمين والمستخدِمات",
"getting_started.documentation": "الدليل", "getting_started.documentation": "الدليل",
"getting_started.heading": "إستعدّ للبدء", "getting_started.heading": "استعدّ للبدء",
"getting_started.invite": "دعوة أشخاص", "getting_started.invite": "دعوة أشخاص",
"getting_started.open_source_notice": "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء، على جيت هب {github}.", "getting_started.open_source_notice": "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء، على جيت هب {github}.",
"getting_started.security": "الأمان", "getting_started.security": "الأمان",
@ -160,16 +161,16 @@
"home.column_settings.basic": "أساسية", "home.column_settings.basic": "أساسية",
"home.column_settings.show_reblogs": "عرض الترقيات", "home.column_settings.show_reblogs": "عرض الترقيات",
"home.column_settings.show_replies": "عرض الردود", "home.column_settings.show_replies": "عرض الردود",
"intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", "intervals.full.minutes": "{number, plural, one {# دقيقة} other {# دقائق}}",
"introduction.federation.action": "التالي", "introduction.federation.action": "التالي",
"introduction.federation.federated.headline": "Federated", "introduction.federation.federated.headline": "الفديرالي",
"introduction.federation.federated.text": "كافة المنشورات التي نُشِرت إلى العامة على الخوادم الأخرى للفديفرس سوف يتم عرضها على الخيط المُوحَّد.", "introduction.federation.federated.text": "كافة المنشورات التي نُشِرت إلى العامة على الخوادم الأخرى للفديفرس سوف يتم عرضها على الخيط المُوحَّد.",
"introduction.federation.home.headline": "Home", "introduction.federation.home.headline": "الرئيسي",
"introduction.federation.home.text": "سوف تُعرَض منشورات الأشخاص الذين تُتابِعهم على الخيط الرئيسي. بإمكانك متابعة أي حساب أيا كان الخادم الذي هو عليه!", "introduction.federation.home.text": "سوف تُعرَض منشورات الأشخاص الذين تُتابِعهم على الخيط الرئيسي. بإمكانك متابعة أي حساب أيا كان الخادم الذي هو عليه!",
"introduction.federation.local.headline": "Local", "introduction.federation.local.headline": "الخيط العام المحلي",
"introduction.federation.local.text": "المنشورات المُوجّهة للعامة على نفس الخادم الذي أنتم عليه ستظهر على الخيط الزمني المحلي.", "introduction.federation.local.text": "المنشورات المُوجّهة للعامة على نفس الخادم الذي أنتم عليه ستظهر على الخيط العام المحلي.",
"introduction.interactions.action": "إنهاء العرض التوضيحي!", "introduction.interactions.action": "إنهاء العرض التوضيحي!",
"introduction.interactions.favourite.headline": "الإضافة إلى المفضلة", "introduction.interactions.favourite.headline": "الإضافة إلى المفضلة",
"introduction.interactions.favourite.text": "يمكِنك إضافة أي تبويق إلى المفضلة و إعلام صاحبه أنك أعجِبت بذاك التبويق.", "introduction.interactions.favourite.text": "يمكِنك إضافة أي تبويق إلى المفضلة و إعلام صاحبه أنك أعجِبت بذاك التبويق.",
@ -179,24 +180,24 @@
"introduction.interactions.reply.text": "يمكنكم الرد على تبويقاتكم و تبويقات الآخرين على شكل سلسلة محادثة.", "introduction.interactions.reply.text": "يمكنكم الرد على تبويقاتكم و تبويقات الآخرين على شكل سلسلة محادثة.",
"introduction.welcome.action": "هيا بنا!", "introduction.welcome.action": "هيا بنا!",
"introduction.welcome.headline": "الخطوات الأولى", "introduction.welcome.headline": "الخطوات الأولى",
"introduction.welcome.text": "مرحبا بكم على الفيديفيرس! بعد لحظات قليلة ، سيكون بمقدوركم بث رسائل والتحدث إلى أصدقائكم عبر تشكيلة واسعة من الخوادم المختلفة. هذا الخادم ، {domain} ، يستضيف ملفكم الشخصي ، لذا يجب تذكر اسمه جيدا.", "introduction.welcome.text": "مرحبا بكم على الفديفرس! بعد لحظات قليلة ، سيكون بمقدوركم بث رسائل والتحدث إلى أصدقائكم عبر تشكيلة واسعة من الخوادم المختلفة. هذا الخادم ، {domain} ، يستضيف ملفكم الشخصي ، لذا يجب تذكر اسمه جيدا.",
"keyboard_shortcuts.back": "للعودة", "keyboard_shortcuts.back": "للعودة",
"keyboard_shortcuts.blocked": "لفتح قائمة المستخدمين المحظورين", "keyboard_shortcuts.blocked": "لفتح قائمة المستخدمين المحظورين",
"keyboard_shortcuts.boost": "للترقية", "keyboard_shortcuts.boost": "للترقية",
"keyboard_shortcuts.column": "للتركيز على منشور على أحد الأعمدة", "keyboard_shortcuts.column": "للتركيز على منشور على أحد الأعمدة",
"keyboard_shortcuts.compose": "للتركيز على نافذة تحرير النصوص", "keyboard_shortcuts.compose": "للتركيز على نافذة تحرير النصوص",
"keyboard_shortcuts.description": "Description", "keyboard_shortcuts.description": "الوصف",
"keyboard_shortcuts.direct": "لفتح عمود الرسائل المباشرة", "keyboard_shortcuts.direct": "لفتح عمود الرسائل المباشرة",
"keyboard_shortcuts.down": "للإنتقال إلى أسفل القائمة", "keyboard_shortcuts.down": "للانتقال إلى أسفل القائمة",
"keyboard_shortcuts.enter": "to open status", "keyboard_shortcuts.enter": "لفتح المنشور",
"keyboard_shortcuts.favourite": "للإضافة إلى المفضلة", "keyboard_shortcuts.favourite": "للإضافة إلى المفضلة",
"keyboard_shortcuts.favourites": "لفتح قائمة المفضلات", "keyboard_shortcuts.favourites": "لفتح قائمة المفضلات",
"keyboard_shortcuts.federated": "لفتح الخيط الزمني الفديرالي", "keyboard_shortcuts.federated": "لفتح الخيط الزمني الفديرالي",
"keyboard_shortcuts.heading": "Keyboard Shortcuts", "keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.home": "لفتح الخيط الرئيسي", "keyboard_shortcuts.home": "لفتح الخيط الرئيسي",
"keyboard_shortcuts.hotkey": "مفتاح الإختصار", "keyboard_shortcuts.hotkey": "مفتاح الاختصار",
"keyboard_shortcuts.legend": "لعرض هذا المفتاح", "keyboard_shortcuts.legend": "لعرض هذا المفتاح",
"keyboard_shortcuts.local": "لفتح الخيط الزمني المحلي", "keyboard_shortcuts.local": "لفتح الخيط العام المحلي",
"keyboard_shortcuts.mention": "لذِكر الناشر", "keyboard_shortcuts.mention": "لذِكر الناشر",
"keyboard_shortcuts.muted": "لفتح قائمة المستخدِمين المكتومين", "keyboard_shortcuts.muted": "لفتح قائمة المستخدِمين المكتومين",
"keyboard_shortcuts.my_profile": "لفتح ملفك الشخصي", "keyboard_shortcuts.my_profile": "لفتح ملفك الشخصي",
@ -208,22 +209,24 @@
"keyboard_shortcuts.search": "للتركيز على البحث", "keyboard_shortcuts.search": "للتركيز على البحث",
"keyboard_shortcuts.start": "لفتح عمود \"هيا نبدأ\"", "keyboard_shortcuts.start": "لفتح عمود \"هيا نبدأ\"",
"keyboard_shortcuts.toggle_hidden": "لعرض أو إخفاء النص مِن وراء التحذير", "keyboard_shortcuts.toggle_hidden": "لعرض أو إخفاء النص مِن وراء التحذير",
"keyboard_shortcuts.toggle_sensitivity": "لعرض/إخفاء الوسائط",
"keyboard_shortcuts.toot": "لتحرير تبويق جديد", "keyboard_shortcuts.toot": "لتحرير تبويق جديد",
"keyboard_shortcuts.unfocus": "لإلغاء التركيز على حقل النص أو نافذة البحث", "keyboard_shortcuts.unfocus": "لإلغاء التركيز على حقل النص أو نافذة البحث",
"keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة", "keyboard_shortcuts.up": "للانتقال إلى أعلى القائمة",
"lightbox.close": "إغلاق", "lightbox.close": "إغلاق",
"lightbox.next": "التالي", "lightbox.next": "التالي",
"lightbox.previous": "العودة", "lightbox.previous": "العودة",
"lightbox.view_context": "اعرض السياق",
"lists.account.add": "أضف إلى القائمة", "lists.account.add": "أضف إلى القائمة",
"lists.account.remove": "إحذف من القائمة", "lists.account.remove": "احذف من القائمة",
"lists.delete": "Delete list", "lists.delete": "احذف القائمة",
"lists.edit": "تعديل القائمة", "lists.edit": "تعديل القائمة",
"lists.edit.submit": "تعديل العنوان", "lists.edit.submit": "تعديل العنوان",
"lists.new.create": "إنشاء قائمة", "lists.new.create": "إنشاء قائمة",
"lists.new.title_placeholder": "عنوان القائمة الجديدة", "lists.new.title_placeholder": "عنوان القائمة الجديدة",
"lists.search": "إبحث في قائمة الحسابات التي تُتابِعها", "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
"lists.subheading": "قوائمك", "lists.subheading": "قوائمك",
"loading_indicator.label": "تحميل ...", "loading_indicator.label": "تحميل...",
"media_gallery.toggle_visible": "عرض / إخفاء", "media_gallery.toggle_visible": "عرض / إخفاء",
"missing_indicator.label": "تعذر العثور عليه", "missing_indicator.label": "تعذر العثور عليه",
"missing_indicator.sublabel": "تعذر العثور على هذا المورد", "missing_indicator.sublabel": "تعذر العثور على هذا المورد",
@ -233,20 +236,22 @@
"navigation_bar.community_timeline": "الخيط العام المحلي", "navigation_bar.community_timeline": "الخيط العام المحلي",
"navigation_bar.compose": "تحرير تبويق جديد", "navigation_bar.compose": "تحرير تبويق جديد",
"navigation_bar.direct": "الرسائل المباشِرة", "navigation_bar.direct": "الرسائل المباشِرة",
"navigation_bar.discover": "إكتشف", "navigation_bar.discover": "اكتشف",
"navigation_bar.domain_blocks": "النطاقات المخفية", "navigation_bar.domain_blocks": "النطاقات المخفية",
"navigation_bar.edit_profile": "تعديل الملف الشخصي", "navigation_bar.edit_profile": "تعديل الملف الشخصي",
"navigation_bar.favourites": "المفضلة", "navigation_bar.favourites": "المفضلة",
"navigation_bar.filters": "الكلمات المكتومة", "navigation_bar.filters": "الكلمات المكتومة",
"navigation_bar.follow_requests": "طلبات المتابعة", "navigation_bar.follow_requests": "طلبات المتابعة",
"navigation_bar.follows_and_followers": "المتابِعين والمتابَعون",
"navigation_bar.info": "عن هذا الخادم", "navigation_bar.info": "عن هذا الخادم",
"navigation_bar.keyboard_shortcuts": "إختصارات لوحة المفاتيح", "navigation_bar.keyboard_shortcuts": "اختصارات لوحة المفاتيح",
"navigation_bar.lists": "القوائم", "navigation_bar.lists": "القوائم",
"navigation_bar.logout": "خروج", "navigation_bar.logout": "خروج",
"navigation_bar.mutes": "الحسابات المكتومة", "navigation_bar.mutes": "الحسابات المكتومة",
"navigation_bar.personal": "Personal", "navigation_bar.personal": "شخصي",
"navigation_bar.pins": "التبويقات المثبتة", "navigation_bar.pins": "التبويقات المثبتة",
"navigation_bar.preferences": "التفضيلات", "navigation_bar.preferences": "التفضيلات",
"navigation_bar.profile_directory": "دليل المستخدِمين",
"navigation_bar.public_timeline": "الخيط العام الموحد", "navigation_bar.public_timeline": "الخيط العام الموحد",
"navigation_bar.security": "الأمان", "navigation_bar.security": "الأمان",
"notification.favourite": "أُعجِب {name} بمنشورك", "notification.favourite": "أُعجِب {name} بمنشورك",
@ -254,19 +259,19 @@
"notification.mention": "{name} ذكرك", "notification.mention": "{name} ذكرك",
"notification.poll": "A poll you have voted in has ended", "notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} قام بترقية تبويقك", "notification.reblog": "{name} قام بترقية تبويقك",
"notifications.clear": "إمسح الإخطارات", "notifications.clear": "امسح الإخطارات",
"notifications.clear_confirmation": "أمتأكد من أنك تود مسح جل الإخطارات الخاصة بك و المتلقاة إلى حد الآن ؟", "notifications.clear_confirmation": "أمتأكد من أنك تود مسح جل الإخطارات الخاصة بك و المتلقاة إلى حد الآن ؟",
"notifications.column_settings.alert": "إشعارات سطح المكتب", "notifications.column_settings.alert": "إشعارات سطح المكتب",
"notifications.column_settings.favourite": "المُفَضَّلة :", "notifications.column_settings.favourite": "المُفَضَّلة:",
"notifications.column_settings.filter_bar.advanced": "عرض كافة الفئات", "notifications.column_settings.filter_bar.advanced": "عرض كافة الفئات",
"notifications.column_settings.filter_bar.category": "شريط الفلترة السريعة", "notifications.column_settings.filter_bar.category": "شريط الفلترة السريعة",
"notifications.column_settings.filter_bar.show": "عرض", "notifications.column_settings.filter_bar.show": "عرض",
"notifications.column_settings.follow": "متابعُون جُدُد :", "notifications.column_settings.follow": "متابعُون جُدُد:",
"notifications.column_settings.mention": "الإشارات :", "notifications.column_settings.mention": "الإشارات:",
"notifications.column_settings.poll": "نتائج استطلاع الرأي:", "notifications.column_settings.poll": "نتائج استطلاع الرأي:",
"notifications.column_settings.push": "الإخطارات المدفوعة", "notifications.column_settings.push": "الإخطارات المدفوعة",
"notifications.column_settings.reblog": "الترقيّات:", "notifications.column_settings.reblog": "الترقيّات:",
"notifications.column_settings.show": "إعرِضها في عمود", "notifications.column_settings.show": "اعرِضها في عمود",
"notifications.column_settings.sound": "أصدر صوتا", "notifications.column_settings.sound": "أصدر صوتا",
"notifications.filter.all": "الكل", "notifications.filter.all": "الكل",
"notifications.filter.boosts": "الترقيات", "notifications.filter.boosts": "الترقيات",
@ -277,11 +282,11 @@
"notifications.group": "{count} إشعارات", "notifications.group": "{count} إشعارات",
"poll.closed": "انتهى", "poll.closed": "انتهى",
"poll.refresh": "تحديث", "poll.refresh": "تحديث",
"poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.total_votes": "{count, plural, one {# صوت} other {# أصوات}}",
"poll.vote": "صَوّت", "poll.vote": "صَوّت",
"poll_button.add_poll": "إضافة استطلاع للرأي", "poll_button.add_poll": "إضافة استطلاع للرأي",
"poll_button.remove_poll": "إزالة استطلاع الرأي", "poll_button.remove_poll": "إزالة استطلاع الرأي",
"privacy.change": "إضبط خصوصية المنشور", "privacy.change": "اضبط خصوصية المنشور",
"privacy.direct.long": "أنشر إلى المستخدمين المشار إليهم فقط", "privacy.direct.long": "أنشر إلى المستخدمين المشار إليهم فقط",
"privacy.direct.short": "مباشر", "privacy.direct.short": "مباشر",
"privacy.private.long": "أنشر لمتابعيك فقط", "privacy.private.long": "أنشر لمتابعيك فقط",
@ -290,20 +295,20 @@
"privacy.public.short": "للعامة", "privacy.public.short": "للعامة",
"privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة", "privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة",
"privacy.unlisted.short": "غير مدرج", "privacy.unlisted.short": "غير مدرج",
"regeneration_indicator.label": "جارٍ التحميل …", "regeneration_indicator.label": "جارٍ التحميل…",
"regeneration_indicator.sublabel": "جارٍ تجهيز تغذية صفحتك الرئيسية !", "regeneration_indicator.sublabel": "جارٍ تجهيز تغذية صفحتك الرئيسية!",
"relative_time.days": "{number}d", "relative_time.days": "{number}ي",
"relative_time.hours": "{number}h", "relative_time.hours": "{number}سا",
"relative_time.just_now": "الآن", "relative_time.just_now": "الآن",
"relative_time.minutes": "{number}m", "relative_time.minutes": "{number}د",
"relative_time.seconds": "{number}s", "relative_time.seconds": "{number}ثا",
"reply_indicator.cancel": "إلغاء", "reply_indicator.cancel": "إلغاء",
"report.forward": "التحويل إلى {target}", "report.forward": "التحويل إلى {target}",
"report.forward_hint": "هذا الحساب ينتمي إلى خادوم آخَر. هل تودّ إرسال نسخة مجهولة مِن التقرير إلى هنالك أيضًا ؟", "report.forward_hint": "هذا الحساب ينتمي إلى خادوم آخَر. هل تودّ إرسال نسخة مجهولة مِن التقرير إلى هنالك أيضًا؟",
"report.hint": "سوف يتم إرسال التقرير إلى المُشرِفين على خادومكم. بإمكانكم الإدلاء بشرح عن سبب الإبلاغ عن الحساب أسفله:", "report.hint": "سوف يتم إرسال التقرير إلى المُشرِفين على خادومكم. بإمكانكم الإدلاء بشرح عن سبب الإبلاغ عن الحساب أسفله:",
"report.placeholder": "تعليقات إضافية", "report.placeholder": "تعليقات إضافية",
"report.submit": "إرسال", "report.submit": "إرسال",
"report.target": "إبلاغ", "report.target": "ابلغ عن {target}",
"search.placeholder": "ابحث", "search.placeholder": "ابحث",
"search_popout.search_format": "نمط البحث المتقدم", "search_popout.search_format": "نمط البحث المتقدم",
"search_popout.tips.full_text": "النص البسيط يقوم بعرض المنشورات التي كتبتها أو قمت بإرسالها أو ترقيتها أو تمت الإشارة إليك فيها من طرف آخرين ، بالإضافة إلى مطابقة أسماء المستخدمين وأسماء العرض وعلامات التصنيف.", "search_popout.tips.full_text": "النص البسيط يقوم بعرض المنشورات التي كتبتها أو قمت بإرسالها أو ترقيتها أو تمت الإشارة إليك فيها من طرف آخرين ، بالإضافة إلى مطابقة أسماء المستخدمين وأسماء العرض وعلامات التصنيف.",
@ -317,11 +322,11 @@
"search_results.total": "{count, number} {count, plural, one {result} و {results}}", "search_results.total": "{count, number} {count, plural, one {result} و {results}}",
"status.admin_account": "افتح الواجهة الإدارية لـ @{name}", "status.admin_account": "افتح الواجهة الإدارية لـ @{name}",
"status.admin_status": "افتح هذا المنشور على واجهة الإشراف", "status.admin_status": "افتح هذا المنشور على واجهة الإشراف",
"status.block": "Block @{name}", "status.block": "احجب @{name}",
"status.cancel_reblog_private": "إلغاء الترقية", "status.cancel_reblog_private": "إلغاء الترقية",
"status.cannot_reblog": "تعذرت ترقية هذا المنشور", "status.cannot_reblog": "تعذرت ترقية هذا المنشور",
"status.copy": "نسخ رابط المنشور", "status.copy": "نسخ رابط المنشور",
"status.delete": "إحذف", "status.delete": "احذف",
"status.detailed_status": "تفاصيل المحادثة", "status.detailed_status": "تفاصيل المحادثة",
"status.direct": "رسالة خاصة إلى @{name}", "status.direct": "رسالة خاصة إلى @{name}",
"status.embed": "إدماج", "status.embed": "إدماج",
@ -345,33 +350,32 @@
"status.redraft": "إزالة و إعادة الصياغة", "status.redraft": "إزالة و إعادة الصياغة",
"status.reply": "ردّ", "status.reply": "ردّ",
"status.replyAll": "رُد على الخيط", "status.replyAll": "رُد على الخيط",
"status.report": "إبلِغ عن @{name}", "status.report": "ابلِغ عن @{name}",
"status.sensitive_toggle": "اضغط للعرض",
"status.sensitive_warning": "محتوى حساس", "status.sensitive_warning": "محتوى حساس",
"status.share": "مشاركة", "status.share": "مشاركة",
"status.show_less": "إعرض أقلّ", "status.show_less": "اعرض أقلّ",
"status.show_less_all": "طي الكل", "status.show_less_all": "طي الكل",
"status.show_more": "أظهر المزيد", "status.show_more": "أظهر المزيد",
"status.show_more_all": "توسيع الكل", "status.show_more_all": "توسيع الكل",
"status.show_thread": "الكشف عن المحادثة", "status.show_thread": "الكشف عن المحادثة",
"status.unmute_conversation": "فك الكتم عن المحادثة", "status.unmute_conversation": "فك الكتم عن المحادثة",
"status.unpin": "فك التدبيس من الملف الشخصي", "status.unpin": "فك التدبيس من الملف الشخصي",
"suggestions.dismiss": "إلغاء الإقتراح", "suggestions.dismiss": "إلغاء الاقتراح",
"suggestions.header": "يمكن أن يهمك…", "suggestions.header": "يمكن أن يهمك…",
"tabs_bar.federated_timeline": "الموحَّد", "tabs_bar.federated_timeline": "الموحَّد",
"tabs_bar.home": "الرئيسية", "tabs_bar.home": "الرئيسية",
"tabs_bar.local_timeline": "المحلي", "tabs_bar.local_timeline": "الخيط العام المحلي",
"tabs_bar.notifications": "الإخطارات", "tabs_bar.notifications": "الإخطارات",
"tabs_bar.search": "البحث", "tabs_bar.search": "البحث",
"time_remaining.days": "{number, plural, one {# day} other {# days}} left", "time_remaining.days": "{number, plural, one {# يوم} other {# أيام}} متبقية",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining", "time_remaining.moments": "لحظات متبقية",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} آخرون {people}} يتحدثون", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} آخرون {people}} يتحدثون",
"ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.", "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
"upload_area.title": "إسحب ثم أفلت للرفع", "upload_area.title": "اسحب ثم أفلت للرفع",
"upload_button.label": "إضافة وسائط (JPEG، PNG، GIF، WebM، MP4، MOV)", "upload_button.label": "إضافة وسائط ({formats})",
"upload_error.limit": "لقد تم بلوغ الحد الأقصى المسموح به لإرسال الملفات.", "upload_error.limit": "لقد تم بلوغ الحد الأقصى المسموح به لإرسال الملفات.",
"upload_error.poll": "لا يمكن إدراج ملفات في استطلاعات الرأي.", "upload_error.poll": "لا يمكن إدراج ملفات في استطلاعات الرأي.",
"upload_form.description": "وصف للمعاقين بصريا", "upload_form.description": "وصف للمعاقين بصريا",

View File

@ -77,6 +77,7 @@
"compose_form.poll.remove_option": "Remove this choice", "compose_form.poll.remove_option": "Remove this choice",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Mark media as sensitive",
"compose_form.sensitive.marked": "Media is marked as sensitive", "compose_form.sensitive.marked": "Media is marked as sensitive",
"compose_form.sensitive.unmarked": "Media is not marked as sensitive", "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
"compose_form.spoiler.marked": "El testu nun va anubrise darrera d'una alvertencia", "compose_form.spoiler.marked": "El testu nun va anubrise darrera d'una alvertencia",
@ -170,7 +171,7 @@
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.local.headline": "Local", "introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!", "introduction.interactions.action": "Finish toot-orial!",
"introduction.interactions.favourite.headline": "Favourite", "introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost", "introduction.interactions.reblog.headline": "Boost",
@ -208,12 +209,14 @@
"keyboard_shortcuts.search": "to focus search", "keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.start": "p'abrir la columna «entamar»", "keyboard_shortcuts.start": "p'abrir la columna «entamar»",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
"keyboard_shortcuts.toot": "p'apenzar un toot nuevu", "keyboard_shortcuts.toot": "p'apenzar un toot nuevu",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "pa xubir na llista", "keyboard_shortcuts.up": "pa xubir na llista",
"lightbox.close": "Close", "lightbox.close": "Close",
"lightbox.next": "Siguiente", "lightbox.next": "Siguiente",
"lightbox.previous": "Previous", "lightbox.previous": "Previous",
"lightbox.view_context": "View context",
"lists.account.add": "Amestar a la llista", "lists.account.add": "Amestar a la llista",
"lists.account.remove": "Desaniciar de la llista", "lists.account.remove": "Desaniciar de la llista",
"lists.delete": "Desaniciar la llista", "lists.delete": "Desaniciar la llista",
@ -239,6 +242,7 @@
"navigation_bar.favourites": "Favoritos", "navigation_bar.favourites": "Favoritos",
"navigation_bar.filters": "Pallabres silenciaes", "navigation_bar.filters": "Pallabres silenciaes",
"navigation_bar.follow_requests": "Solicitúes de siguimientu", "navigation_bar.follow_requests": "Solicitúes de siguimientu",
"navigation_bar.follows_and_followers": "Follows and followers",
"navigation_bar.info": "Tocante a esta instancia", "navigation_bar.info": "Tocante a esta instancia",
"navigation_bar.keyboard_shortcuts": "Atayos", "navigation_bar.keyboard_shortcuts": "Atayos",
"navigation_bar.lists": "Llistes", "navigation_bar.lists": "Llistes",
@ -247,6 +251,7 @@
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Personal",
"navigation_bar.pins": "Toots fixaos", "navigation_bar.pins": "Toots fixaos",
"navigation_bar.preferences": "Preferencies", "navigation_bar.preferences": "Preferencies",
"navigation_bar.profile_directory": "Profile directory",
"navigation_bar.public_timeline": "Llinia temporal federada", "navigation_bar.public_timeline": "Llinia temporal federada",
"navigation_bar.security": "Seguranza", "navigation_bar.security": "Seguranza",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} favourited your status",
@ -346,7 +351,6 @@
"status.reply": "Responder", "status.reply": "Responder",
"status.replyAll": "Reply to thread", "status.replyAll": "Reply to thread",
"status.report": "Report @{name}", "status.report": "Report @{name}",
"status.sensitive_toggle": "Fai clic pa velu",
"status.sensitive_warning": "Conteníu sensible", "status.sensitive_warning": "Conteníu sensible",
"status.share": "Share", "status.share": "Share",
"status.show_less": "Amosar menos", "status.show_less": "Amosar menos",

View File

@ -77,6 +77,7 @@
"compose_form.poll.remove_option": "Remove this choice", "compose_form.poll.remove_option": "Remove this choice",
"compose_form.publish": "Раздумай", "compose_form.publish": "Раздумай",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Mark media as sensitive",
"compose_form.sensitive.marked": "Media is marked as sensitive", "compose_form.sensitive.marked": "Media is marked as sensitive",
"compose_form.sensitive.unmarked": "Media is not marked as sensitive", "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
"compose_form.spoiler.marked": "Text is hidden behind warning", "compose_form.spoiler.marked": "Text is hidden behind warning",
@ -170,7 +171,7 @@
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.local.headline": "Local", "introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!", "introduction.interactions.action": "Finish toot-orial!",
"introduction.interactions.favourite.headline": "Favourite", "introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost", "introduction.interactions.reblog.headline": "Boost",
@ -208,12 +209,14 @@
"keyboard_shortcuts.search": "to focus search", "keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.start": "to open \"get started\" column", "keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
"keyboard_shortcuts.toot": "to start a brand new toot", "keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list", "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Затвори", "lightbox.close": "Затвори",
"lightbox.next": "Next", "lightbox.next": "Next",
"lightbox.previous": "Previous", "lightbox.previous": "Previous",
"lightbox.view_context": "View context",
"lists.account.add": "Add to list", "lists.account.add": "Add to list",
"lists.account.remove": "Remove from list", "lists.account.remove": "Remove from list",
"lists.delete": "Delete list", "lists.delete": "Delete list",
@ -239,6 +242,7 @@
"navigation_bar.favourites": "Favourites", "navigation_bar.favourites": "Favourites",
"navigation_bar.filters": "Muted words", "navigation_bar.filters": "Muted words",
"navigation_bar.follow_requests": "Follow requests", "navigation_bar.follow_requests": "Follow requests",
"navigation_bar.follows_and_followers": "Follows and followers",
"navigation_bar.info": "Extended information", "navigation_bar.info": "Extended information",
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
"navigation_bar.lists": "Lists", "navigation_bar.lists": "Lists",
@ -247,6 +251,7 @@
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Personal",
"navigation_bar.pins": "Pinned toots", "navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Предпочитания", "navigation_bar.preferences": "Предпочитания",
"navigation_bar.profile_directory": "Profile directory",
"navigation_bar.public_timeline": "Публичен канал", "navigation_bar.public_timeline": "Публичен канал",
"navigation_bar.security": "Security", "navigation_bar.security": "Security",
"notification.favourite": "{name} хареса твоята публикация", "notification.favourite": "{name} хареса твоята публикация",
@ -346,7 +351,6 @@
"status.reply": "Отговор", "status.reply": "Отговор",
"status.replyAll": "Reply to thread", "status.replyAll": "Reply to thread",
"status.report": "Report @{name}", "status.report": "Report @{name}",
"status.sensitive_toggle": "Покажи",
"status.sensitive_warning": "Деликатно съдържание", "status.sensitive_warning": "Деликатно съдържание",
"status.share": "Share", "status.share": "Share",
"status.show_less": "Show less", "status.show_less": "Show less",

View File

@ -1,384 +1,388 @@
{ {
"account.add_or_remove_from_list": "লিস্টে আরো যুক্ত বা মুছে ফেলুন", "account.add_or_remove_from_list": "তালিকাতে আরো যুক্ত বা মুছে ফেলুন",
"account.badges.bot": "রোবট", "account.badges.bot": "রোবট",
"account.block": "@{name} কে বন্ধ করুন", "account.block": "@{name} বন্ধ করুন",
"account.block_domain": "{domain} থেকে সব সরিয়ে ফেলুন", "account.block_domain": "{domain} থেকে সব সরিয়ে ফেলুন",
"account.blocked": "বন্ধ করা হয়েছে", "account.blocked": "বন্ধ করা হয়েছে",
"account.direct": "@{name}কে সরকারি পাঠান", "account.direct": "@{name}কে সরকারি লিখুন",
"account.domain_blocked": "বেবিসিটটি সরানো আছে", "account.domain_blocked": "ওয়েবসাইট সরিয়ে ফেলা হয়েছে",
"account.edit_profile": "Edit profile", "account.edit_profile": "নিজের পাতা সম্পাদনা করুন",
"account.endorse": "Feature on profile", "account.endorse": "নিজের পাতায় দেখান",
"account.follow": "Follow", "account.follow": "অনুসরণ করুন",
"account.followers": "Followers", "account.followers": "অনুসরণকারক",
"account.followers.empty": "No one follows this user yet.", "account.followers.empty": "এই ব্যবহারকারীকে কেও এখনো অনুসরণ করে না।",
"account.follows": "Follows", "account.follows": "যাদেরকে অনুসরণ করেন",
"account.follows.empty": "This user doesn't follow anyone yet.", "account.follows.empty": "এই ব্যবহারকারী কাওকে এখনো অনুসরণ করেন না।",
"account.follows_you": "Follows you", "account.follows_you": "আপনাকে অনুসরণ করে",
"account.hide_reblogs": "Hide boosts from @{name}", "account.hide_reblogs": "@{name}র সমর্থনগুলি সরিয়ে ফেলুন",
"account.link_verified_on": "Ownership of this link was checked on {date}", "account.link_verified_on": "এই লিংকের মালিকানা চেক করা হয়েছে {date} তারিকে",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.locked_info": "এই নিবন্ধনের গোপনীয়তার ক্ষেত্র তালা দেওয়া আছে। নিবন্ধনকারী অনুসরণ করার অনুমতি যাদেরকে দেবেন, শুধু তারাই অনুসরণ করতে পারবেন।",
"account.media": "Media", "account.media": "ছবি বা ভিডিও",
"account.mention": "Mention @{name}", "account.mention": "@{name} কে উল্লেখ করুন",
"account.moved_to": "{name} has moved to:", "account.moved_to": "{name} চলে গেছে এখানে:",
"account.mute": "Mute @{name}", "account.mute": "@{name}র কার্যক্রম সরিয়ে ফেলুন",
"account.mute_notifications": "Mute notifications from @{name}", "account.mute_notifications": "@{name}র প্রজ্ঞাপন আপনার কাছ থেকে সরিয়ে ফেলুন",
"account.muted": "Muted", "account.muted": "সরানো আছে",
"account.posts": "Toots", "account.posts": "টুট",
"account.posts_with_replies": "Toots and replies", "account.posts_with_replies": "টুট এবং মতামত",
"account.report": "Report @{name}", "account.report": "@{name}কে রিপোর্ট করে দিন",
"account.requested": "Awaiting approval. Click to cancel follow request", "account.requested": "অনুমতির অপেক্ষায় আছে। অনুসরণ করার অনুরোধ বাতিল করতে এখানে ক্লিক করুন",
"account.share": "Share @{name}'s profile", "account.share": "@{name}র পাতা অন্যদের দেখান",
"account.show_reblogs": "Show boosts from @{name}", "account.show_reblogs": "@{name}র সমর্থনগুলো দেখুন",
"account.unblock": "Unblock @{name}", "account.unblock": "@{name}র কার্যকলাপ আবার দেখুন",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "{domain}থেকে আবার দেখুন",
"account.unendorse": "Don't feature on profile", "account.unendorse": "নিজের পাতায় এটা দেখতে চান না",
"account.unfollow": "Unfollow", "account.unfollow": "অনুসরণ বন্ধ করুন",
"account.unmute": "Unmute @{name}", "account.unmute": "@{name}র কার্যকলাপ আবার দেখুন",
"account.unmute_notifications": "Unmute notifications from @{name}", "account.unmute_notifications": "@{name}র প্রজ্ঞাপন দেওয়ার অনুমতি দিন",
"alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.message": "অপ্রত্যাশিত একটি সমস্যা হয়েছে।",
"alert.unexpected.title": "Oops!", "alert.unexpected.title": "ওহো!",
"boost_modal.combo": "You can press {combo} to skip this next time", "boost_modal.combo": "পরেরবার আপনি {combo} চাপ দিলে এটার শেষে চলে যেতে পারবেন",
"bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.body": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।",
"bundle_column_error.retry": "Try again", "bundle_column_error.retry": "আবার চেষ্টা করুন",
"bundle_column_error.title": "Network error", "bundle_column_error.title": "নেটওয়ার্কের সমস্যা হচ্ছে",
"bundle_modal_error.close": "Close", "bundle_modal_error.close": "বন্ধ করুন",
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।",
"bundle_modal_error.retry": "Try again", "bundle_modal_error.retry": "আবার চেষ্টা করুন",
"column.blocks": "Blocked users", "column.blocks": "যাদের বন্ধ করে রাখা হয়েছে",
"column.community": "Local timeline", "column.community": "স্থানীয় সময়সারি",
"column.direct": "Direct messages", "column.direct": "সরাসরি লেখা",
"column.domain_blocks": "Hidden domains", "column.domain_blocks": "সরিয়ে ফেলা ওয়েবসাইট",
"column.favourites": "Favourites", "column.favourites": "পছন্দের গুলো",
"column.follow_requests": "Follow requests", "column.follow_requests": "অনুসরণের অনুমতি চেয়েছে যারা",
"column.home": "Home", "column.home": "বাড়ি",
"column.lists": "Lists", "column.lists": "তালিকাগুলো",
"column.mutes": "Muted users", "column.mutes": "যাদের কার্যক্রম দেখা বন্ধ আছে",
"column.notifications": "Notifications", "column.notifications": "প্রজ্ঞাপনগুলো",
"column.pins": "Pinned toot", "column.pins": "পিন করা টুট",
"column.public": "Federated timeline", "column.public": "যুক্ত সময়রেখা",
"column_back_button.label": "Back", "column_back_button.label": "পেছনে",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "সেটিংগুলো সরান",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "কলমটা বামে সরান",
"column_header.moveRight_settings": "Move column to the right", "column_header.moveRight_settings": "কলমটা ডানে সরান",
"column_header.pin": "Pin", "column_header.pin": "পিন দিয়ে রাখুন",
"column_header.show_settings": "Show settings", "column_header.show_settings": "সেটিং দেখান",
"column_header.unpin": "Unpin", "column_header.unpin": "পিন খুলুন",
"column_subheading.settings": "Settings", "column_subheading.settings": "সেটিং",
"community.column_settings.media_only": "Media Only", "community.column_settings.media_only": "শুধুমাত্র ছবি বা ভিডিও",
"compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", "compose_form.direct_message_warning": "শুধুমাত্র যাদেরকে উল্লেখ করা হয়েছে তাদেরকেই এই টুটটি পাঠানো হবে ।",
"compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.direct_message_warning_learn_more": "আরো জানুন",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", "compose_form.hashtag_warning": "কোনো হ্যাশট্যাগের ভেতরে এই টুটটি থাকবেনা কারণ এটি তালিকাবহির্ভূত। শুধুমাত্র প্রকাশ্য ঠোটগুলো হ্যাশট্যাগের ভেতরে খুঁজে পাওয়া যাবে।",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "আপনার নিবন্ধনে তালা দেওয়া নেই, যে কেও আপনাকে অনুসরণ করতে পারবে এবং অনুশারকদের জন্য লেখা দেখতে পারবে।",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "তালা দেওয়া",
"compose_form.placeholder": "What is on your mind?", "compose_form.placeholder": "আপনি কি ভাবছেন ?",
"compose_form.poll.add_option": "Add a choice", "compose_form.poll.add_option": "আরেকটি বিকল্প যোগ করুন",
"compose_form.poll.duration": "Poll duration", "compose_form.poll.duration": "ভোটগ্রহনের সময়",
"compose_form.poll.option_placeholder": "Choice {number}", "compose_form.poll.option_placeholder": "বিকল্প {number}",
"compose_form.poll.remove_option": "Remove this choice", "compose_form.poll.remove_option": "এই বিকল্পটি মুছে ফেলুন",
"compose_form.publish": "Toot", "compose_form.publish": "টুট",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.marked": "Media is marked as sensitive", "compose_form.sensitive.hide": "Mark media as sensitive",
"compose_form.sensitive.unmarked": "Media is not marked as sensitive", "compose_form.sensitive.marked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়েছে",
"compose_form.spoiler.marked": "Text is hidden behind warning", "compose_form.sensitive.unmarked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়নি",
"compose_form.spoiler.unmarked": "Text is not hidden", "compose_form.spoiler.marked": "লেখাটি সাবধানতার পেছনে লুকানো আছে",
"compose_form.spoiler_placeholder": "Write your warning here", "compose_form.spoiler.unmarked": "লেখাটি লুকানো নেই",
"confirmation_modal.cancel": "Cancel", "compose_form.spoiler_placeholder": "আপনার সাবধানতা এখানে লিখুন",
"confirmations.block.block_and_report": "Block & Report", "confirmation_modal.cancel": "বাতিল করুন",
"confirmations.block.confirm": "Block", "confirmations.block.block_and_report": "বন্ধ করুন এবং রিপোর্ট করুন",
"confirmations.block.message": "Are you sure you want to block {name}?", "confirmations.block.confirm": "বন্ধ করুন",
"confirmations.delete.confirm": "Delete", "confirmations.block.message": "আপনি কি নিশ্চিত {name} কে বন্ধ করতে চান ?",
"confirmations.delete.message": "Are you sure you want to delete this status?", "confirmations.delete.confirm": "মুছে ফেলুন",
"confirmations.delete_list.confirm": "Delete", "confirmations.delete.message": "আপনি কি নিশ্চিত যে এই লেখাটি মুছে ফেলতে চান ?",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.delete_list.confirm": "মুছে ফেলুন",
"confirmations.domain_block.confirm": "Hide entire domain", "confirmations.delete_list.message": "আপনি কি নিশ্চিত যে আপনি এই তালিকাটি স্থায়িভাবে মুছে ফেলতে চান ?",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", "confirmations.domain_block.confirm": "এই ওয়েবসাইট থেকে সব সরান",
"confirmations.mute.confirm": "Mute", "confirmations.domain_block.message": "আপনি কি সত্যি সত্যি নিশ্চিত যে {domain} ওয়েবসাইট থেকে সব সরাতে চান ? সাধারণত কিছু লক্ষ্যবস্তু বন্ধ এবং সরানোযা যথেষ্ট। নিশ্চিত করলে ওই ওয়েবসাইট থেকে কোনোকিছু কোনখানে দেখবেন না। যারা আপনাকে অনুসরণ করে ওই ওয়েবসাইট থেকে তাদেরকেও মুছে ফেলা হবে।",
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.confirm": "সরিয়ে ফেলুন",
"confirmations.redraft.confirm": "Delete & redraft", "confirmations.mute.message": "আপনি কি নিশ্চিত {name} সরিয়ে ফেলতে চান ?",
"confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", "confirmations.redraft.confirm": "মুছে ফেলুন এবং আবার সম্পাদন করুন",
"confirmations.reply.confirm": "Reply", "confirmations.redraft.message": "আপনি কি নিশ্চিত এটি মুছে ফেলে এবং আবার সম্পাদন করতে চান ? এটাতে যা পছন্দিত, সমর্থন বা মতামত আছে সেগুলো নতুন লেখার সাথে যুক্ত থাকবে না।",
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.reply.confirm": "মতামত",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.reply.message": "এখন মতামত লিখতে গেলে আপনার এখন যেটা লিখছেন সেটা মুছে যাবে। আপনি নি নিশ্চিত এটা করতে চান ?",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.confirm": "অনুসরণ বন্ধ করুন",
"embed.instructions": "Embed this status on your website by copying the code below.", "confirmations.unfollow.message": "আপনি কি নিশ্চিত {name} কে আর অনুসরণ করতে চান না ?",
"embed.preview": "Here is what it will look like:", "embed.instructions": "এই লেখাটি আপনার ওয়েবসাইটে যুক্ত করতে নিচের কোডটি বেবহার করুন।",
"emoji_button.activity": "Activity", "embed.preview": "সেটা দেখতে এরকম হবে:",
"emoji_button.custom": "Custom", "emoji_button.activity": "কার্যকলাপ",
"emoji_button.flags": "Flags", "emoji_button.custom": "প্রথা",
"emoji_button.food": "Food & Drink", "emoji_button.flags": "পতাকা",
"emoji_button.label": "Insert emoji", "emoji_button.food": "খাদ্য ও পানীয়",
"emoji_button.nature": "Nature", "emoji_button.label": "এমজি যুক্ত করুন",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", "emoji_button.nature": "প্রকৃতি",
"emoji_button.objects": "Objects", "emoji_button.not_found": "ইমোজি পাওয়া যায়নি !! (╯°□°)╯︵ ┻━┻",
"emoji_button.people": "People", "emoji_button.objects": "বস্তূ",
"emoji_button.recent": "Frequently used", "emoji_button.people": "মানুষ",
"emoji_button.search": "Search...", "emoji_button.recent": "ঘন ব্যাবহৃত",
"emoji_button.search_results": "Search results", "emoji_button.search": "খুজুন...",
"emoji_button.symbols": "Symbols", "emoji_button.search_results": "খোঁজার ফলাফল",
"emoji_button.travel": "Travel & Places", "emoji_button.symbols": "প্রতীক",
"empty_column.account_timeline": "No toots here!", "emoji_button.travel": "ভ্রমণ এবং স্থান",
"empty_column.account_unavailable": "Profile unavailable", "empty_column.account_timeline": "এখানে কোনো টুট নেই!",
"empty_column.blocks": "You haven't blocked any users yet.", "empty_column.account_unavailable": "নিজস্ব পাতা নেই",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.blocks": "আপনি কোনো ব্যবহারকারীদের বন্ধ করেন নি।",
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", "empty_column.community": "স্থানীয় সময়রেখাতে কিছু নেই। প্রকাশ্যভাবে কিছু লিখে লেখালেখির উদ্বোধন করে ফেলুন!",
"empty_column.domain_blocks": "There are no hidden domains yet.", "empty_column.direct": "আপনার কাছে সরাসরি পাঠানো কোনো লেখা নেই। যদি কেও পাঠায়, সেটা এখানে দেখা যাবে।",
"empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", "empty_column.domain_blocks": "এখনো কোনো সরানো ওয়েবসাইট নেই।",
"empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", "empty_column.favourited_statuses": "আপনার পছন্দের কোনো টুট এখনো নেই। আপনি কোনো লেখা পছন্দের হিসেবে চিহ্নিত করলে এখানে পাওয়া যাবে।",
"empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", "empty_column.favourites": "কেও এখনো এটাকে পছন্দের টুট হিসেবে চিহ্নিত করেনি। যদি করে, তখন তাদের এখানে পাওয়া যাবে।",
"empty_column.hashtag": "There is nothing in this hashtag yet.", "empty_column.follow_requests": "আপনার এখনো কোনো অনুসরণের আবেদন পাঠানো নেই। যদি পাঠায়, এখানে পাওয়া যাবে।",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", "empty_column.hashtag": "এই হেসটাগে এখনো কিছু নেই।",
"empty_column.home.public_timeline": "the public timeline", "empty_column.home": "আপনার বাড়ির সময়রেখা এখনো খালি! {public}এ ঘুরে আসুন অথবা অনুসন্ধান বেবহার করে শুরু করতে পারেন এবং অন্য ব্যবহারকারীদের সাথে সাক্ষাৎ করতে পারেন।",
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", "empty_column.home.public_timeline": "প্রকাশ্য সময়রেখা",
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", "empty_column.list": "এই তালিকাতে এখনো কিছু নেই. যখন এই তালিকায় থাকা ব্যবহারকারী নতুন কিছু লিখবে, সেগুলো এখানে পাওয়া যাবে।",
"empty_column.mutes": "You haven't muted any users yet.", "empty_column.lists": "আপনার এখনো কোনো তালিকা তৈরী নেই। যদি বা যখন তৈরী করেন, সেগুলো এখানে পাওয়া যাবে।",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.mutes": "আপনি এখনো কোনো ব্যবহারকারীকে সরাননি।",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", "empty_column.notifications": "আপনার এখনো কোনো প্রজ্ঞাপন নেই। কথোপকথন শুরু করতে, অন্যদের সাথে মেলামেশা করতে পারেন।",
"follow_request.authorize": "Authorize", "empty_column.public": "এখানে এখনো কিছু নেই! প্রকাশ্য ভাবে কিছু লিখুন বা অন্য সার্ভার থেকে কাওকে অনুসরণ করে এই জায়গা ভরে ফেলুন",
"follow_request.reject": "Reject", "follow_request.authorize": "অনুমতি দিন",
"getting_started.developers": "Developers", "follow_request.reject": "প্রত্যাখ্যান করুন",
"getting_started.directory": "Profile directory", "getting_started.developers": "তৈরিকারকদের জন্য",
"getting_started.documentation": "Documentation", "getting_started.directory": "নিজস্ব পাতার তালিকা",
"getting_started.heading": "Getting started", "getting_started.documentation": "নথিপত্র",
"getting_started.invite": "Invite people", "getting_started.heading": "শুরু করা",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", "getting_started.invite": "অন্যদের আমন্ত্রণ করুন",
"getting_started.security": "Security", "getting_started.open_source_notice": "মাস্টাডন একটি মুক্ত সফটওয়্যার। আপনি তৈরিতে সাহায্য করতে পারেন অথবা সমস্যা রিপোর্ট করতে পারেন গিটহাবে {github}।",
"getting_started.terms": "Terms of service", "getting_started.security": "নিরাপত্তা",
"hashtag.column_header.tag_mode.all": "and {additional}", "getting_started.terms": "ব্যবহারের নিয়মাবলী",
"hashtag.column_header.tag_mode.any": "or {additional}", "hashtag.column_header.tag_mode.all": "এবং {additional}",
"hashtag.column_header.tag_mode.none": "without {additional}", "hashtag.column_header.tag_mode.any": "অথবা {additional}",
"hashtag.column_settings.select.no_options_message": "No suggestions found", "hashtag.column_header.tag_mode.none": "বাদ দিয়ে {additional}",
"hashtag.column_settings.select.placeholder": "Enter hashtags…", "hashtag.column_settings.select.no_options_message": "কোনটা পাওয়া যায় নি",
"hashtag.column_settings.tag_mode.all": "All of these", "hashtag.column_settings.select.placeholder": "হ্যাশট্যাগের ভেতরে ঢুকুন…",
"hashtag.column_settings.tag_mode.any": "Any of these", "hashtag.column_settings.tag_mode.all": "এগুলো সব",
"hashtag.column_settings.tag_mode.none": "None of these", "hashtag.column_settings.tag_mode.any": "এর ভেতরে যেকোনোটা",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column", "hashtag.column_settings.tag_mode.none": "এগুলোর একটাও না",
"home.column_settings.basic": "Basic", "hashtag.column_settings.tag_toggle": "আরো ট্যাগ এই কলামে যুক্ত করুন",
"home.column_settings.show_reblogs": "Show boosts", "home.column_settings.basic": "সাধারণ",
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_reblogs": "সমর্থনগুলো দেখান",
"home.column_settings.show_replies": "মতামত দেখান",
"intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.hours": "{number, plural, one {# ঘটা} other {# ঘটা}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
"introduction.federation.action": "Next", "introduction.federation.action": "পরবর্তী",
"introduction.federation.federated.headline": "Federated", "introduction.federation.federated.headline": "যুক্তবিশ্ব",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", "introduction.federation.federated.text": "অন্যান্য যুক্তবিশ্বের সার্ভারের লেখাগুলি যুক্তবিশ্বের সময়রেখাতে আসবে ।",
"introduction.federation.home.headline": "Home", "introduction.federation.home.headline": "বাড়ি",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", "introduction.federation.home.text": "যাদেরকে অনুসরণ করেন তাদের লেখাগুলো আপনার বাড়ি-সময়রেখাতে আসবে। আপনি এখান থেকে যুক্তবিশ্বে যেকোনো সার্ভারের যে কাওকে অনুসরণ করতে পারেন!",
"introduction.federation.local.headline": "Local", "introduction.federation.local.headline": "স্থানীয়",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", "introduction.federation.local.text": "আপনি যে সার্ভারে আছেন সেখানকার মানুষের প্রকাশ্য লেখাগুলো স্থানীয় সময়রেখাতে আসবে।",
"introduction.interactions.action": "Finish toot-orial!", "introduction.interactions.action": "ব্যবহার জানার অংশটি শেষ করুন!",
"introduction.interactions.favourite.headline": "Favourite", "introduction.interactions.favourite.headline": "পছন্দের",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", "introduction.interactions.favourite.text": "পরে পড়ার জন্য বা লেখা পছন্ধ হয়েছে সেটা লেখককে জানাতে, কোনো লেখা পছন্দের হিসেবে চিহ্নিত করতে পারেন।",
"introduction.interactions.reblog.headline": "Boost", "introduction.interactions.reblog.headline": "সমর্থন",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", "introduction.interactions.reblog.text": "কারোর লেখা সমর্থন দিয়ে চিহ্নিত করে সেটা আপনার অনুসরণকারীদের দেখতে পারেন।",
"introduction.interactions.reply.headline": "Reply", "introduction.interactions.reply.headline": "মতামত",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", "introduction.interactions.reply.text": "আপনি অন্যদের এবং নিজের লেখায় মতামত টুট করতে পারেন, যেগুলো লেখার সাথে কথোপকথন হিসেবে যুক্ত থাকবে।",
"introduction.welcome.action": "Let's go!", "introduction.welcome.action": "শুরু করা যাক!",
"introduction.welcome.headline": "First steps", "introduction.welcome.headline": "প্রথম ধাপ",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", "introduction.welcome.text": "যুক্তবিশ্বে স্বাগতম! কিছুক্ষনের মধ্যেই আপনি আপনার লেখা বিভিন্ন সার্ভারে সম্প্রচার করতে পারবেন। কিন্তু মনে রাখবে যে এটা একটা বিশেষ সার্ভার, {domain} কারণ এখানে আপনার নিজেস্ব পাতা রাখা হচ্ছে।",
"keyboard_shortcuts.back": "to navigate back", "keyboard_shortcuts.back": "পেছনে যেতে",
"keyboard_shortcuts.blocked": "to open blocked users list", "keyboard_shortcuts.blocked": "বন্ধ করা ব্যবহারকারীদের তালিকা দেখতে",
"keyboard_shortcuts.boost": "to boost", "keyboard_shortcuts.boost": "সমর্থন করতে",
"keyboard_shortcuts.column": "to focus a status in one of the columns", "keyboard_shortcuts.column": "কোনো কলামএ কোনো লেখা ফোকাস করতে",
"keyboard_shortcuts.compose": "to focus the compose textarea", "keyboard_shortcuts.compose": "লেখা সম্পদনার জায়গায় ফোকাস করতে",
"keyboard_shortcuts.description": "Description", "keyboard_shortcuts.description": "বিবরণ",
"keyboard_shortcuts.direct": "to open direct messages column", "keyboard_shortcuts.direct": "সরাসরি পাঠানো লেখা দেখতে",
"keyboard_shortcuts.down": "to move down in the list", "keyboard_shortcuts.down": "তালিকার ভেতরে নিচে যেতে",
"keyboard_shortcuts.enter": "to open status", "keyboard_shortcuts.enter": "অবস্থা দেখতে",
"keyboard_shortcuts.favourite": "to favourite", "keyboard_shortcuts.favourite": "পছন্দের দেখতে",
"keyboard_shortcuts.favourites": "to open favourites list", "keyboard_shortcuts.favourites": "পছন্দের তালিকা বের করতে",
"keyboard_shortcuts.federated": "to open federated timeline", "keyboard_shortcuts.federated": "যুক্তবিশ্বের সময়রেখাতে যেতে",
"keyboard_shortcuts.heading": "Keyboard Shortcuts", "keyboard_shortcuts.heading": "কিবোর্ডের দ্রুতকারক (শর্টকাট)",
"keyboard_shortcuts.home": "to open home timeline", "keyboard_shortcuts.home": "বাড়ির সময়রেখা খুলতে",
"keyboard_shortcuts.hotkey": "Hotkey", "keyboard_shortcuts.hotkey": "দ্রুতকারক ছবিগুলো",
"keyboard_shortcuts.legend": "to display this legend", "keyboard_shortcuts.legend": "এই প্রদর্শনঅর্থ(legend) দেখতে",
"keyboard_shortcuts.local": "to open local timeline", "keyboard_shortcuts.local": "স্থানীয় সময়রেখাতে যেতে",
"keyboard_shortcuts.mention": "to mention author", "keyboard_shortcuts.mention": "লেখককে উল্লেখ করতে",
"keyboard_shortcuts.muted": "to open muted users list", "keyboard_shortcuts.muted": "বন্ধ করা ব্যবহারকারীদের তালিকা খুলতে",
"keyboard_shortcuts.my_profile": "to open your profile", "keyboard_shortcuts.my_profile": "নিজের পাতা দেখতে",
"keyboard_shortcuts.notifications": "to open notifications column", "keyboard_shortcuts.notifications": "প্রজ্ঞাপনের কলাম খুলতে",
"keyboard_shortcuts.pinned": "to open pinned toots list", "keyboard_shortcuts.pinned": "পিন দেওয়া টুটের তালিকা খুলতে",
"keyboard_shortcuts.profile": "to open author's profile", "keyboard_shortcuts.profile": "লেখকের পাতা দেখতে",
"keyboard_shortcuts.reply": "to reply", "keyboard_shortcuts.reply": "মতামত দিতে",
"keyboard_shortcuts.requests": "to open follow requests list", "keyboard_shortcuts.requests": "অনুসরণ অনুরোধের তালিকা দেখতে",
"keyboard_shortcuts.search": "to focus search", "keyboard_shortcuts.search": "খোঁজার অংশে ফোকাস করতে",
"keyboard_shortcuts.start": "to open \"get started\" column", "keyboard_shortcuts.start": "\"প্রথম শুরুর\" কলাম বের করতে",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "CW লেখা দেখতে বা লুকাতে",
"keyboard_shortcuts.toot": "to start a brand new toot", "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "keyboard_shortcuts.toot": "নতুন একটা টুট লেখা শুরু করতে",
"keyboard_shortcuts.up": "to move up in the list", "keyboard_shortcuts.unfocus": "লেখা বা খোঁজার জায়গায় ফোকাস না করতে",
"lightbox.close": "Close", "keyboard_shortcuts.up": "তালিকার উপরের দিকে যেতে",
"lightbox.next": "Next", "lightbox.close": "বন্ধ",
"lightbox.previous": "Previous", "lightbox.next": "পরবর্তী",
"lists.account.add": "Add to list", "lightbox.previous": "পূর্ববর্তী",
"lists.account.remove": "Remove from list", "lightbox.view_context": "View context",
"lists.delete": "Delete list", "lists.account.add": "তালিকাতে যুক্ত করতে",
"lists.edit": "Edit list", "lists.account.remove": "তালিকা থেকে বাদ দিতে",
"lists.edit.submit": "Change title", "lists.delete": "তালিকা মুছে ফেলতে",
"lists.new.create": "Add list", "lists.edit": "তালিকা সম্পাদনা করতে",
"lists.new.title_placeholder": "New list title", "lists.edit.submit": "শিরোনাম সম্পাদনা করতে",
"lists.search": "Search among people you follow", "lists.new.create": "তালিকাতে যুক্ত করতে",
"lists.subheading": "Your lists", "lists.new.title_placeholder": "তালিকার নতুন শিরোনাম দিতে",
"loading_indicator.label": "Loading...", "lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
"media_gallery.toggle_visible": "Toggle visibility", "lists.subheading": "আপনার তালিকা",
"missing_indicator.label": "Not found", "loading_indicator.label": "আসছে...",
"missing_indicator.sublabel": "This resource could not be found", "media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান",
"mute_modal.hide_notifications": "Hide notifications from this user?", "missing_indicator.label": "খুঁজে পাওয়া যায়নি",
"navigation_bar.apps": "Mobile apps", "missing_indicator.sublabel": "জিনিসটা খুঁজে পাওয়া যায়নি",
"navigation_bar.blocks": "Blocked users", "mute_modal.hide_notifications": "এই ব্যবহারকারীর প্রজ্ঞাপন বন্ধ করবেন ?",
"navigation_bar.community_timeline": "Local timeline", "navigation_bar.apps": "মোবাইলের আপ্প",
"navigation_bar.compose": "Compose new toot", "navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী",
"navigation_bar.direct": "Direct messages", "navigation_bar.community_timeline": "স্থানীয় সময়রেখা",
"navigation_bar.discover": "Discover", "navigation_bar.compose": "নতুন টুট লিখুন",
"navigation_bar.domain_blocks": "Hidden domains", "navigation_bar.direct": "সরাসরি লেখা",
"navigation_bar.edit_profile": "Edit profile", "navigation_bar.discover": "ঘুরে দেখুন",
"navigation_bar.favourites": "Favourites", "navigation_bar.domain_blocks": "বন্ধ করা ওয়েবসাইট",
"navigation_bar.filters": "Muted words", "navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করুন",
"navigation_bar.follow_requests": "Follow requests", "navigation_bar.favourites": "পছন্দের",
"navigation_bar.info": "About this server", "navigation_bar.filters": "বন্ধ করা শব্দ",
"navigation_bar.keyboard_shortcuts": "Hotkeys", "navigation_bar.follow_requests": "অনুসরণের অনুরোধগুলি",
"navigation_bar.lists": "Lists", "navigation_bar.follows_and_followers": "Follows and followers",
"navigation_bar.logout": "Logout", "navigation_bar.info": "এই সার্ভার সম্পর্কে",
"navigation_bar.mutes": "Muted users", "navigation_bar.keyboard_shortcuts": "হটকীগুলি",
"navigation_bar.personal": "Personal", "navigation_bar.lists": "তালিকাগুলো",
"navigation_bar.pins": "Pinned toots", "navigation_bar.logout": "বাইরে যান",
"navigation_bar.preferences": "Preferences", "navigation_bar.mutes": "যেসব বেভহারকারীদের কার্যক্রম বন্ধ করা আছে",
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.personal": "নিজস্ব",
"navigation_bar.security": "Security", "navigation_bar.pins": "পিন দেওয়া টুট",
"notification.favourite": "{name} favourited your status", "navigation_bar.preferences": "পছন্দসমূহ",
"notification.follow": "{name} followed you", "navigation_bar.profile_directory": "Profile directory",
"notification.mention": "{name} mentioned you", "navigation_bar.public_timeline": "যুক্তবিশ্বের সময়রেখা",
"notification.poll": "A poll you have voted in has ended", "navigation_bar.security": "নিরাপত্তা",
"notification.reblog": "{name} boosted your status", "notification.favourite": "{name} আপনার কার্যক্রম পছন্দ করেছেন",
"notifications.clear": "Clear notifications", "notification.follow": "{name} আপনাকে অনুসরণ করেছেন",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", "notification.mention": "{name} আপনাকে উল্লেখ করেছেন",
"notifications.column_settings.alert": "Desktop notifications", "notification.poll": "আপনি ভোট দিয়েছিলেন এমন এক নির্বাচনের ভোটের সময় শেষ হয়েছে",
"notifications.column_settings.favourite": "Favourites:", "notification.reblog": "{name} আপনার কার্যক্রমে সমর্থন দেখিয়েছেন",
"notifications.column_settings.filter_bar.advanced": "Display all categories", "notifications.clear": "প্রজ্ঞাপনগুলো মুছে ফেলতে",
"notifications.column_settings.filter_bar.category": "Quick filter bar", "notifications.clear_confirmation": "আপনি কি নির্চিত প্রজ্ঞাপনগুলো মুছে ফেলতে চান ?",
"notifications.column_settings.filter_bar.show": "Show", "notifications.column_settings.alert": "কম্পিউটারে প্রজ্ঞাপন",
"notifications.column_settings.follow": "New followers:", "notifications.column_settings.favourite": "পছন্দের:",
"notifications.column_settings.mention": "Mentions:", "notifications.column_settings.filter_bar.advanced": "সব শ্রেণীগুলো দেখতে",
"notifications.column_settings.poll": "Poll results:", "notifications.column_settings.filter_bar.category": "দ্রুত ছাঁকনি বার",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.filter_bar.show": "দেখতে",
"notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.follow": "নতুন অনুসরণকারীরা:",
"notifications.column_settings.show": "Show in column", "notifications.column_settings.mention": "প্রজ্ঞাপনগুলো:",
"notifications.column_settings.sound": "Play sound", "notifications.column_settings.poll": "নির্বাচনের ফলাফল:",
"notifications.filter.all": "All", "notifications.column_settings.push": "পুশ প্রজ্ঞাপন",
"notifications.filter.boosts": "Boosts", "notifications.column_settings.reblog": "সমর্থনগুলো:",
"notifications.filter.favourites": "Favourites", "notifications.column_settings.show": "কলামে দেখান",
"notifications.filter.follows": "Follows", "notifications.column_settings.sound": "শব্দ বাজাতে",
"notifications.filter.mentions": "Mentions", "notifications.filter.all": "সব",
"notifications.filter.polls": "Poll results", "notifications.filter.boosts": "সমর্থনগুলো",
"notifications.group": "{count} notifications", "notifications.filter.favourites": "পছন্দের গুলো",
"poll.closed": "Closed", "notifications.filter.follows": "অনুসরণের",
"poll.refresh": "Refresh", "notifications.filter.mentions": "উল্লেখিত",
"poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "notifications.filter.polls": "নির্বাচনের ফলাফল",
"poll.vote": "Vote", "notifications.group": "{count} প্রজ্ঞাপন",
"poll_button.add_poll": "Add a poll", "poll.closed": "বন্ধ",
"poll_button.remove_poll": "Remove poll", "poll.refresh": "আবার সতেজ করতে",
"privacy.change": "Adjust status privacy", "poll.total_votes": "{count, plural, one {# ভোট} other {# ভোট}}",
"privacy.direct.long": "Post to mentioned users only", "poll.vote": "ভোট",
"privacy.direct.short": "Direct", "poll_button.add_poll": "একটা নির্বাচন যোগ করতে",
"privacy.private.long": "Post to followers only", "poll_button.remove_poll": "নির্বাচন বাদ দিতে",
"privacy.private.short": "Followers-only", "privacy.change": "লেখার গোপনীয়তা অবস্থা ঠিক করতে",
"privacy.public.long": "Post to public timelines", "privacy.direct.long": "শুধুমাত্র উল্লেখিত ব্যবহারকারীদের কাছে লিখতে",
"privacy.public.short": "Public", "privacy.direct.short": "সরাসরি",
"privacy.unlisted.long": "Do not show in public timelines", "privacy.private.long": "শুধুমাত্র আপনার অনুসরণকারীদের লিখতে",
"privacy.unlisted.short": "Unlisted", "privacy.private.short": "শুধুমাত্র অনুসরণকারীদের জন্য",
"regeneration_indicator.label": "Loading…", "privacy.public.long": "সর্বজনীন প্রকাশ্য সময়রেখাতে লিখতে",
"regeneration_indicator.sublabel": "Your home feed is being prepared!", "privacy.public.short": "সর্বজনীন প্রকাশ্য",
"relative_time.days": "{number}d", "privacy.unlisted.long": "সর্বজনীন প্রকাশ্য সময়রেখাতে না দেখাতে",
"relative_time.hours": "{number}h", "privacy.unlisted.short": "প্রকাশ্য নয়",
"relative_time.just_now": "now", "regeneration_indicator.label": "আসছে…",
"relative_time.minutes": "{number}m", "regeneration_indicator.sublabel": "আপনার বাড়ির-সময়রেখা প্রস্তূত করা হচ্ছে!",
"relative_time.seconds": "{number}s", "relative_time.days": "{number} দিন",
"reply_indicator.cancel": "Cancel", "relative_time.hours": "{number} ঘন্টা",
"report.forward": "Forward to {target}", "relative_time.just_now": "এখন",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", "relative_time.minutes": "{number}ম",
"report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", "relative_time.seconds": "{number} সেকেন্ড",
"report.placeholder": "Additional comments", "reply_indicator.cancel": "বাতিল করতে",
"report.submit": "Submit", "report.forward": "এটা আরো পাঠান {target} তে",
"report.target": "Report {target}", "report.forward_hint": "এই নিবন্ধনটি অন্য একটি সার্ভারে। অপ্রকাশিতনামাভাবে রিপোর্টের কপি সেখানেও কি পাঠাতে চান ?",
"search.placeholder": "Search", "report.hint": "রিপোর্টটি আপনার সার্ভারের পরিচালকের কাছে পাঠানো হবে। রিপোর্ট পাঠানোর কারণ নিচে বিস্তারিত লিখতে পারেন:",
"search_popout.search_format": "Advanced search format", "report.placeholder": "অন্য কোনো মন্তব্য",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "report.submit": "জমা দিন",
"search_popout.tips.hashtag": "hashtag", "report.target": "{target} রিপোর্ট করুন",
"search_popout.tips.status": "status", "search.placeholder": "খুঁজতে",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.search_format": "বিস্তারিতভাবে খোঁজার পদ্ধতি",
"search_popout.tips.user": "user", "search_popout.tips.full_text": "সাধারণ লেখা দিয়ে খুঁজলে বের হবে সেরকম আপনার লেখা, পছন্দের লেখা, সমর্থন করা লেখা, আপনাকে উল্লেখকরা কোনো লেখা, যা খুঁজছেন সেরকম কোনো ব্যবহারকারীর নাম বা কোনো হ্যাশট্যাগগুলো।",
"search_results.accounts": "People", "search_popout.tips.hashtag": "হ্যাশট্যাগ",
"search_results.hashtags": "Hashtags", "search_popout.tips.status": "লেখা",
"search_results.statuses": "Toots", "search_popout.tips.text": "সাধারণ লেখা দিয়ে খুঁজলে বের হবে সেরকম ব্যবহারকারীর নাম বা কোনো হ্যাশট্যাগগুলো",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}", "search_popout.tips.user": "ব্যবহারকারী",
"status.admin_account": "Open moderation interface for @{name}", "search_results.accounts": "মানুষ",
"status.admin_status": "Open this status in the moderation interface", "search_results.hashtags": "হ্যাশট্যাগগুলি",
"status.block": "Block @{name}", "search_results.statuses": "টুট",
"status.cancel_reblog_private": "Unboost", "search_results.total": "{count, number} {count, plural, one {ফলাফল} other {ফলাফল}}",
"status.cannot_reblog": "This post cannot be boosted", "status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন",
"status.copy": "Copy link to status", "status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন",
"status.delete": "Delete", "status.block": "@{name}কে বন্ধ করুন",
"status.detailed_status": "Detailed conversation view", "status.cancel_reblog_private": "সমর্থন বাতিল করতে",
"status.direct": "Direct message @{name}", "status.cannot_reblog": "এটিতে সমর্থন দেওয়া যাবেনা",
"status.embed": "Embed", "status.copy": "লেখাটির লিংক কপি করতে",
"status.favourite": "Favourite", "status.delete": "মুছে ফেলতে",
"status.filtered": "Filtered", "status.detailed_status": "বিস্তারিত কথোপকথনের হিসেবে দেখতে",
"status.load_more": "Load more", "status.direct": "@{name} কে সরাসরি পাঠান",
"status.media_hidden": "Media hidden", "status.embed": "এমবেড করতে",
"status.mention": "Mention @{name}", "status.favourite": "পছন্দের করতে",
"status.more": "More", "status.filtered": "ছাঁকনিদিত",
"status.mute": "Mute @{name}", "status.load_more": "আরো দেখুন",
"status.mute_conversation": "Mute conversation", "status.media_hidden": "ছবি বা ভিডিও পেছনে",
"status.open": "Expand this status", "status.mention": "@{name}কে উল্লেখ করতে",
"status.pin": "Pin on profile", "status.more": "আরো",
"status.pinned": "Pinned toot", "status.mute": "@{name}র কার্যক্রম সরিয়ে ফেলতে",
"status.read_more": "Read more", "status.mute_conversation": "কথোপকথননের প্রজ্ঞাপন সরিয়ে ফেলতে",
"status.reblog": "Boost", "status.open": "এটার সম্পূর্ণটা দেখতে",
"status.reblog_private": "Boost to original audience", "status.pin": "নিজের পাতায় এটা পিন করতে",
"status.reblogged_by": "{name} boosted", "status.pinned": "পিন করা টুট",
"status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", "status.read_more": "আরো পড়ুন",
"status.redraft": "Delete & re-draft", "status.reblog": "সমর্থন দিতে",
"status.reply": "Reply", "status.reblog_private": "আপনার অনুসরণকারীদের কাছে এটার সমর্থন দেখাতে",
"status.replyAll": "Reply to thread", "status.reblogged_by": "{name} সমর্থন দিয়েছে",
"status.report": "Report @{name}", "status.reblogs.empty": "এখনো কেও এটাতে সমর্থন দেয়নি। যখন কেও দেয়, সেটা তখন এখানে দেখা যাবে।",
"status.sensitive_toggle": "Click to view", "status.redraft": "মুছে আবার নতুন করে লিখতে",
"status.sensitive_warning": "Sensitive content", "status.reply": "মতামত জানাতে",
"status.share": "Share", "status.replyAll": "লেখাযুক্ত সবার কাছে মতামত জানাতে",
"status.show_less": "Show less", "status.report": "@{name}কে রিপোর্ট করতে",
"status.show_less_all": "Show less for all", "status.sensitive_warning": "সংবেদনশীল কিছু",
"status.show_more": "Show more", "status.share": "অন্যদের জানান",
"status.show_more_all": "Show more for all", "status.show_less": "কম দেখতে",
"status.show_thread": "Show thread", "status.show_less_all": "সবগুলোতে কম দেখতে",
"status.unmute_conversation": "Unmute conversation", "status.show_more": "আরো দেখাতে",
"status.unpin": "Unpin from profile", "status.show_more_all": "সবগুলোতে আরো দেখতে",
"suggestions.dismiss": "Dismiss suggestion", "status.show_thread": "আলোচনা দেখতে",
"suggestions.header": "You might be interested in…", "status.unmute_conversation": "আলোচনার প্রজ্ঞাপন চালু করতে",
"tabs_bar.federated_timeline": "Federated", "status.unpin": "নিজের পাতা থেকে পিন করে রাখাটির পিন খুলতে",
"tabs_bar.home": "Home", "suggestions.dismiss": "সাহায্যের জন্য পরামর্শগুলো সরাতে",
"tabs_bar.local_timeline": "Local", "suggestions.header": "আপনি হয়তোবা এগুলোতে আগ্রহী হতে পারেন…",
"tabs_bar.notifications": "Notifications", "tabs_bar.federated_timeline": "যুক্তবিশ্ব",
"tabs_bar.search": "Search", "tabs_bar.home": "বাড়ি",
"time_remaining.days": "{number, plural, one {# day} other {# days}} left", "tabs_bar.local_timeline": "স্থানীয়",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", "tabs_bar.notifications": "প্রজ্ঞাপনগুলো",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "tabs_bar.search": "খুঁজতে",
"time_remaining.moments": "Moments remaining", "time_remaining.days": "{number, plural, one {# day} other {# days}} বাকি আছে",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} বাকি আছে",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} বাকি আছে",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "time_remaining.moments": "সময় বাকি আছে",
"upload_area.title": "Drag & drop to upload", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} বাকি আছে",
"upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} কথা বলছে",
"upload_error.limit": "File upload limit exceeded.", "ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।",
"upload_error.poll": "File upload not allowed with polls.", "upload_area.title": "টেনে এখানে ছেড়ে দিলে এখানে যুক্ত করা যাবে",
"upload_form.description": "Describe for the visually impaired", "upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_form.focus": "Crop", "upload_error.limit": "যা যুক্ত করতে চাচ্ছেন সেটি বেশি বড়, এখানকার সর্বাধিকের মেমোরির উপরে চলে গেছে।",
"upload_form.undo": "Delete", "upload_error.poll": "নির্বাচনক্ষেত্রে কোনো ফাইল যুক্ত করা যাবেনা।",
"upload_progress.label": "Uploading...", "upload_form.description": "যারা দেখতে পায়না তাদের জন্য এটা বর্ণনা করতে",
"video.close": "Close video", "upload_form.focus": "সাধারণ দেখাটি পরিবর্তন করতে",
"video.exit_fullscreen": "Exit full screen", "upload_form.undo": "মুছে ফেলতে",
"video.expand": "Expand video", "upload_progress.label": "যুক্ত করতে পাঠানো হচ্ছে...",
"video.fullscreen": "Full screen", "video.close": "ভিডিওটি বন্ধ করতে",
"video.hide": "Hide video", "video.exit_fullscreen": "পূর্ণ পর্দা থেকে বাইরে বের হতে",
"video.mute": "Mute sound", "video.expand": "ভিডিওটি বড়ো করতে",
"video.pause": "Pause", "video.fullscreen": "পূর্ণ পর্দা করতে",
"video.play": "Play", "video.hide": "ভিডিওটি লুকাতে",
"video.unmute": "Unmute sound" "video.mute": "শব্দ বন্ধ করতে",
"video.pause": "থামাতে",
"video.play": "শুরু করতে",
"video.unmute": "শব্দ চালু করতে"
} }

View File

@ -1,7 +1,7 @@
{ {
"account.add_or_remove_from_list": "Afegir o Treure de les llistes", "account.add_or_remove_from_list": "Afegir o Treure de les llistes",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.block": "Bloca @{name}", "account.block": "Bloqueja @{name}",
"account.block_domain": "Amaga-ho tot de {domain}", "account.block_domain": "Amaga-ho tot de {domain}",
"account.blocked": "Bloquejat", "account.blocked": "Bloquejat",
"account.direct": "Missatge directe @{name}", "account.direct": "Missatge directe @{name}",
@ -11,13 +11,13 @@
"account.follow": "Segueix", "account.follow": "Segueix",
"account.followers": "Seguidors", "account.followers": "Seguidors",
"account.followers.empty": "Encara ningú no segueix aquest usuari.", "account.followers.empty": "Encara ningú no segueix aquest usuari.",
"account.follows": "Seguint", "account.follows": "Seguiments",
"account.follows.empty": "Aquest usuari encara no segueix a ningú.", "account.follows.empty": "Aquest usuari encara no segueix a ningú.",
"account.follows_you": "Et segueix", "account.follows_you": "Et segueix",
"account.hide_reblogs": "Amaga els impulsos de @{name}", "account.hide_reblogs": "Amaga els impulsos de @{name}",
"account.link_verified_on": "La propietat d'aquest enllaç es va verificar el dia {date}", "account.link_verified_on": "La propietat d'aquest enllaç es va verificar el dia {date}",
"account.locked_info": "Aquest estat de privadesa del compte està definit com a bloquejat. El propietari revisa manualment qui pot seguir-lo.", "account.locked_info": "Aquest estat de privadesa del compte està definit com a bloquejat. El propietari revisa manualment qui pot seguir-lo.",
"account.media": "Media", "account.media": "Mèdia",
"account.mention": "Esmentar @{name}", "account.mention": "Esmentar @{name}",
"account.moved_to": "{name} s'ha mogut a:", "account.moved_to": "{name} s'ha mogut a:",
"account.mute": "Silencia @{name}", "account.mute": "Silencia @{name}",
@ -44,7 +44,7 @@
"bundle_modal_error.close": "Tanca", "bundle_modal_error.close": "Tanca",
"bundle_modal_error.message": "S'ha produït un error en carregar aquest component.", "bundle_modal_error.message": "S'ha produït un error en carregar aquest component.",
"bundle_modal_error.retry": "Torna-ho a provar", "bundle_modal_error.retry": "Torna-ho a provar",
"column.blocks": "Usuaris blocats", "column.blocks": "Usuaris bloquejats",
"column.community": "Línia de temps local", "column.community": "Línia de temps local",
"column.direct": "Missatges directes", "column.direct": "Missatges directes",
"column.domain_blocks": "Dominis ocults", "column.domain_blocks": "Dominis ocults",
@ -54,7 +54,7 @@
"column.lists": "Llistes", "column.lists": "Llistes",
"column.mutes": "Usuaris silenciats", "column.mutes": "Usuaris silenciats",
"column.notifications": "Notificacions", "column.notifications": "Notificacions",
"column.pins": "Toot fixat", "column.pins": "Toots fixats",
"column.public": "Línia de temps federada", "column.public": "Línia de temps federada",
"column_back_button.label": "Enrere", "column_back_button.label": "Enrere",
"column_header.hide_settings": "Amaga la configuració", "column_header.hide_settings": "Amaga la configuració",
@ -65,29 +65,30 @@
"column_header.unpin": "No fixis", "column_header.unpin": "No fixis",
"column_subheading.settings": "Configuració", "column_subheading.settings": "Configuració",
"community.column_settings.media_only": "Només multimèdia", "community.column_settings.media_only": "Només multimèdia",
"compose_form.direct_message_warning": "Aquest toot només serà enviat als usuaris esmentats. De totes maneres, els operadors de la teva o de qualsevol de les instàncies receptores poden inspeccionar aquest missatge.", "compose_form.direct_message_warning": "Aquest toot només serà enviat als usuaris esmentats.",
"compose_form.direct_message_warning_learn_more": "Aprèn més", "compose_form.direct_message_warning_learn_more": "Aprèn més",
"compose_form.hashtag_warning": "Aquest toot no es mostrarà en cap etiqueta ja que no està llistat. Només els toots públics poden ser cercats per etiqueta.", "compose_form.hashtag_warning": "Aquest toot no es mostrarà en cap etiqueta ja que no està llistat. Només els toots públics poden ser cercats per etiqueta.",
"compose_form.lock_disclaimer": "El teu compte no està bloquejat {locked}. Tothom pot seguir-te i veure els teus missatges a seguidors.", "compose_form.lock_disclaimer": "El teu compte no està bloquejat {locked}. Tothom pot seguir-te i veure els teus missatges a seguidors.",
"compose_form.lock_disclaimer.lock": "blocat", "compose_form.lock_disclaimer.lock": "bloquejat",
"compose_form.placeholder": "En què estàs pensant?", "compose_form.placeholder": "En què penses?",
"compose_form.poll.add_option": "Afegeix una opció", "compose_form.poll.add_option": "Afegeix una opció",
"compose_form.poll.duration": "Durada de l'enquesta", "compose_form.poll.duration": "Durada de l'enquesta",
"compose_form.poll.option_placeholder": "Opció {number}", "compose_form.poll.option_placeholder": "Opció {number}",
"compose_form.poll.remove_option": "Elimina aquesta opció", "compose_form.poll.remove_option": "Elimina aquesta opció",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Marcar mèdia com a sensible",
"compose_form.sensitive.marked": "Mèdia marcat com a sensible", "compose_form.sensitive.marked": "Mèdia marcat com a sensible",
"compose_form.sensitive.unmarked": "Mèdia no està marcat com a sensible", "compose_form.sensitive.unmarked": "Mèdia no està marcat com a sensible",
"compose_form.spoiler.marked": "Text es ocult sota l'avís", "compose_form.spoiler.marked": "Text es ocult sota l'avís",
"compose_form.spoiler.unmarked": "Text no ocult", "compose_form.spoiler.unmarked": "Text no ocult",
"compose_form.spoiler_placeholder": "Escriu l'avís aquí", "compose_form.spoiler_placeholder": "Escriu l'avís aquí",
"confirmation_modal.cancel": "Cancel·la", "confirmation_modal.cancel": "Cancel·la",
"confirmations.block.block_and_report": "Block & Report", "confirmations.block.block_and_report": "Bloquejar i informar",
"confirmations.block.confirm": "Bloca", "confirmations.block.confirm": "Bloqueja",
"confirmations.block.message": "Estàs segur que vols blocar {name}?", "confirmations.block.message": "Estàs segur que vols bloquejar a {name}?",
"confirmations.delete.confirm": "Suprimeix", "confirmations.delete.confirm": "Suprimeix",
"confirmations.delete.message": "Estàs segur que vols suprimir aquest estat?", "confirmations.delete.message": "Estàs segur que vols suprimir aquest toot?",
"confirmations.delete_list.confirm": "Suprimeix", "confirmations.delete_list.confirm": "Suprimeix",
"confirmations.delete_list.message": "Estàs segur que vols suprimir permanentment aquesta llista?", "confirmations.delete_list.message": "Estàs segur que vols suprimir permanentment aquesta llista?",
"confirmations.domain_block.confirm": "Amaga tot el domini", "confirmations.domain_block.confirm": "Amaga tot el domini",
@ -95,12 +96,12 @@
"confirmations.mute.confirm": "Silencia", "confirmations.mute.confirm": "Silencia",
"confirmations.mute.message": "Estàs segur que vols silenciar {name}?", "confirmations.mute.message": "Estàs segur que vols silenciar {name}?",
"confirmations.redraft.confirm": "Esborrar i refer", "confirmations.redraft.confirm": "Esborrar i refer",
"confirmations.redraft.message": "Estàs segur que vols esborrar aquesta publicació i tornar a redactar-la? Perderàs totes els impulsos i favorits, i les respostes a la publicació original es quedaran orfes.", "confirmations.redraft.message": "Estàs segur que vols esborrar aquest toot i tornar a redactar-lo? Perderàs totes els impulsos i favorits, i les respostes al toot original es quedaran orfes.",
"confirmations.reply.confirm": "Respon", "confirmations.reply.confirm": "Respon",
"confirmations.reply.message": "Responen ara es sobreescriurà el missatge que estàs editant. Estàs segur que vols continuar?", "confirmations.reply.message": "Responen ara es sobreescriurà el missatge que estàs editant. Estàs segur que vols continuar?",
"confirmations.unfollow.confirm": "Deixa de seguir", "confirmations.unfollow.confirm": "Deixa de seguir",
"confirmations.unfollow.message": "Estàs segur que vols deixar de seguir {name}?", "confirmations.unfollow.message": "Estàs segur que vols deixar de seguir {name}?",
"embed.instructions": "Incrusta aquest estat al lloc web copiant el codi a continuació.", "embed.instructions": "Incrusta aquest toot al lloc web copiant el codi a continuació.",
"embed.preview": "Aquí tenim quin aspecte tindrá:", "embed.preview": "Aquí tenim quin aspecte tindrá:",
"emoji_button.activity": "Activitat", "emoji_button.activity": "Activitat",
"emoji_button.custom": "Personalitzat", "emoji_button.custom": "Personalitzat",
@ -117,18 +118,18 @@
"emoji_button.symbols": "Símbols", "emoji_button.symbols": "Símbols",
"emoji_button.travel": "Viatges i Llocs", "emoji_button.travel": "Viatges i Llocs",
"empty_column.account_timeline": "No hi ha toots aquí!", "empty_column.account_timeline": "No hi ha toots aquí!",
"empty_column.account_unavailable": "Profile unavailable", "empty_column.account_unavailable": "Perfil no disponible",
"empty_column.blocks": "Encara no has bloquejat cap usuari.", "empty_column.blocks": "Encara no has bloquejat cap usuari.",
"empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per fer rodar la pilota!", "empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per a fer rodar la pilota!",
"empty_column.direct": "Encara no tens missatges directes. Quan enviïs o rebis un, es mostrarà aquí.", "empty_column.direct": "Encara no tens missatges directes. Quan enviïs o rebis un, es mostrarà aquí.",
"empty_column.domain_blocks": "Encara no hi ha dominis ocults.", "empty_column.domain_blocks": "Encara no hi ha dominis ocults.",
"empty_column.favourited_statuses": "Encara no tens cap toot favorit. Quan en tinguis, apareixerà aquí.", "empty_column.favourited_statuses": "Encara no tens cap toot favorit. Quan en tinguis, apareixerà aquí.",
"empty_column.favourites": "Encara ningú ha marcat aquest toot com a favorit. Quan algú ho faci, apareixera aquí.", "empty_column.favourites": "Encara ningú ha marcat aquest toot com a favorit. Quan algú ho faci, apareixera aquí.",
"empty_column.follow_requests": "Encara no teniu cap petició de seguiment. Quan rebeu una, apareixerà aquí.", "empty_column.follow_requests": "Encara no teniu cap petició de seguiment. Quan rebis una, apareixerà aquí.",
"empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.", "empty_column.hashtag": "Encara no hi ha res en aquesta etiqueta.",
"empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.", "empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
"empty_column.home.public_timeline": "la línia de temps pública", "empty_column.home.public_timeline": "la línia de temps pública",
"empty_column.list": "Encara no hi ha res en aquesta llista. Quan els membres d'aquesta llista publiquin nous estats, apareixeran aquí.", "empty_column.list": "Encara no hi ha res en aquesta llista. Quan els membres d'aquesta llista publiquin nous toots, apareixeran aquí.",
"empty_column.lists": "Encara no tens cap llista. Quan en facis una, apareixerà 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.mutes": "Encara no has silenciat cap usuari.",
"empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.", "empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.",
@ -183,40 +184,42 @@
"keyboard_shortcuts.back": "navegar enrera", "keyboard_shortcuts.back": "navegar enrera",
"keyboard_shortcuts.blocked": "per obrir la llista d'usuaris bloquejats", "keyboard_shortcuts.blocked": "per obrir la llista d'usuaris bloquejats",
"keyboard_shortcuts.boost": "impulsar", "keyboard_shortcuts.boost": "impulsar",
"keyboard_shortcuts.column": "per centrar un estat en una de les columnes", "keyboard_shortcuts.column": "per a centrar un toot en una de les columnes",
"keyboard_shortcuts.compose": "per centrar l'area de composició de text", "keyboard_shortcuts.compose": "per centrar l'area de composició de text",
"keyboard_shortcuts.description": "Descripció", "keyboard_shortcuts.description": "Descripció",
"keyboard_shortcuts.direct": "per obrir la columna de missatges directes", "keyboard_shortcuts.direct": "per obrir la columna de missatges directes",
"keyboard_shortcuts.down": "per baixar en la llista", "keyboard_shortcuts.down": "per baixar en la llista",
"keyboard_shortcuts.enter": "ampliar estat", "keyboard_shortcuts.enter": "ampliar el toot",
"keyboard_shortcuts.favourite": "afavorir", "keyboard_shortcuts.favourite": "afavorir",
"keyboard_shortcuts.favourites": "per obrir la llista de favorits", "keyboard_shortcuts.favourites": "per obrir la llista de favorits",
"keyboard_shortcuts.federated": "per obrir la línia de temps federada", "keyboard_shortcuts.federated": "per obrir la línia de temps federada",
"keyboard_shortcuts.heading": "Dreçeres de teclat", "keyboard_shortcuts.heading": "Dreçeres de teclat",
"keyboard_shortcuts.home": "per obrir la línia de temps Inici", "keyboard_shortcuts.home": "per a obrir la línia de temps Inici",
"keyboard_shortcuts.hotkey": "Tecla d'accés directe", "keyboard_shortcuts.hotkey": "Tecla d'accés directe",
"keyboard_shortcuts.legend": "per a mostrar aquesta llegenda", "keyboard_shortcuts.legend": "per a mostrar aquesta llegenda",
"keyboard_shortcuts.local": "per obrir la línia de temps local", "keyboard_shortcuts.local": "per a obrir la línia de temps local",
"keyboard_shortcuts.mention": "per esmentar l'autor", "keyboard_shortcuts.mention": "per a esmentar l'autor",
"keyboard_shortcuts.muted": "per obrir la llista d'usuaris silenciats", "keyboard_shortcuts.muted": "per a obrir la llista d'usuaris silenciats",
"keyboard_shortcuts.my_profile": "per obrir el teu perfil", "keyboard_shortcuts.my_profile": "per a obrir el teu perfil",
"keyboard_shortcuts.notifications": "per obrir la columna de notificacions", "keyboard_shortcuts.notifications": "per a obrir la columna de notificacions",
"keyboard_shortcuts.pinned": "per obrir la llista de toots fixats", "keyboard_shortcuts.pinned": "per a obrir la llista de toots fixats",
"keyboard_shortcuts.profile": "per obrir el perfil de l'autor", "keyboard_shortcuts.profile": "per a obrir el perfil de l'autor",
"keyboard_shortcuts.reply": "respondre", "keyboard_shortcuts.reply": "respondre",
"keyboard_shortcuts.requests": "per obrir la llista de sol·licituds de seguiment", "keyboard_shortcuts.requests": "per a obrir la llista de sol·licituds de seguiment",
"keyboard_shortcuts.search": "per centrar la cerca", "keyboard_shortcuts.search": "per a centrar la cerca",
"keyboard_shortcuts.start": "per obrir la columna \"Començar\"", "keyboard_shortcuts.start": "per a obrir la columna \"Començar\"",
"keyboard_shortcuts.toggle_hidden": "per a mostrar/amagar text sota CW", "keyboard_shortcuts.toggle_hidden": "per a mostrar/amagar text sota CW",
"keyboard_shortcuts.toggle_sensitivity": "per a mostrar/amagar mèdia",
"keyboard_shortcuts.toot": "per a començar un toot nou de trinca", "keyboard_shortcuts.toot": "per a començar un toot nou de trinca",
"keyboard_shortcuts.unfocus": "descentrar l'area de composició de text/cerca", "keyboard_shortcuts.unfocus": "descentrar l'area de composició de text/cerca",
"keyboard_shortcuts.up": "moure amunt en la llista", "keyboard_shortcuts.up": "moure amunt en la llista",
"lightbox.close": "Tancar", "lightbox.close": "Tancar",
"lightbox.next": "Següent", "lightbox.next": "Següent",
"lightbox.previous": "Anterior", "lightbox.previous": "Anterior",
"lightbox.view_context": "Veure el context",
"lists.account.add": "Afegir a la llista", "lists.account.add": "Afegir a la llista",
"lists.account.remove": "Treure de la llista", "lists.account.remove": "Treure de la llista",
"lists.delete": "Delete list", "lists.delete": "Esborrar llista",
"lists.edit": "Editar llista", "lists.edit": "Editar llista",
"lists.edit.submit": "Canvi de títol", "lists.edit.submit": "Canvi de títol",
"lists.new.create": "Afegir llista", "lists.new.create": "Afegir llista",
@ -228,7 +231,7 @@
"missing_indicator.label": "No trobat", "missing_indicator.label": "No trobat",
"missing_indicator.sublabel": "Aquest recurs no pot ser trobat", "missing_indicator.sublabel": "Aquest recurs no pot ser trobat",
"mute_modal.hide_notifications": "Amagar notificacions d'aquest usuari?", "mute_modal.hide_notifications": "Amagar notificacions d'aquest usuari?",
"navigation_bar.apps": "Apps Mòbils", "navigation_bar.apps": "Apps mòbils",
"navigation_bar.blocks": "Usuaris bloquejats", "navigation_bar.blocks": "Usuaris bloquejats",
"navigation_bar.community_timeline": "Línia de temps Local", "navigation_bar.community_timeline": "Línia de temps Local",
"navigation_bar.compose": "Redacta nou toot", "navigation_bar.compose": "Redacta nou toot",
@ -239,6 +242,7 @@
"navigation_bar.favourites": "Favorits", "navigation_bar.favourites": "Favorits",
"navigation_bar.filters": "Paraules silenciades", "navigation_bar.filters": "Paraules silenciades",
"navigation_bar.follow_requests": "Sol·licituds de seguiment", "navigation_bar.follow_requests": "Sol·licituds de seguiment",
"navigation_bar.follows_and_followers": "Seguits i seguidors",
"navigation_bar.info": "Sobre aquest servidor", "navigation_bar.info": "Sobre aquest servidor",
"navigation_bar.keyboard_shortcuts": "Dreceres de teclat", "navigation_bar.keyboard_shortcuts": "Dreceres de teclat",
"navigation_bar.lists": "Llistes", "navigation_bar.lists": "Llistes",
@ -247,6 +251,7 @@
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Personal",
"navigation_bar.pins": "Toots fixats", "navigation_bar.pins": "Toots fixats",
"navigation_bar.preferences": "Preferències", "navigation_bar.preferences": "Preferències",
"navigation_bar.profile_directory": "Directori de perfils",
"navigation_bar.public_timeline": "Línia de temps federada", "navigation_bar.public_timeline": "Línia de temps federada",
"navigation_bar.security": "Seguretat", "navigation_bar.security": "Seguretat",
"notification.favourite": "{name} ha afavorit el teu estat", "notification.favourite": "{name} ha afavorit el teu estat",
@ -303,24 +308,24 @@
"report.hint": "El informe s'enviarà als moderadors del teu servidor. Pots explicar perquè vols informar d'aquest compte aquí:", "report.hint": "El informe s'enviarà als moderadors del teu servidor. Pots explicar perquè vols informar d'aquest compte aquí:",
"report.placeholder": "Comentaris addicionals", "report.placeholder": "Comentaris addicionals",
"report.submit": "Enviar", "report.submit": "Enviar",
"report.target": "Informes", "report.target": "Informes {target}",
"search.placeholder": "Cercar", "search.placeholder": "Cercar",
"search_popout.search_format": "Format de cerca avançada", "search_popout.search_format": "Format de cerca avançada",
"search_popout.tips.full_text": "Text simple recupera publicacions que has escrit, les marcades com a favorites, les impulsades o en les que has estat esmentat, així com usuaris, noms d'usuari i etiquetes.", "search_popout.tips.full_text": "Text simple recupera publicacions que has escrit, les marcades com a favorites, les impulsades o en les que has estat esmentat, així com usuaris, noms d'usuari i etiquetes.",
"search_popout.tips.hashtag": "etiqueta", "search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "status", "search_popout.tips.status": "estat",
"search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i les etiquetes", "search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i les etiquetes",
"search_popout.tips.user": "usuari", "search_popout.tips.user": "usuari",
"search_results.accounts": "Gent", "search_results.accounts": "Gent",
"search_results.hashtags": "Etiquetes", "search_results.hashtags": "Etiquetes",
"search_results.statuses": "Toots", "search_results.statuses": "Toots",
"search_results.total": "{count, number} {count, plural, un {result} altres {results}}", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
"status.admin_account": "Obre l'interfície de moderació per a @{name}", "status.admin_account": "Obre l'interfície de moderació per a @{name}",
"status.admin_status": "Obre aquest estat a la interfície de moderació", "status.admin_status": "Obre aquest toot a la interfície de moderació",
"status.block": "Bloqueja @{name}", "status.block": "Bloqueja @{name}",
"status.cancel_reblog_private": "Desfer l'impuls", "status.cancel_reblog_private": "Desfer l'impuls",
"status.cannot_reblog": "Aquesta publicació no pot ser impulsada", "status.cannot_reblog": "Aquesta publicació no pot ser impulsada",
"status.copy": "Copia l'enllaç a l'estat", "status.copy": "Copia l'enllaç al toot",
"status.delete": "Esborrar", "status.delete": "Esborrar",
"status.detailed_status": "Visualització detallada de la conversa", "status.detailed_status": "Visualització detallada de la conversa",
"status.direct": "Missatge directe @{name}", "status.direct": "Missatge directe @{name}",
@ -346,7 +351,6 @@
"status.reply": "Respondre", "status.reply": "Respondre",
"status.replyAll": "Respondre al tema", "status.replyAll": "Respondre al tema",
"status.report": "Informar sobre @{name}", "status.report": "Informar sobre @{name}",
"status.sensitive_toggle": "Clic per veure",
"status.sensitive_warning": "Contingut sensible", "status.sensitive_warning": "Contingut sensible",
"status.share": "Compartir", "status.share": "Compartir",
"status.show_less": "Mostra menys", "status.show_less": "Mostra menys",
@ -368,12 +372,12 @@
"time_remaining.minutes": "{number, plural, one {# minut} other {# minuts}} restants", "time_remaining.minutes": "{number, plural, one {# minut} other {# minuts}} restants",
"time_remaining.moments": "Moments restants", "time_remaining.moments": "Moments restants",
"time_remaining.seconds": "{number, plural, one {# segon} other {# segons}} restants", "time_remaining.seconds": "{number, plural, one {# segon} other {# segons}} restants",
"trends.count_by_accounts": "{count} {rawCount, plural, una {person} altres {people}} parlant", "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {gent}} talking",
"ui.beforeunload": "El vostre esborrany es perdrà si sortiu de Mastodon.", "ui.beforeunload": "El teu esborrany es perdrà si surts de Mastodon.",
"upload_area.title": "Arrossega i deixa anar per carregar", "upload_area.title": "Arrossega i deixa anar per a carregar",
"upload_button.label": "Afegir multimèdia (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_button.label": "Afegir multimèdia (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "S'ha superat el límit de càrrega d'arxius.", "upload_error.limit": "S'ha superat el límit de càrrega d'arxius.",
"upload_error.poll": "No es permet l'enviament de fitxers amb les enquestes.", "upload_error.poll": "No es permet l'enviament de fitxers en les enquestes.",
"upload_form.description": "Descriure els problemes visuals", "upload_form.description": "Descriure els problemes visuals",
"upload_form.focus": "Modificar la previsualització", "upload_form.focus": "Modificar la previsualització",
"upload_form.undo": "Esborra", "upload_form.undo": "Esborra",

View File

@ -77,6 +77,7 @@
"compose_form.poll.remove_option": "Toglie sta scelta", "compose_form.poll.remove_option": "Toglie sta scelta",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Indicà u media cum'è sensibile",
"compose_form.sensitive.marked": "Media indicatu cum'è sensibile", "compose_form.sensitive.marked": "Media indicatu cum'è sensibile",
"compose_form.sensitive.unmarked": "Media micca indicatu cum'è sensibile", "compose_form.sensitive.unmarked": "Media micca indicatu cum'è sensibile",
"compose_form.spoiler.marked": "Testu piattatu daret'à un'avertimentu", "compose_form.spoiler.marked": "Testu piattatu daret'à un'avertimentu",
@ -165,7 +166,7 @@
"intervals.full.minutes": "{number, plural, one {# minuta} other {# minute}}", "intervals.full.minutes": "{number, plural, one {# minuta} other {# minute}}",
"introduction.federation.action": "Cuntinuà", "introduction.federation.action": "Cuntinuà",
"introduction.federation.federated.headline": "Federata", "introduction.federation.federated.headline": "Federata",
"introduction.federation.federated.text": "I statuti pubblichi da l'altri servori di u fediverse saranu mustrati nant'à a linea pubblica federata.", "introduction.federation.federated.text": "I statuti pubblichi da l'altri servori di u fediverse saranu mustrati nant'à a linea pubblica glubale.",
"introduction.federation.home.headline": "Accolta", "introduction.federation.home.headline": "Accolta",
"introduction.federation.home.text": "I statuti da a ghjente che vo siguitate saranu affissati nant'à a linea d'accolta. Pudete seguità qualvogliasia nant'à tutti i servori!", "introduction.federation.home.text": "I statuti da a ghjente che vo siguitate saranu affissati nant'à a linea d'accolta. Pudete seguità qualvogliasia nant'à tutti i servori!",
"introduction.federation.local.headline": "Lucale", "introduction.federation.local.headline": "Lucale",
@ -191,7 +192,7 @@
"keyboard_shortcuts.enter": "apre u statutu", "keyboard_shortcuts.enter": "apre u statutu",
"keyboard_shortcuts.favourite": "aghjunghje à i favuriti", "keyboard_shortcuts.favourite": "aghjunghje à i favuriti",
"keyboard_shortcuts.favourites": "per apre a lista di i favuriti", "keyboard_shortcuts.favourites": "per apre a lista di i favuriti",
"keyboard_shortcuts.federated": "per apre a linea pubblica federata", "keyboard_shortcuts.federated": "per apre a linea pubblica glubale",
"keyboard_shortcuts.heading": "Accorte cù a tastera", "keyboard_shortcuts.heading": "Accorte cù a tastera",
"keyboard_shortcuts.home": "per apre a linea d'accolta", "keyboard_shortcuts.home": "per apre a linea d'accolta",
"keyboard_shortcuts.hotkey": "Accorta", "keyboard_shortcuts.hotkey": "Accorta",
@ -208,12 +209,14 @@
"keyboard_shortcuts.search": "fucalizà nant'à l'area di circata", "keyboard_shortcuts.search": "fucalizà nant'à l'area di circata",
"keyboard_shortcuts.start": "per apre a culonna \"per principià\"", "keyboard_shortcuts.start": "per apre a culonna \"per principià\"",
"keyboard_shortcuts.toggle_hidden": "vede/piattà u testu daretu à l'avertimentu CW", "keyboard_shortcuts.toggle_hidden": "vede/piattà u testu daretu à l'avertimentu CW",
"keyboard_shortcuts.toggle_sensitivity": "vede/piattà i media",
"keyboard_shortcuts.toot": "scrive un novu statutu", "keyboard_shortcuts.toot": "scrive un novu statutu",
"keyboard_shortcuts.unfocus": "ùn fucalizà più l'area di testu", "keyboard_shortcuts.unfocus": "ùn fucalizà più l'area di testu",
"keyboard_shortcuts.up": "cullà indè a lista", "keyboard_shortcuts.up": "cullà indè a lista",
"lightbox.close": "Chjudà", "lightbox.close": "Chjudà",
"lightbox.next": "Siguente", "lightbox.next": "Siguente",
"lightbox.previous": "Pricidente", "lightbox.previous": "Pricidente",
"lightbox.view_context": "Vede u cuntestu",
"lists.account.add": "Aghjunghje à a lista", "lists.account.add": "Aghjunghje à a lista",
"lists.account.remove": "Toglie di a lista", "lists.account.remove": "Toglie di a lista",
"lists.delete": "Supprime a lista", "lists.delete": "Supprime a lista",
@ -239,6 +242,7 @@
"navigation_bar.favourites": "Favuriti", "navigation_bar.favourites": "Favuriti",
"navigation_bar.filters": "Parolle silenzate", "navigation_bar.filters": "Parolle silenzate",
"navigation_bar.follow_requests": "Dumande d'abbunamentu", "navigation_bar.follow_requests": "Dumande d'abbunamentu",
"navigation_bar.follows_and_followers": "Abbunati è abbunamenti",
"navigation_bar.info": "À prupositu di u servore", "navigation_bar.info": "À prupositu di u servore",
"navigation_bar.keyboard_shortcuts": "Accorte cù a tastera", "navigation_bar.keyboard_shortcuts": "Accorte cù a tastera",
"navigation_bar.lists": "Liste", "navigation_bar.lists": "Liste",
@ -247,6 +251,7 @@
"navigation_bar.personal": "Persunale", "navigation_bar.personal": "Persunale",
"navigation_bar.pins": "Statuti puntarulati", "navigation_bar.pins": "Statuti puntarulati",
"navigation_bar.preferences": "Preferenze", "navigation_bar.preferences": "Preferenze",
"navigation_bar.profile_directory": "Annuariu di i prufili",
"navigation_bar.public_timeline": "Linea pubblica glubale", "navigation_bar.public_timeline": "Linea pubblica glubale",
"navigation_bar.security": "Sicurità", "navigation_bar.security": "Sicurità",
"notification.favourite": "{name} hà aghjuntu u vostru statutu à i so favuriti", "notification.favourite": "{name} hà aghjuntu u vostru statutu à i so favuriti",
@ -286,14 +291,14 @@
"privacy.direct.short": "Direttu", "privacy.direct.short": "Direttu",
"privacy.private.long": "Mustrà solu à l'abbunati", "privacy.private.long": "Mustrà solu à l'abbunati",
"privacy.private.short": "Privatu", "privacy.private.short": "Privatu",
"privacy.public.long": "Mustrà à tuttu u mondu nant'a linea pubblica", "privacy.public.long": "Mustrà à tuttu u mondu nant'à e linee pubbliche",
"privacy.public.short": "Pubblicu", "privacy.public.short": "Pubblicu",
"privacy.unlisted.long": "Ùn mette micca nant'a linea pubblica (ma tutt'u mondu pò vede u statutu nant'à u vostru prufile)", "privacy.unlisted.long": "Ùn mette micca nant'à e linee pubbliche",
"privacy.unlisted.short": "Micca listatu", "privacy.unlisted.short": "Micca listatu",
"regeneration_indicator.label": "Caricamentu…", "regeneration_indicator.label": "Caricamentu…",
"regeneration_indicator.sublabel": "Priparazione di a vostra pagina d'accolta!", "regeneration_indicator.sublabel": "Priparazione di a vostra pagina d'accolta!",
"relative_time.days": "{number}d", "relative_time.days": "{number}ghj",
"relative_time.hours": "{number}h", "relative_time.hours": "{number}o",
"relative_time.just_now": "avà", "relative_time.just_now": "avà",
"relative_time.minutes": "{number}m", "relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s", "relative_time.seconds": "{number}s",
@ -346,7 +351,6 @@
"status.reply": "Risponde", "status.reply": "Risponde",
"status.replyAll": "Risponde à tutti", "status.replyAll": "Risponde à tutti",
"status.report": "Palisà @{name}", "status.report": "Palisà @{name}",
"status.sensitive_toggle": "Cliccate per vede",
"status.sensitive_warning": "Cuntinutu sensibile", "status.sensitive_warning": "Cuntinutu sensibile",
"status.share": "Sparte", "status.share": "Sparte",
"status.show_less": "Ripiegà", "status.show_less": "Ripiegà",

Some files were not shown because too many files have changed in this diff Show More