Merge remote-tracking branch 'neatchee/feat/emoji_reactions'

This commit is contained in:
Erin Shepherd 2023-02-28 00:39:43 +01:00
commit 67687000be
1024 changed files with 18711 additions and 6730 deletions

View file

@ -15,6 +15,12 @@
"webben.browserslist" "webben.browserslist"
], ],
"features": {
"ghcr.io/devcontainers/features/sshd:1": {
"version": "latest"
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or the host. // This can be used to network with other containers or the host.
"forwardPorts": [3000, 4000], "forwardPorts": [3000, 4000],

View file

@ -1,6 +1,10 @@
module.exports = { module.exports = {
root: true, root: true,
extends: [
'eslint:recommended',
],
env: { env: {
browser: true, browser: true,
node: true, node: true,
@ -64,8 +68,8 @@ module.exports = {
eqeqeq: 'error', eqeqeq: 'error',
indent: ['warn', 2], indent: ['warn', 2],
'jsx-quotes': ['error', 'prefer-single'], 'jsx-quotes': ['error', 'prefer-single'],
'no-case-declarations': 'off',
'no-catch-shadow': 'error', 'no-catch-shadow': 'error',
'no-cond-assign': 'error',
'no-console': [ 'no-console': [
'warn', 'warn',
{ {
@ -75,18 +79,16 @@ module.exports = {
], ],
}, },
], ],
'no-fallthrough': 'error', 'no-empty': 'off',
'no-irregular-whitespace': 'error',
'no-mixed-spaces-and-tabs': 'warn',
'no-nested-ternary': 'warn', 'no-nested-ternary': 'warn',
'no-prototype-builtins': 'off',
'no-restricted-properties': [ 'no-restricted-properties': [
'error', 'error',
{ property: 'substring', message: 'Use .slice instead of .substring.' }, { property: 'substring', message: 'Use .slice instead of .substring.' },
{ property: 'substr', message: 'Use .slice instead of .substr.' }, { property: 'substr', message: 'Use .slice instead of .substr.' },
], ],
'no-self-assign': 'off',
'no-trailing-spaces': 'warn', 'no-trailing-spaces': 'warn',
'no-undef': 'error',
'no-unreachable': 'error',
'no-unused-expressions': 'error', 'no-unused-expressions': 'error',
'no-unused-vars': [ 'no-unused-vars': [
'error', 'error',
@ -96,6 +98,7 @@ module.exports = {
ignoreRestSiblings: true, ignoreRestSiblings: true,
}, },
], ],
'no-useless-escape': 'off',
'object-curly-spacing': ['error', 'always'], 'object-curly-spacing': ['error', 'always'],
'padded-blocks': [ 'padded-blocks': [
'error', 'error',
@ -105,7 +108,6 @@ module.exports = {
], ],
quotes: ['error', 'single'], quotes: ['error', 'single'],
semi: 'error', semi: 'error',
strict: 'off',
'valid-typeof': 'error', 'valid-typeof': 'error',
'react/jsx-boolean-value': 'error', 'react/jsx-boolean-value': 'error',

View file

@ -1,11 +1,11 @@
name: "CodeQL" name: 'CodeQL'
on: on:
push: push:
branches: [ "main" ] branches: ['main']
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ "main" ] branches: ['main']
schedule: schedule:
- cron: '22 6 * * 1' - cron: '22 6 * * 1'
@ -21,43 +21,42 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'javascript', 'ruby' ] language: ['javascript', 'ruby']
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file. # By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality # queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # Command-line programs to run using the OS shell.
# If this step fails, then you should remove it and run the build manually (see below) # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # If the Autobuild fails above, remove it and uncomment the following three lines.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# If the Autobuild fails above, remove it and uncomment the following three lines. # - run: |
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
# - run: | - name: Perform CodeQL Analysis
# echo "Run, Build Application using script" uses: github/codeql-action/analyze@v2
# ./location_of_script_within_repo/buildscript.sh with:
category: '/language:${{matrix.language}}'
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

38
.github/workflows/lint-json.yml vendored Normal file
View file

@ -0,0 +1,38 @@
name: JSON Linting
on:
push:
branches-ignore:
- 'dependabot/**'
paths:
- 'package.json'
- 'yarn.lock'
- '.prettier*'
- '**/*.json'
- '.github/workflows/lint-json.yml'
pull_request:
paths:
- 'package.json'
- 'yarn.lock'
- '.prettier*'
- '**/*.json'
- '.github/workflows/lint-json.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
- name: Install all yarn packages
run: yarn --frozen-lockfile
- name: Prettier
run: yarn prettier --check "**/*.json"

40
.github/workflows/lint-yml.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: YML Linting
on:
push:
branches-ignore:
- 'dependabot/**'
paths:
- 'package.json'
- 'yarn.lock'
- '.prettier*'
- '**/*.yaml'
- '**/*.yml'
- '.github/workflows/lint-yml.yml'
pull_request:
paths:
- 'package.json'
- 'yarn.lock'
- '.prettier*'
- '**/*.yaml'
- '**/*.yml'
- '.github/workflows/lint-yml.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
- name: Install all yarn packages
run: yarn --frozen-lockfile
- name: Prettier
run: yarn prettier --check "**/*.{yml,yaml}"

View file

@ -57,8 +57,6 @@ jobs:
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Check prettier formatting
run: yarn format-check
- name: Set-up RuboCop Problem Mathcher - name: Set-up RuboCop Problem Mathcher
uses: r7kamura/rubocop-problem-matchers-action@v1 uses: r7kamura/rubocop-problem-matchers-action@v1
- name: Set-up Stylelint Problem Matcher - name: Set-up Stylelint Problem Matcher

View file

@ -70,3 +70,10 @@ docker-compose.override.yml
# Ignore locale files # Ignore locale files
/app/javascript/mastodon/locales /app/javascript/mastodon/locales
/config/locales /config/locales
# Ignore glitch-soc locale files
/app/javascript/flavours/glitch/locales
/config/locales-glitch
# Ignore glitch-soc emoji map file
/app/javascript/flavours/glitch/features/emoji/emoji_map.json

View file

@ -250,7 +250,7 @@ Metrics/ModuleLength:
Metrics/ParameterLists: Metrics/ParameterLists:
Max: 5 # RuboCop default 5 Max: 5 # RuboCop default 5
CountKeywordArgs: true # RuboCop default true CountKeywordArgs: true # RuboCop default true
MaxOptionalParameters: 3 # RuboCop default 3 MaxOptionalParameters: 3 # RuboCop default 3
Exclude: Exclude:
- app/models/concerns/account_interactions.rb - app/models/concerns/account_interactions.rb

View file

@ -3,6 +3,182 @@ 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.
## [4.1.0] - UNRELEASED
### Added
- **Add support for importing/exporting server-wide domain blocks** ([enbylenore](https://github.com/mastodon/mastodon/pull/20597), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21471), [dariusk](https://github.com/mastodon/mastodon/pull/22803), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21470))
- Add listing of followed hashtags ([connorshea](https://github.com/mastodon/mastodon/pull/21773))
- Add support for editing media description and focus point of already-sent posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20878))
- Add follow request banner on account header ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20785))
- Add confirmation screen when handling reports ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22375), [Gargron](https://github.com/mastodon/mastodon/pull/23156))
- Add option to make the landing page be `/about` even when trends are enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20808))
- Add `noindex` setting back to the admin interface ([prplecake](https://github.com/mastodon/mastodon/pull/22205))
- Add instance peers API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22810))
- Add instance activity API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22833))
- Add `account.approved` webhook ([Saiv46](https://github.com/mastodon/mastodon/pull/22938))
- Add 12 hours option to polls ([Pleclown](https://github.com/mastodon/mastodon/pull/21131))
- Add dropdown menu item to open admin interface for remote domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21895))
- Add `--remove-headers`, `--prune-profiles` and `--include-follows` flags to `tootctl media remove` ([evanphilip](https://github.com/mastodon/mastodon/pull/22149))
- Add `--email` and `--dry-run` options to `tootctl accounts delete` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22328))
- Add `tootctl accounts migrate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22330))
- Add `tootctl accounts prune` ([tribela](https://github.com/mastodon/mastodon/pull/18397))
- Add `tootctl domains purge` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22063))
- Add `SIDEKIQ_CONCURRENCY` environment variable ([muffinista](https://github.com/mastodon/mastodon/pull/19589))
- Add `MIN_THREADS` environment variable to set minimum Puma threads ([jimeh](https://github.com/mastodon/mastodon/pull/21048))
- Add explanation text to log-in page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20946))
- Add user profile OpenGraph tag on post pages ([bramus](https://github.com/mastodon/mastodon/pull/21423))
- Add maskable icon support for Android ([workeffortwaste](https://github.com/mastodon/mastodon/pull/20904))
- Add Belarusian to supported languages ([Mixaill](https://github.com/mastodon/mastodon/pull/22022))
- Add Western Frisian to supported languages ([ykzts](https://github.com/mastodon/mastodon/pull/18602))
- Add Montenegrin to the language picker ([ayefries](https://github.com/mastodon/mastodon/pull/21013))
- Add Southern Sami and Lule Sami to the language picker ([Jullan-M](https://github.com/mastodon/mastodon/pull/21262))
- Add logging for Rails cache timeouts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21667))
- Add color highlight for active hashtag “follow” button ([MFTabriz](https://github.com/mastodon/mastodon/pull/21629))
- Add brotli compression to `assets:precompile` ([Izorkin](https://github.com/mastodon/mastodon/pull/19025))
- Add “disabled” account filter to the `/admin/accounts` UI ([tribela](https://github.com/mastodon/mastodon/pull/21282))
- Add transparency to modal background for accessibility ([edent](https://github.com/mastodon/mastodon/pull/18081))
- Add `title` attribute to video elements in media attachments ([bramus](https://github.com/mastodon/mastodon/pull/21420))
- Add left and right margins to emojis ([dsblank](https://github.com/mastodon/mastodon/pull/20464))
- Add `reading:autoplay:gifs` to `/api/v1/preferences` ([j-f1](https://github.com/mastodon/mastodon/pull/22706))
- Add `hide_collections` parameter to `/api/v1/accounts/credentials` ([CarlSchwan](https://github.com/mastodon/mastodon/pull/22790))
- Add more specific error messages to HTTP signature verification ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21617))
- Add Storj DCS to cloud object storage options in the `mastodon:setup` rake task ([jtolio](https://github.com/mastodon/mastodon/pull/21929))
- Add checkmark symbol in the checkbox for sensitive media ([sidp](https://github.com/mastodon/mastodon/pull/22795))
- Add missing accessibility attributes to logout link in modals ([kytta](https://github.com/mastodon/mastodon/pull/22549))
- Add missing accessibility attributes to “Hide image” button in `MediaGallery` ([hs4man21](https://github.com/mastodon/mastodon/pull/22513))
- Add missing accessibility attributes to hide content warning field when disabled ([hs4man21](https://github.com/mastodon/mastodon/pull/22568))
- Add `aria-hidden` to footer circle dividers to improve accessibility ([hs4man21](https://github.com/mastodon/mastodon/pull/22576))
### Changed
- **Ensure exact match is the first result in hashtag searches** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21315))
- Change account search to return followed accounts first ([dariusk](https://github.com/mastodon/mastodon/pull/22956))
- Change batch account suspension to create a strike ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20897))
- Change default reply language to match the default language when replying to a translated post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22272))
- Change misleading wording about waitlists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20850))
- Increase width of the unread notification border ([connorshea](https://github.com/mastodon/mastodon/pull/21692))
- Change new post notification button on profiles to make it more apparent when it is enabled ([tribela](https://github.com/mastodon/mastodon/pull/22541))
- Change trending tags admin interface to always show batch action controls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23013))
- Change wording of some OAuth scope descriptions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22491))
- Change wording of admin report handling actions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18388))
- Change confirm prompts for relationships management ([tribela](https://github.com/mastodon/mastodon/pull/19411))
- Change language surrounding disability in prompts for media descriptions ([hs4man21](https://github.com/mastodon/mastodon/pull/20923))
- Change confusing wording in the sign in banner ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22490))
- Change account moderation notes to make links clickable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22553))
- Save avatar or header correctly even if the other one fails ([tribela](https://github.com/mastodon/mastodon/pull/18465))
- Change `referrer-policy` to `same-origin` application-wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23014), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23037))
- Add 'private' to `Cache-Control`, match Rails expectations ([daxtens](https://github.com/mastodon/mastodon/pull/20608))
- Make the button that expands the compose form differentiable from the button that publishes a post ([Tak](https://github.com/mastodon/mastodon/pull/20864))
- Change automatic post deletion configuration to be accessible to moved users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20774))
- Make tag following idempotent ([trwnh](https://github.com/mastodon/mastodon/pull/20860), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21285))
- Use buildx functions for faster builds ([inductor](https://github.com/mastodon/mastodon/pull/20692))
- Split off Dockerfile components for faster builds ([moritzheiber](https://github.com/mastodon/mastodon/pull/20933), [ineffyble](https://github.com/mastodon/mastodon/pull/20948), [BtbN](https://github.com/mastodon/mastodon/pull/21028))
- Change last occurrence of “silence” to “limit” in UI text ([cincodenada](https://github.com/mastodon/mastodon/pull/20637))
- Change “hide toot” to “hide post” ([seanthegeek](https://github.com/mastodon/mastodon/pull/22385))
- Don't allow URLs that contain non-normalized paths to be verified ([dgl](https://github.com/mastodon/mastodon/pull/20999))
- Change the “Trending now” header to be a link to the Explore page ([connorshea](https://github.com/mastodon/mastodon/pull/21759))
- Change PostgreSQL connection timeout from 2 minutes to 15 seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21790))
- Make handle more easily selectable on profile page ([cadars](https://github.com/mastodon/mastodon/pull/21479))
- Allow admins to refresh remotely-suspended accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22327))
- Change dropdown menu to contain “Copy link to post” even for non-public posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21316))
- Allow adding relays in secure mode and limited federation mode ([ineffyble](https://github.com/mastodon/mastodon/pull/22324))
- Change timestamps to be displayed using the user's timezone throughout the moderation interface ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21878), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22555))
- Change CSP directives on API to be tight and concise ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20960))
- Change web UI to not autofocus the compose form ([raboof](https://github.com/mastodon/mastodon/pull/16517))
- Change idempotency key handling for posting when database access is slow ([lambda](https://github.com/mastodon/mastodon/pull/21840))
- Change remote media files to be downloaded outside of transactions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21796))
- Improve contrast of charts in “poll has ended” notifications ([j-f1](https://github.com/mastodon/mastodon/pull/22575))
- Change OEmbed detection and validation to be somewhat more lenient ([ineffyble](https://github.com/mastodon/mastodon/pull/22533))
- Widen ElasticSearch version detection to not display a warning for OpenSearch ([VyrCossont](https://github.com/mastodon/mastodon/pull/22422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23064))
- Change link verification to allow pages larger than 1MB as long as the link is in the first 1MB ([untitaker](https://github.com/mastodon/mastodon/pull/22879))
- Update default Node.js version to Node.js 16 ([ineffyble](https://github.com/mastodon/mastodon/pull/22223), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22342))
### Removed
- Officially remove support for Ruby 2.6 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21477))
- Remove LDSignature on actor Delete activities ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21466))
- Remove `object-fit` polyfill used for old versions of Microsoft Edge ([shuuji3](https://github.com/mastodon/mastodon/pull/22693))
- Remove empty `title` tag from mailer layout ([nametoolong](https://github.com/mastodon/mastodon/pull/23078))
### Fixed
- **Fix changing domain block severity not undoing individual account effects** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22135))
- Fix suspension worker crashing on S3-compatible setups without ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22487))
- Fix possible race conditions when suspending/unsuspending accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22363))
- Fix being stuck in edit mode when deleting the edited status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22126))
- Fix some performance issues with `/admin/instances` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21907))
- Fix some pre-4.0 admin audit logs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22091))
- Fix missing OAuth scopes for admin APIs ([trwnh](https://github.com/mastodon/mastodon/pull/20918), [trwnh](https://github.com/mastodon/mastodon/pull/20979))
- Fix voter count not being cleared when a poll is reset ([afontenot](https://github.com/mastodon/mastodon/pull/21700))
- Fix attachments of edited statuses not being fetched ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21565))
- Fix irreversible and whole_word parameters handling in `/api/v1/filters` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21988))
- Fix 500 error when marking posts as sensitive while some of them are deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22134))
- Fix expanded statuses not always being scrolled into view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21797))
- Fix not being able to scroll the remote interaction modal on small screens ([xendke](https://github.com/mastodon/mastodon/pull/21763))
- Fix disappearing “Explore” tabs on Safari ([nyura](https://github.com/mastodon/mastodon/pull/20917), [ykzts](https://github.com/mastodon/mastodon/pull/20982))
- Fix wrong padding in RTL layout ([Gargron](https://github.com/mastodon/mastodon/pull/23157))
- Fix being unable to get a single EmailDomainBlock from the admin API ([trwnh](https://github.com/mastodon/mastodon/pull/20846))
- Fix pagination of followed tags ([trwnh](https://github.com/mastodon/mastodon/pull/20861))
- Fix dropdown menu positions when scrolling ([sidp](https://github.com/mastodon/mastodon/pull/22916), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23062))
- Fix mysterious registration failure when “Require a reason to join” is set with open registrations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22127))
- Fix attachment rendering of edited posts in OpenGraph ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22270))
- Fix invalid/empty RSS feed link on account pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20772))
- Fix error in `VerifyLinkService` when processing links with no href ([joshuap](https://github.com/mastodon/mastodon/pull/20741))
- Fix media uploads with FFmpeg 5 ([dead10ck](https://github.com/mastodon/mastodon/pull/21191))
- Fix sensitive flag not being set when replying to a post with a content warning under certain conditions ([kedamaDQ](https://github.com/mastodon/mastodon/pull/21724))
- Fix “Share @user's profile” profile menu item not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21490))
- Fix crash and incorrect behavior in `tootctl domains crawl` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19004))
- Fix autoplay on iOS ([jamesadney](https://github.com/mastodon/mastodon/pull/21422))
- Fix spaces not being stripped in admin account search ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21324))
- Fix spaces not being stripped when adding relays ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22655))
- Fix infinite loading spinner instead of soft 404 for non-existing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21303))
- Fix minor visual issue with the top border of verified account fields ([j-f1](https://github.com/mastodon/mastodon/pull/22006))
- Fix pending account approval and rejection not being recorded in the admin audit log ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/22088))
- Fix “Sign up” button with closed registrations not opening modal on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22060))
- Fix UI header overflowing on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21783))
- Fix 500 error when trying to migrate to an invalid address ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21462))
- Fix crash when trying to fetch unobtainable avatar of user using external authentication ([lochiiconnectivity](https://github.com/mastodon/mastodon/pull/22462))
- Fix potential duplicate statuses in Explore tab ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22121))
- Fix deprecation warning in `tootctl accounts rotate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22120))
- Fix missing style in warning and strike cards ([AtelierSnek](https://github.com/mastodon/mastodon/pull/22177), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22302))
- Fix wasteful request to `/api/v1/custom_emojis` when not logged in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22326))
- Fix replies sometimes being delivered to user-blocked domains ([tribela](https://github.com/mastodon/mastodon/pull/22117))
- Fix admin dashboard crash when using some ElasticSearch replacements ([cortices](https://github.com/mastodon/mastodon/pull/21006))
- Fix profile avatar being slightly offset into left border ([RiedleroD](https://github.com/mastodon/mastodon/pull/20994))
- Fix N+1 queries in `NotificationsController` ([nametoolong](https://github.com/mastodon/mastodon/pull/21202))
- Fix being unable to react to announcements with the keycap number sign emoji ([kescherCode](https://github.com/mastodon/mastodon/pull/22231))
- Fix height computation of post embeds ([hodgesmr](https://github.com/mastodon/mastodon/pull/22141))
- Fix accessibility issue of the search bar due to hidden placeholder ([alexstine](https://github.com/mastodon/mastodon/pull/21275))
- Fix layout change handler not being removed due to a typo ([nschonni](https://github.com/mastodon/mastodon/pull/21829))
- Fix typo in the default `S3_HOSTNAME` used in the `mastodon:setup` rake task ([danp](https://github.com/mastodon/mastodon/pull/19932))
- Fix the top action bar appearing in the multi-column layout ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20943))
- Fix inability to use local LibreTranslate without setting `ALLOWED_PRIVATE_ADDRESSES` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21926))
- Fix punycoded local domains not being prettified in initial state ([Tritlo](https://github.com/mastodon/mastodon/pull/21440))
- Fix CSP violation warning by removing inline CSS from SVG logo ([luxiaba](https://github.com/mastodon/mastodon/pull/20814))
- Fix margin for search field on medium window size ([minacle](https://github.com/mastodon/mastodon/pull/21606))
- Fix search popout scrolling with the page in single-column mode ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/16463))
- Fix minor status cache hydration discrepancy ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19879))
- Fix `・` detection in hashtags ([parthoghosh24](https://github.com/mastodon/mastodon/pull/22888))
- Fix hashtag follows bypassing user blocks ([tribela](https://github.com/mastodon/mastodon/pull/22849))
- Fix moved accounts being incorrectly redirected to account settings when trying to view a remote profile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22497))
- Fix site upload validations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22479))
- Fix “Add new domain block” button using last submitted search value instead of the current one ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22485))
- Fix misleading hashtag warning when posting with “Followers only” or “Mentioned people only” visibility ([n0toose](https://github.com/mastodon/mastodon/pull/22827))
- Fix embedded posts with videos grabbing focus ([Akkiesoft](https://github.com/mastodon/mastodon/pull/22778))
- Fix `$` not being escaped in `.env.production` files generated by the `mastodon:setup` rake task ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23012), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23072))
- Fix sanitizer parsing link text as HTML when stripping unsupported links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22558))
- Fix `scheduled_at` input not using `datetime-local` when editing announcements ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21896))
- Fix REST API serializer for `Account` not including `moved` when the moved account has itself moved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22483))
- Fix `/api/v1/admin/trends/tags` using wrong serializer ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18943))
- Fix situations in which instance actor can be set to a Mastodon-incompatible name ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22307))
### Security
- Add `form-action` CSP directive ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20781), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20958), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20962))
- Fix unbounded recursion in account discovery ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22025))
- Revoke all authorized applications on password reset ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21325))
## [4.0.2] - 2022-11-15 ## [4.0.2] - 2022-11-15
### Fixed ### Fixed

View file

@ -40,7 +40,7 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org [homepage]: https://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/ [version]: https://contributor-covenant.org/version/1/4/

View file

@ -6,6 +6,12 @@ Here are some guidelines, and ways you can help.
> (This document is a bit of a work-in-progress, so please bear with us. > (This document is a bit of a work-in-progress, so please bear with us.
> If you don't see what you're looking for here, please don't hesitate to reach out!) > If you don't see what you're looking for here, please don't hesitate to reach out!)
## Translations
You can submit glitch-soc-specific translations via [Crowdin](https://crowdin.com/project/glitch-soc). They are periodically merged into the codebase.
[![Crowdin](https://badges.crowdin.net/glitch-soc/localized.svg)](https://crowdin.com/project/glitch-soc)
## Planning ## ## Planning ##
Right now a lot of the planning for this project takes place in our development Discord, or through GitHub Issues and Projects. Right now a lot of the planning for this project takes place in our development Discord, or through GitHub Issues and Projects.

17
Gemfile
View file

@ -10,7 +10,7 @@ gem 'puma', '~> 5.6'
gem 'rails', '~> 6.1.7' gem 'rails', '~> 6.1.7'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2' gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.4' gem 'rack', '~> 2.2.6'
gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.4' gem 'pg', '~> 1.4'
@ -19,7 +19,7 @@ gem 'pghero', '~> 2.8'
gem 'dotenv-rails', '~> 2.8' gem 'dotenv-rails', '~> 2.8'
gem 'aws-sdk-s3', '~> 1.117', require: false gem 'aws-sdk-s3', '~> 1.117', require: false
gem 'fog-core', '<= 2.1.0' gem 'fog-core', '<= 2.4.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'kt-paperclip', '~> 7.1' gem 'kt-paperclip', '~> 7.1'
gem 'blurhash', '~> 0.1' gem 'blurhash', '~> 0.1'
@ -51,7 +51,7 @@ gem 'ed25519', '~> 1.3'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
gem 'hiredis', '~> 0.6' gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.9' gem 'redis-namespace', '~> 1.10'
gem 'htmlentities', '~> 4.3' gem 'htmlentities', '~> 4.3'
gem 'http', '~> 5.1' gem 'http', '~> 5.1'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
@ -60,14 +60,14 @@ gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.13' gem 'nokogiri', '~> 1.14'
gem 'nsa', '~> 0.2' gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.13' gem 'oj', '~> 3.13'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.14'
gem 'parslet' gem 'parslet'
gem 'posix-spawn' gem 'posix-spawn'
gem 'public_suffix', '~> 5.0' gem 'public_suffix', '~> 5.0'
gem 'pundit', '~> 2.2' gem 'pundit', '~> 2.3'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 6.6' gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 1.1', require: 'rack/cors' gem 'rack-cors', '~> 1.1', require: 'rack/cors'
@ -79,7 +79,7 @@ gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 2.1' gem 'rqrcode', '~> 2.1'
gem 'ruby-progressbar', '~> 1.11' gem 'ruby-progressbar', '~> 1.11'
gem 'sanitize', '~> 6.0' gem 'sanitize', '~> 6.0'
gem 'scenic', '~> 1.6' gem 'scenic', '~> 1.7'
gem 'sidekiq', '~> 6.5' gem 'sidekiq', '~> 6.5'
gem 'sidekiq-scheduler', '~> 4.0' gem 'sidekiq-scheduler', '~> 4.0'
gem 'sidekiq-unique-jobs', '~> 7.1' gem 'sidekiq-unique-jobs', '~> 7.1'
@ -120,14 +120,13 @@ end
group :test do group :test do
gem 'capybara', '~> 3.38' gem 'capybara', '~> 3.38'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 3.0' gem 'faker', '~> 3.1'
gem 'json-schema', '~> 3.0' gem 'json-schema', '~> 3.0'
gem 'microformats', '~> 4.4'
gem 'rack-test', '~> 2.0' gem 'rack-test', '~> 2.0'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec_junit_formatter', '~> 0.6' gem 'rspec_junit_formatter', '~> 0.6'
gem 'rspec-sidekiq', '~> 3.1' gem 'rspec-sidekiq', '~> 3.1'
gem 'simplecov', '~> 0.21', require: false gem 'simplecov', '~> 0.22', require: false
gem 'webmock', '~> 3.18' gem 'webmock', '~> 3.18'
end end

View file

@ -10,40 +10,40 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.1.7) actioncable (6.1.7.1)
actionpack (= 6.1.7) actionpack (= 6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.7) actionmailbox (6.1.7.1)
actionpack (= 6.1.7) actionpack (= 6.1.7.1)
activejob (= 6.1.7) activejob (= 6.1.7.1)
activerecord (= 6.1.7) activerecord (= 6.1.7.1)
activestorage (= 6.1.7) activestorage (= 6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.7) actionmailer (6.1.7.1)
actionpack (= 6.1.7) actionpack (= 6.1.7.1)
actionview (= 6.1.7) actionview (= 6.1.7.1)
activejob (= 6.1.7) activejob (= 6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.7) actionpack (6.1.7.1)
actionview (= 6.1.7) actionview (= 6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.7) actiontext (6.1.7.1)
actionpack (= 6.1.7) actionpack (= 6.1.7.1)
activerecord (= 6.1.7) activerecord (= 6.1.7.1)
activestorage (= 6.1.7) activestorage (= 6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.7) actionview (6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -54,22 +54,22 @@ GEM
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.8) active_record_query_trace (1.8)
activejob (6.1.7) activejob (6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.7) activemodel (6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
activerecord (6.1.7) activerecord (6.1.7.1)
activemodel (= 6.1.7) activemodel (= 6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
activestorage (6.1.7) activestorage (6.1.7.1)
actionpack (= 6.1.7) actionpack (= 6.1.7.1)
activejob (= 6.1.7) activejob (= 6.1.7.1)
activerecord (= 6.1.7) activerecord (= 6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (6.1.7) activesupport (6.1.7.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@ -130,7 +130,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
redis (>= 1.0, < 6) redis (>= 1.0, < 6)
builder (3.2.4) builder (3.2.4)
bullet (7.0.4) bullet (7.0.7)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundler-audit (0.9.1) bundler-audit (0.9.1)
@ -184,6 +184,7 @@ GEM
crass (1.0.6) crass (1.0.6)
css_parser (1.12.0) css_parser (1.12.0)
addressable addressable
date (3.3.3)
debug_inspector (1.0.0) debug_inspector (1.0.0)
devise (4.8.1) devise (4.8.1)
bcrypt (~> 3.0) bcrypt (~> 3.0)
@ -203,7 +204,7 @@ GEM
diff-lcs (1.5.0) diff-lcs (1.5.0)
discard (1.2.1) discard (1.2.1)
activerecord (>= 4.2, < 8) activerecord (>= 4.2, < 8)
docile (1.3.4) docile (1.4.0)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.6.2) doorkeeper (5.6.2)
@ -223,12 +224,12 @@ GEM
faraday (~> 1) faraday (~> 1)
multi_json multi_json
encryptor (3.0.0) encryptor (3.0.0)
erubi (1.11.0) erubi (1.12.0)
et-orbi (1.2.7) et-orbi (1.2.7)
tzinfo tzinfo
excon (0.76.0) excon (0.95.0)
fabrication (2.30.0) fabrication (2.30.0)
faker (3.0.0) faker (3.1.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.9.3) faraday (1.9.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
@ -271,7 +272,7 @@ GEM
fog-core (>= 1.45, <= 2.1.0) fog-core (>= 1.45, <= 2.1.0)
fog-json (>= 1.0) fog-json (>= 1.0)
ipaddress (>= 0.8) ipaddress (>= 0.8)
formatador (0.2.5) formatador (0.3.0)
fugit (1.7.1) fugit (1.7.1)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
@ -282,7 +283,7 @@ GEM
addressable (~> 2.7) addressable (~> 2.7)
omniauth (>= 1.9, < 3) omniauth (>= 1.9, < 3)
openid_connect (~> 1.2) openid_connect (~> 1.2)
globalid (1.0.0) globalid (1.0.1)
activesupport (>= 5.0) activesupport (>= 5.0)
hamlit (2.13.0) hamlit (2.13.0)
temple (>= 0.8.2) temple (>= 0.8.2)
@ -301,7 +302,7 @@ GEM
hiredis (0.6.3) hiredis (0.6.3)
hkdf (0.3.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
http (5.1.0) http (5.1.1)
addressable (~> 2.8) addressable (~> 2.8)
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (~> 2.2) http-form_data (~> 2.2)
@ -330,9 +331,9 @@ GEM
idn-ruby (0.1.5) idn-ruby (0.1.5)
ipaddress (0.8.3) ipaddress (0.8.3)
jmespath (1.6.2) jmespath (1.6.2)
json (2.6.2) json (2.6.3)
json-canonicalization (0.3.0) json-canonicalization (0.3.0)
json-jwt (1.13.0) json-jwt (1.14.0)
activesupport (>= 4.2) activesupport (>= 4.2)
aes_key_wrap aes_key_wrap
bindata bindata
@ -349,7 +350,7 @@ GEM
json-schema (3.0.0) json-schema (3.0.0)
addressable (>= 2.8) addressable (>= 2.8)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.4.1) jwt (2.5.0)
kaminari (1.2.2) kaminari (1.2.2)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2) kaminari-actionview (= 1.2.2)
@ -389,8 +390,11 @@ GEM
loofah (2.19.1) loofah (2.19.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.8.0.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
makara (0.5.1) makara (0.5.1)
activerecord (>= 5.2.0) activerecord (>= 5.2.0)
marcel (1.0.2) marcel (1.0.2)
@ -399,19 +403,21 @@ GEM
matrix (0.4.2) matrix (0.4.2)
memory_profiler (1.0.1) memory_profiler (1.0.1)
method_source (1.0.0) method_source (1.0.0)
microformats (4.4.1)
json (~> 2.2)
nokogiri (~> 1.10)
mime-types (3.4.1) mime-types (3.4.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105) mime-types-data (3.2022.0105)
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.8.0) mini_portile2 (2.8.1)
minitest (5.16.3) minitest (5.17.0)
msgpack (1.6.0) msgpack (1.6.0)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.1.1) multipart-post (2.1.1)
net-imap (0.3.4)
date
net-protocol
net-ldap (0.17.1) net-ldap (0.17.1)
net-pop (0.1.2)
net-protocol
net-protocol (0.1.3) net-protocol (0.1.3)
timeout timeout
net-scp (4.0.0.rc1) net-scp (4.0.0.rc1)
@ -420,7 +426,7 @@ GEM
net-protocol net-protocol
net-ssh (7.0.1) net-ssh (7.0.1)
nio4r (2.5.8) nio4r (2.5.8)
nokogiri (1.13.10) nokogiri (1.14.0)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
racc (~> 1.4) racc (~> 1.4)
nsa (0.2.8) nsa (0.2.8)
@ -456,9 +462,9 @@ GEM
openssl-signature_algorithm (1.2.1) openssl-signature_algorithm (1.2.1)
openssl (> 2.0, < 3.1) openssl (> 2.0, < 3.1)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ox (2.14.11) ox (2.14.13)
parallel (1.22.1) parallel (1.22.1)
parser (3.1.2.1) parser (3.2.0.0)
ast (~> 2.4.1) ast (~> 2.4.1)
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
@ -488,11 +494,11 @@ GEM
public_suffix (5.0.1) public_suffix (5.0.1)
puma (5.6.5) puma (5.6.5)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.2.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.6.1) racc (1.6.2)
rack (2.2.4) rack (2.2.6.2)
rack-attack (6.6.1) rack-attack (6.6.1)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.1.1) rack-cors (1.1.1)
@ -507,20 +513,20 @@ GEM
rack rack
rack-test (2.0.2) rack-test (2.0.2)
rack (>= 1.3) rack (>= 1.3)
rails (6.1.7) rails (6.1.7.1)
actioncable (= 6.1.7) actioncable (= 6.1.7.1)
actionmailbox (= 6.1.7) actionmailbox (= 6.1.7.1)
actionmailer (= 6.1.7) actionmailer (= 6.1.7.1)
actionpack (= 6.1.7) actionpack (= 6.1.7.1)
actiontext (= 6.1.7) actiontext (= 6.1.7.1)
actionview (= 6.1.7) actionview (= 6.1.7.1)
activejob (= 6.1.7) activejob (= 6.1.7.1)
activemodel (= 6.1.7) activemodel (= 6.1.7.1)
activerecord (= 6.1.7) activerecord (= 6.1.7.1)
activestorage (= 6.1.7) activestorage (= 6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.7) railties (= 6.1.7.1)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
@ -536,9 +542,9 @@ GEM
railties (>= 6.0.0, < 7) railties (>= 6.0.0, < 7)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (6.1.7) railties (6.1.7.1)
actionpack (= 6.1.7) actionpack (= 6.1.7.1)
activesupport (= 6.1.7) activesupport (= 6.1.7.1)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
@ -550,11 +556,11 @@ GEM
rdf (~> 3.2) rdf (~> 3.2)
redcarpet (3.5.1) redcarpet (3.5.1)
redis (4.5.1) redis (4.5.1)
redis-namespace (1.9.0) redis-namespace (1.10.0)
redis (>= 4) redis (>= 4)
redlock (1.3.2) redlock (1.3.2)
redis (>= 3.0.0, < 6.0) redis (>= 3.0.0, < 6.0)
regexp_parser (2.6.0) regexp_parser (2.6.1)
request_store (1.5.1) request_store (1.5.1)
rack (>= 1.4) rack (>= 1.4)
responders (3.0.1) responders (3.0.1)
@ -589,27 +595,30 @@ GEM
rspec-support (3.11.1) rspec-support (3.11.1)
rspec_junit_formatter (0.6.0) rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0) rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.39.0) rubocop (1.43.0)
json (~> 2.3) json (~> 2.3)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.1.2.1) parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.23.0, < 2.0) rubocop-ast (>= 1.24.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.23.0) rubocop-ast (1.24.1)
parser (>= 3.1.1.0) parser (>= 3.1.1.0)
rubocop-performance (1.15.1) rubocop-capybara (2.17.0)
rubocop (~> 1.41)
rubocop-performance (1.15.2)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.17.2) rubocop-rails (2.17.4)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.15.0) rubocop-rspec (2.18.0)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-capybara
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
ruby-saml (1.13.0) ruby-saml (1.13.0)
nokogiri (>= 1.10.5) nokogiri (>= 1.10.5)
@ -622,7 +631,7 @@ GEM
sanitize (6.0.0) sanitize (6.0.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
scenic (1.6.0) scenic (1.7.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 4.0.0) railties (>= 4.0.0)
semantic_range (3.0.0) semantic_range (3.0.0)
@ -648,12 +657,12 @@ GEM
simple_form (5.1.0) simple_form (5.1.0)
actionpack (>= 5.2) actionpack (>= 5.2)
activemodel (>= 5.2) activemodel (>= 5.2)
simplecov (0.21.2) simplecov (0.22.0)
docile (~> 1.1) docile (~> 1.1)
simplecov-html (~> 0.11) simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1) simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3) simplecov-html (0.12.3)
simplecov_json_formatter (0.1.2) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sprockets (3.7.2) sprockets (3.7.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
@ -707,7 +716,7 @@ GEM
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8.2) unf_ext (0.0.8.2)
unicode-display_width (2.3.0) unicode-display_width (2.4.2)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
validate_email (0.1.6) validate_email (0.1.6)
activemodel (>= 3.0) activemodel (>= 3.0)
@ -784,10 +793,10 @@ DEPENDENCIES
dotenv-rails (~> 2.8) dotenv-rails (~> 2.8)
ed25519 (~> 1.3) ed25519 (~> 1.3)
fabrication (~> 2.30) fabrication (~> 2.30)
faker (~> 3.0) faker (~> 3.1)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fastimage fastimage
fog-core (<= 2.1.0) fog-core (<= 2.4.0)
fog-openstack (~> 0.3) fog-openstack (~> 0.3)
fuubar (~> 2.5) fuubar (~> 2.5)
gitlab-omniauth-openid-connect (~> 0.10.0) gitlab-omniauth-openid-connect (~> 0.10.0)
@ -812,10 +821,9 @@ DEPENDENCIES
makara (~> 0.5) makara (~> 0.5)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
memory_profiler memory_profiler
microformats (~> 4.4)
mime-types (~> 3.4.1) mime-types (~> 3.4.1)
net-ldap (~> 0.17) net-ldap (~> 0.17)
nokogiri (~> 1.13) nokogiri (~> 1.14)
nsa (~> 0.2) nsa (~> 0.2)
oj (~> 3.13) oj (~> 3.13)
omniauth (~> 1.9) omniauth (~> 1.9)
@ -834,8 +842,8 @@ DEPENDENCIES
pry-rails (~> 0.3) pry-rails (~> 0.3)
public_suffix (~> 5.0) public_suffix (~> 5.0)
puma (~> 5.6) puma (~> 5.6)
pundit (~> 2.2) pundit (~> 2.3)
rack (~> 2.2.4) rack (~> 2.2.6)
rack-attack (~> 6.6) rack-attack (~> 6.6)
rack-cors (~> 1.1) rack-cors (~> 1.1)
rack-test (~> 2.0) rack-test (~> 2.0)
@ -846,7 +854,7 @@ DEPENDENCIES
rdf-normalize (~> 0.5) rdf-normalize (~> 0.5)
redcarpet (~> 3.5) redcarpet (~> 3.5)
redis (~> 4.5) redis (~> 4.5)
redis-namespace (~> 1.9) redis-namespace (~> 1.10)
rexml (~> 3.2) rexml (~> 3.2)
rqrcode (~> 2.1) rqrcode (~> 2.1)
rspec-rails (~> 5.1) rspec-rails (~> 5.1)
@ -858,14 +866,14 @@ DEPENDENCIES
rubocop-rspec rubocop-rspec
ruby-progressbar (~> 1.11) ruby-progressbar (~> 1.11)
sanitize (~> 6.0) sanitize (~> 6.0)
scenic (~> 1.6) scenic (~> 1.7)
sidekiq (~> 6.5) sidekiq (~> 6.5)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 4.0) sidekiq-scheduler (~> 4.0)
sidekiq-unique-jobs (~> 7.1) sidekiq-unique-jobs (~> 7.1)
simple-navigation (~> 4.4) simple-navigation (~> 4.4)
simple_form (~> 5.1) simple_form (~> 5.1)
simplecov (~> 0.21) simplecov (~> 0.22)
sprockets (~> 3.7.2) sprockets (~> 3.7.2)
sprockets-rails (~> 3.4) sprockets-rails (~> 3.4)
stackprof stackprof
@ -880,9 +888,3 @@ DEPENDENCIES
webpacker (~> 5.4) webpacker (~> 5.4)
webpush! webpush!
xorcist (~> 1.1) xorcist (~> 1.1)
RUBY VERSION
ruby 3.0.4p208
BUNDLED WITH
2.2.33

View file

@ -21,7 +21,7 @@ module Admin
account_action.save! account_action.save!
if account_action.with_report? if account_action.with_report?
redirect_to admin_reports_path redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id])
else else
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end

View file

@ -23,9 +23,7 @@ module Admin
@import = Admin::Import.new(import_params) @import = Admin::Import.new(import_params)
return render :new unless @import.validate return render :new unless @import.validate
parse_import_data!(export_headers) @import.csv_rows.each do |row|
@data.take(Admin::Import::ROWS_PROCESSING_LIMIT).each do |row|
domain = row['#domain'].strip domain = row['#domain'].strip
next if DomainAllow.allowed?(domain) next if DomainAllow.allowed?(domain)

View file

@ -23,24 +23,30 @@ module Admin
@import = Admin::Import.new(import_params) @import = Admin::Import.new(import_params)
return render :new unless @import.validate return render :new unless @import.validate
parse_import_data!(export_headers)
@global_private_comment = I18n.t('admin.export_domain_blocks.import.private_comment_template', source: @import.data_file_name, date: I18n.l(Time.now.utc)) @global_private_comment = I18n.t('admin.export_domain_blocks.import.private_comment_template', source: @import.data_file_name, date: I18n.l(Time.now.utc))
@form = Form::DomainBlockBatch.new @form = Form::DomainBlockBatch.new
@domain_blocks = @data.take(Admin::Import::ROWS_PROCESSING_LIMIT).filter_map do |row| @domain_blocks = @import.csv_rows.filter_map do |row|
domain = row['#domain'].strip domain = row['#domain'].strip
next if DomainBlock.rule_for(domain).present? next if DomainBlock.rule_for(domain).present?
domain_block = DomainBlock.new(domain: domain, domain_block = DomainBlock.new(domain: domain,
severity: row['#severity'].strip, severity: row.fetch('#severity', :suspend),
reject_media: row['#reject_media'].strip, reject_media: row.fetch('#reject_media', false),
reject_reports: row['#reject_reports'].strip, reject_reports: row.fetch('#reject_reports', false),
private_comment: @global_private_comment, private_comment: @global_private_comment,
public_comment: row['#public_comment']&.strip, public_comment: row['#public_comment'],
obfuscate: row['#obfuscate'].strip) obfuscate: row.fetch('#obfuscate', false))
domain_block if domain_block.valid? if domain_block.invalid?
flash.now[:alert] = I18n.t('admin.export_domain_blocks.invalid_domain_block', error: domain_block.errors.full_messages.join(', '))
next
end
domain_block
rescue ArgumentError => e
flash.now[:alert] = I18n.t('admin.export_domain_blocks.invalid_domain_block', error: e.message)
next
end end
@warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain) @warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain)

View file

@ -49,7 +49,7 @@ module Admin
private private
def set_instance def set_instance
@instance = Instance.find(params[:id]) @instance = Instance.find(TagManager.instance.normalize_domain(params[:id]&.strip))
end end
def set_instances def set_instances

View file

@ -3,6 +3,11 @@
class Admin::Reports::ActionsController < Admin::BaseController class Admin::Reports::ActionsController < Admin::BaseController
before_action :set_report before_action :set_report
def preview
authorize @report, :show?
@moderation_action = action_from_button
end
def create def create
authorize @report, :show? authorize @report, :show?
@ -13,7 +18,8 @@ class Admin::Reports::ActionsController < Admin::BaseController
status_ids: @report.status_ids, status_ids: @report.status_ids,
current_account: current_account, current_account: current_account,
report_id: @report.id, report_id: @report.id,
send_email_notification: !@report.spam? send_email_notification: !@report.spam?,
text: params[:text]
) )
status_batch_action.save! status_batch_action.save!
@ -23,13 +29,16 @@ class Admin::Reports::ActionsController < Admin::BaseController
report_id: @report.id, report_id: @report.id,
target_account: @report.target_account, target_account: @report.target_account,
current_account: current_account, current_account: current_account,
send_email_notification: !@report.spam? send_email_notification: !@report.spam?,
text: params[:text]
) )
account_action.save! account_action.save!
else
return redirect_to admin_report_path(@report), alert: I18n.t('admin.reports.unknown_action_msg', action: action_from_button)
end end
redirect_to admin_reports_path redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: @report.id)
end end
private private
@ -47,6 +56,8 @@ class Admin::Reports::ActionsController < Admin::BaseController
'silence' 'silence'
elsif params[:suspend] elsif params[:suspend]
'suspend' 'suspend'
elsif params[:moderation_action]
params[:moderation_action]
end end
end end
end end

View file

@ -21,7 +21,17 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
private private
def account_params def account_params
params.permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value]) params.permit(
:display_name,
:note,
:avatar,
:header,
:locked,
:bot,
:discoverable,
:hide_collections,
fields_attributes: [:name, :value]
)
end end
def user_settings_params def user_settings_params

View file

@ -3,6 +3,14 @@
class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController
before_action -> { authorize_if_got_token! :'admin:read' } before_action -> { authorize_if_got_token! :'admin:read' }
def index
if current_user&.can?(:manage_taxonomies)
render json: @tags, each_serializer: REST::Admin::TagSerializer
else
super
end
end
private private
def enabled? def enabled?

View file

@ -80,6 +80,7 @@ class Api::V1::StatusesController < Api::BaseController
current_account.id, current_account.id,
text: status_params[:status], text: status_params[:status],
media_ids: status_params[:media_ids], media_ids: status_params[:media_ids],
media_attributes: status_params[:media_attributes],
sensitive: status_params[:sensitive], sensitive: status_params[:sensitive],
language: status_params[:language], language: status_params[:language],
spoiler_text: status_params[:spoiler_text], spoiler_text: status_params[:spoiler_text],
@ -131,6 +132,12 @@ class Api::V1::StatusesController < Api::BaseController
:scheduled_at, :scheduled_at,
:content_type, :content_type,
media_ids: [], media_ids: [],
media_attributes: [
:id,
:thumbnail,
:description,
:focus,
],
poll: [ poll: [
:multiple, :multiple,
:hide_totals, :hide_totals,

View file

@ -26,14 +26,4 @@ module AdminExportControllerConcern
def import_params def import_params
params.require(:admin_import).permit(:data) params.require(:admin_import).permit(:data)
end end
def import_data_path
params[:admin_import][:data].path
end
def parse_import_data!(default_headers)
data = CSV.read(import_data_path, headers: true, encoding: 'UTF-8')
data = CSV.read(import_data_path, headers: default_headers, encoding: 'UTF-8') unless data.headers&.first&.strip&.include?(default_headers[0])
@data = data.reject(&:blank?)
end
end end

View file

@ -46,11 +46,11 @@ module SignatureVerification
end end
def require_account_signature! def require_account_signature!
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
end end
def require_actor_signature! def require_actor_signature!
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor
end end
def signed_request? def signed_request?
@ -97,11 +97,11 @@ module SignatureVerification
actor = stoplight_wrap_request { actor_refresh_key!(actor) } actor = stoplight_wrap_request { actor_refresh_key!(actor) }
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
return actor unless verify_signature(actor, signature, compare_signed_string).nil? return actor unless verify_signature(actor, signature, compare_signed_string).nil?
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)" fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
rescue SignatureVerificationError => e rescue SignatureVerificationError => e
fail_with! e.message fail_with! e.message
rescue HTTP::Error, OpenSSL::SSL::SSLError => e rescue HTTP::Error, OpenSSL::SSL::SSLError => e
@ -118,8 +118,8 @@ module SignatureVerification
private private
def fail_with!(message) def fail_with!(message, **options)
@signature_verification_failure_reason = message @signature_verification_failure_reason = { error: message }.merge(options)
@signed_request_actor = nil @signed_request_actor = nil
end end
@ -209,8 +209,8 @@ module SignatureVerification
end end
expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present? expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present?
rescue ArgumentError rescue ArgumentError => e
return false raise SignatureVerificationError, "Invalid Date header: #{e.message}"
end end
expires_time ||= created_time + 5.minutes unless created_time.nil? expires_time ||= created_time + 5.minutes unless created_time.nil?

View file

@ -4,22 +4,17 @@ module WebAppControllerConcern
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
prepend_before_action :redirect_unauthenticated_to_permalinks!
before_action :set_pack before_action :set_pack
before_action :redirect_unauthenticated_to_permalinks!
before_action :set_app_body_class before_action :set_app_body_class
before_action :set_referrer_policy_header
end end
def set_app_body_class def set_app_body_class
@body_classes = 'app-body' @body_classes = 'app-body'
end end
def set_referrer_policy_header
response.headers['Referrer-Policy'] = 'origin'
end
def redirect_unauthenticated_to_permalinks! def redirect_unauthenticated_to_permalinks!
return if user_signed_in? return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in
redirect_path = PermalinkRedirector.new(request.path).redirect_path redirect_path = PermalinkRedirector.new(request.path).redirect_path

View file

@ -194,7 +194,7 @@ ready(() => {
} }
document.querySelector('a#add-instance-button')?.addEventListener('click', (e) => { document.querySelector('a#add-instance-button')?.addEventListener('click', (e) => {
const domain = document.getElementById('by_domain')?.value; const domain = document.querySelector('input[type="text"]#by_domain')?.value;
if (domain) { if (domain) {
const url = new URL(event.target.href); const url = new URL(event.target.href);

View file

@ -181,6 +181,18 @@ export function submitCompose(routerHistory) {
dispatch(submitComposeRequest()); dispatch(submitComposeRequest());
// If we're editing a post with media attachments, those have not
// necessarily been changed on the server. Do it now in the same
// API call.
let media_attributes;
if (statusId !== null) {
media_attributes = media.map(item => ({
id: item.get('id'),
description: item.get('description'),
focus: item.get('focus'),
}));
}
api(getState).request({ api(getState).request({
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`, url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
method: statusId === null ? 'post' : 'put', method: statusId === null ? 'post' : 'put',
@ -189,6 +201,7 @@ export function submitCompose(routerHistory) {
content_type: getState().getIn(['compose', 'content_type']), content_type: getState().getIn(['compose', 'content_type']),
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: media.map(item => item.get('id')), media_ids: media.map(item => item.get('id')),
media_attributes,
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0), sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
spoiler_text: spoilerText, spoiler_text: spoilerText,
visibility: getState().getIn(['compose', 'privacy']), visibility: getState().getIn(['compose', 'privacy']),
@ -415,11 +428,31 @@ export function changeUploadCompose(id, params) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(changeUploadComposeRequest()); dispatch(changeUploadComposeRequest());
api(getState).put(`/api/v1/media/${id}`, params).then(response => { let media = getState().getIn(['compose', 'media_attachments']).find((item) => item.get('id') === id);
dispatch(changeUploadComposeSuccess(response.data));
}).catch(error => { // Editing already-attached media is deferred to editing the post itself.
dispatch(changeUploadComposeFail(id, error)); // For simplicity's sake, fake an API reply.
}); if (media && !media.get('unattached')) {
let { description, focus } = params;
const data = media.toJS();
if (description) {
data.description = description;
}
if (focus) {
focus = focus.split(',');
data.meta = { focus: { x: parseFloat(focus[0]), y: parseFloat(focus[1]) } };
}
dispatch(changeUploadComposeSuccess(data, true));
} else {
api(getState).put(`/api/v1/media/${id}`, params).then(response => {
dispatch(changeUploadComposeSuccess(response.data, false));
}).catch(error => {
dispatch(changeUploadComposeFail(id, error));
});
}
}; };
}; };
@ -430,10 +463,11 @@ export function changeUploadComposeRequest() {
}; };
}; };
export function changeUploadComposeSuccess(media) { export function changeUploadComposeSuccess(media, attached) {
return { return {
type: COMPOSE_UPLOAD_CHANGE_SUCCESS, type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
media: media, media: media,
attached: attached,
skipLoading: true, skipLoading: true,
}; };
}; };

View file

@ -1,8 +1,8 @@
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
export function openDropdownMenu(id, placement, keyboard, scroll_key) { export function openDropdownMenu(id, keyboard, scroll_key) {
return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard, scroll_key }; return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key };
} }
export function closeDropdownMenu(id) { export function closeDropdownMenu(id) {

View file

@ -1,9 +1,17 @@
import api from '../api'; import api, { getLinks } from '../api';
export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST'; export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST';
export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS'; export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS';
export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL'; export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL';
export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST';
export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST'; export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST';
export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS'; export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS';
export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL'; export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL';
@ -37,6 +45,78 @@ export const fetchHashtagFail = error => ({
error, error,
}); });
export const fetchFollowedHashtags = () => (dispatch, getState) => {
dispatch(fetchFollowedHashtagsRequest());
api(getState).get('/api/v1/followed_tags').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(fetchFollowedHashtagsFail(err));
});
};
export function fetchFollowedHashtagsRequest() {
return {
type: FOLLOWED_HASHTAGS_FETCH_REQUEST,
};
};
export function fetchFollowedHashtagsSuccess(followed_tags, next) {
return {
type: FOLLOWED_HASHTAGS_FETCH_SUCCESS,
followed_tags,
next,
};
};
export function fetchFollowedHashtagsFail(error) {
return {
type: FOLLOWED_HASHTAGS_FETCH_FAIL,
error,
};
};
export function expandFollowedHashtags() {
return (dispatch, getState) => {
const url = getState().getIn(['followed_tags', 'next']);
if (url === null) {
return;
}
dispatch(expandFollowedHashtagsRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFollowedHashtagsFail(error));
});
};
};
export function expandFollowedHashtagsRequest() {
return {
type: FOLLOWED_HASHTAGS_EXPAND_REQUEST,
};
};
export function expandFollowedHashtagsSuccess(followed_tags, next) {
return {
type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
followed_tags,
next,
};
};
export function expandFollowedHashtagsFail(error) {
return {
type: FOLLOWED_HASHTAGS_EXPAND_FAIL,
error,
};
};
export const followHashtag = name => (dispatch, getState) => { export const followHashtag = name => (dispatch, getState) => {
dispatch(followHashtagRequest(name)); dispatch(followHashtagRequest(name));

View file

@ -50,7 +50,7 @@ export default class Trends extends React.PureComponent {
<Hashtag <Hashtag
key={hashtag.name} key={hashtag.name}
name={hashtag.name} name={hashtag.name}
href={`/admin/tags/${hashtag.id}`} href={hashtag.id === undefined ? undefined : `/admin/tags/${hashtag.id}`}
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1} people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1} uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
history={hashtag.history.reverse().map(day => day.uses)} history={hashtag.history.reverse().map(day => day.uses)}

View file

@ -2,9 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import IconButton from './icon_button'; import IconButton from './icon_button';
import Overlay from 'react-overlays/lib/Overlay'; import Overlay from 'react-overlays/Overlay';
import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import classNames from 'classnames'; import classNames from 'classnames';
import { CircularProgress } from 'flavours/glitch/components/loading_indicator'; import { CircularProgress } from 'flavours/glitch/components/loading_indicator';
@ -24,9 +22,6 @@ class DropdownMenu extends React.PureComponent {
scrollable: PropTypes.bool, scrollable: PropTypes.bool,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
style: PropTypes.object, style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
openedViaKeyboard: PropTypes.bool, openedViaKeyboard: PropTypes.bool,
renderItem: PropTypes.func, renderItem: PropTypes.func,
renderHeader: PropTypes.func, renderHeader: PropTypes.func,
@ -35,11 +30,6 @@ class DropdownMenu extends React.PureComponent {
static defaultProps = { static defaultProps = {
style: {}, style: {},
placement: 'bottom',
};
state = {
mounted: false,
}; };
handleDocumentClick = e => { handleDocumentClick = e => {
@ -56,8 +46,6 @@ class DropdownMenu extends React.PureComponent {
if (this.focusedItem && this.props.openedViaKeyboard) { if (this.focusedItem && this.props.openedViaKeyboard) {
this.focusedItem.focus({ preventScroll: true }); this.focusedItem.focus({ preventScroll: true });
} }
this.setState({ mounted: true });
} }
componentWillUnmount () { componentWillUnmount () {
@ -139,40 +127,28 @@ class DropdownMenu extends React.PureComponent {
} }
render () { render () {
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props; const { items, scrollable, renderHeader, loading } = this.props;
const { mounted } = this.state;
let renderItem = this.props.renderItem || this.renderItem; let renderItem = this.props.renderItem || this.renderItem;
return ( return (
<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 }) }}> <div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })} ref={this.setRef}>
{({ opacity, scaleX, scaleY }) => ( {loading && (
// It should not be transformed when mounting because the resulting <CircularProgress size={30} strokeWidth={3.5} />
// size will be used to determine the coordinate of the menu by )}
// react-overlays
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })}> {!loading && renderHeader && (
{loading && ( <div className='dropdown-menu__container__header'>
<CircularProgress size={30} strokeWidth={3.5} /> {renderHeader(items)}
)}
{!loading && renderHeader && (
<div className='dropdown-menu__container__header'>
{renderHeader(items)}
</div>
)}
{!loading && (
<ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
{items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
</ul>
)}
</div>
</div> </div>
)} )}
</Motion>
{!loading && (
<ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
{items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
</ul>
)}
</div>
); );
} }
@ -197,7 +173,6 @@ export default class Dropdown extends React.PureComponent {
isUserTouching: PropTypes.func, isUserTouching: PropTypes.func,
onOpen: PropTypes.func.isRequired, onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number, openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool, openedViaKeyboard: PropTypes.bool,
renderItem: PropTypes.func, renderItem: PropTypes.func,
@ -213,13 +188,11 @@ export default class Dropdown extends React.PureComponent {
id: id++, id: id++,
}; };
handleClick = ({ target, type }) => { handleClick = ({ type }) => {
if (this.state.id === this.props.openDropdownId) { if (this.state.id === this.props.openDropdownId) {
this.handleClose(); this.handleClose();
} else { } else {
const { top } = target.getBoundingClientRect(); this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click');
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
} }
} }
@ -303,7 +276,6 @@ export default class Dropdown extends React.PureComponent {
disabled, disabled,
loading, loading,
scrollable, scrollable,
dropdownPlacement,
openDropdownId, openDropdownId,
openedViaKeyboard, openedViaKeyboard,
children, children,
@ -314,7 +286,6 @@ export default class Dropdown extends React.PureComponent {
const open = this.state.id === openDropdownId; const open = this.state.id === openDropdownId;
const button = children ? React.cloneElement(React.Children.only(children), { const button = children ? React.cloneElement(React.Children.only(children), {
ref: this.setTargetRef,
onClick: this.handleClick, onClick: this.handleClick,
onMouseDown: this.handleMouseDown, onMouseDown: this.handleMouseDown,
onKeyDown: this.handleButtonKeyDown, onKeyDown: this.handleButtonKeyDown,
@ -326,7 +297,6 @@ export default class Dropdown extends React.PureComponent {
active={open} active={open}
disabled={disabled} disabled={disabled}
size={size} size={size}
ref={this.setTargetRef}
onClick={this.handleClick} onClick={this.handleClick}
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown} onKeyDown={this.handleButtonKeyDown}
@ -336,19 +306,27 @@ export default class Dropdown extends React.PureComponent {
return ( return (
<React.Fragment> <React.Fragment>
{button} <span ref={this.setTargetRef}>
{button}
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}> </span>
<DropdownMenu <Overlay show={open} offset={[5, 5]} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
items={items} {({ props, arrowProps, placement }) => (
loading={loading} <div {...props}>
scrollable={scrollable} <div className={`dropdown-animation dropdown-menu ${placement}`}>
onClose={this.handleClose} <div className={`dropdown-menu__arrow ${placement}`} {...arrowProps} />
openedViaKeyboard={openedViaKeyboard} <DropdownMenu
renderItem={renderItem} items={items}
renderHeader={renderHeader} loading={loading}
onItemClick={this.handleItemClick} scrollable={scrollable}
/> onClose={this.handleClose}
openedViaKeyboard={openedViaKeyboard}
renderItem={renderItem}
renderHeader={renderHeader}
onItemClick={this.handleItemClick}
/>
</div>
</div>
)}
</Overlay> </Overlay>
</React.Fragment> </React.Fragment>
); );

View file

@ -4,7 +4,6 @@ import { fetchHistory } from 'flavours/glitch/actions/history';
import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
const mapStateToProps = (state, { statusId }) => ({ const mapStateToProps = (state, { statusId }) => ({
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
openDropdownId: state.getIn(['dropdown_menu', 'openId']), openDropdownId: state.getIn(['dropdown_menu', 'openId']),
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
items: state.getIn(['history', statusId, 'items']), items: state.getIn(['history', statusId, 'items']),
@ -13,9 +12,9 @@ const mapStateToProps = (state, { statusId }) => ({
const mapDispatchToProps = (dispatch, { statusId }) => ({ const mapDispatchToProps = (dispatch, { statusId }) => ({
onOpen (id, onItemClick, dropdownPlacement, keyboard) { onOpen (id, onItemClick, keyboard) {
dispatch(fetchHistory(statusId)); dispatch(fetchHistory(statusId));
dispatch(openDropdownMenu(id, dropdownPlacement, keyboard)); dispatch(openDropdownMenu(id, keyboard));
}, },
onClose (id) { onClose (id) {

View file

@ -30,6 +30,7 @@ export default class IconButton extends React.PureComponent {
counter: PropTypes.number, counter: PropTypes.number,
obfuscateCount: PropTypes.bool, obfuscateCount: PropTypes.bool,
href: PropTypes.string, href: PropTypes.string,
ariaHidden: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -39,6 +40,7 @@ export default class IconButton extends React.PureComponent {
animate: false, animate: false,
overlay: false, overlay: false,
tabIndex: '0', tabIndex: '0',
ariaHidden: false,
}; };
state = { state = {
@ -115,6 +117,7 @@ export default class IconButton extends React.PureComponent {
counter, counter,
obfuscateCount, obfuscateCount,
href, href,
ariaHidden,
} = this.props; } = this.props;
const { const {
@ -155,6 +158,7 @@ export default class IconButton extends React.PureComponent {
<button <button
aria-label={title} aria-label={title}
aria-expanded={expanded} aria-expanded={expanded}
aria-hidden={ariaHidden}
title={title} title={title}
className={classes} className={classes}
onClick={this.handleClick} onClick={this.handleClick}

View file

@ -376,7 +376,7 @@ class MediaGallery extends React.PureComponent {
</button> </button>
); );
} else if (visible) { } else if (visible) {
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' overlay onClick={this.handleOpen} />; spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' overlay onClick={this.handleOpen} ariaHidden />;
} else { } else {
spoilerButton = ( spoilerButton = (
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'> <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>

View file

@ -106,7 +106,7 @@ class Status extends ImmutablePureComponent {
scrollKey: PropTypes.string, scrollKey: PropTypes.string,
deployPictureInPicture: PropTypes.func, deployPictureInPicture: PropTypes.func,
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,
pictureInPicture: PropTypes.shape({ pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool, inUse: PropTypes.bool,
available: PropTypes.bool, available: PropTypes.bool,
}), }),
@ -607,7 +607,7 @@ class Status extends ImmutablePureComponent {
attachments = status.get('media_attachments'); attachments = status.get('media_attachments');
if (pictureInPicture.inUse) { if (pictureInPicture.get('inUse')) {
media.push(<PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />); media.push(<PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />);
mediaIcons.push('video-camera'); mediaIcons.push('video-camera');
} else if (attachments.size > 0) { } else if (attachments.size > 0) {
@ -635,7 +635,7 @@ class Status extends ImmutablePureComponent {
width={this.props.cachedMediaWidth} width={this.props.cachedMediaWidth}
height={110} height={110}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
visible={this.state.showMedia} visible={this.state.showMedia}
@ -664,7 +664,7 @@ class Status extends ImmutablePureComponent {
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
width={this.props.cachedMediaWidth} width={this.props.cachedMediaWidth}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
visible={this.state.showMedia} visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
/>)} />)}

View file

@ -10,6 +10,7 @@ import RelativeTimestamp from './relative_timestamp';
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
import classNames from 'classnames'; import classNames from 'classnames';
import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container'; import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container';
const messages = defineMessages({ const messages = defineMessages({
@ -39,9 +40,10 @@ const messages = defineMessages({
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' }, embed: { id: 'status.embed', defaultMessage: 'Embed' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
hide: { id: 'status.hide', defaultMessage: 'Hide toot' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
hide: { id: 'status.hide', defaultMessage: 'Hide post' },
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
filter: { id: 'status.filter', defaultMessage: 'Filter this post' }, filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
@ -206,8 +208,7 @@ class StatusActionBar extends ImmutablePureComponent {
render () { render () {
const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props; const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props;
const { signedIn } = this.context.identity; const { permissions } = this.context.identity;
const anonymousAccess = !me; const anonymousAccess = !me;
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
@ -262,19 +263,19 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
if ((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) { if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
menu.push(null); menu.push(null);
if (accountAdminLink !== undefined) { if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
menu.push({ if (accountAdminLink !== undefined) {
text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: accountAdminLink(status.getIn(['account', 'id'])) });
href: accountAdminLink(status.getIn(['account', 'id'])), }
}); if (statusAdminLink !== undefined) {
menu.push({ text: intl.formatMessage(messages.admin_status), href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')) });
}
} }
if (statusAdminLink !== undefined) { if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
menu.push({ const domain = status.getIn(['account', 'acct']).split('@')[1];
text: intl.formatMessage(messages.admin_status), menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')),
});
} }
} }
} }
@ -308,7 +309,7 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleHideClick} /> <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleHideClick} />
); );
const canReact = signedIn && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions; const canReact = permissions && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions;
const reactButton = ( const reactButton = (
<IconButton <IconButton
className='status__action-bar-button' className='status__action-bar-button'
@ -332,7 +333,7 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} /> <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} /> <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
{ {
signedIn permissions
? <EmojiPickerDropdown className='status__action-bar-button' onPickEmoji={this.handleEmojiPick} button={reactButton} disabled={!canReact} /> ? <EmojiPickerDropdown className='status__action-bar-button' onPickEmoji={this.handleEmojiPick} button={reactButton} disabled={!canReact} />
: reactButton : reactButton
} }
@ -354,6 +355,7 @@ class StatusActionBar extends ImmutablePureComponent {
/> />
</div> </div>
<div className='status__action-bar-spacer' />
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>} <RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</a> </a>

View file

@ -5,18 +5,17 @@ import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
import { isUserTouching } from '../is_mobile'; import { isUserTouching } from '../is_mobile';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
openDropdownId: state.getIn(['dropdown_menu', 'openId']), openDropdownId: state.getIn(['dropdown_menu', 'openId']),
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
}); });
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
onOpen(id, onItemClick, dropdownPlacement, keyboard) { onOpen(id, onItemClick, keyboard) {
dispatch(isUserTouching() ? openModal('ACTIONS', { dispatch(isUserTouching() ? openModal('ACTIONS', {
status, status,
actions: items, actions: items,
onClick: onItemClick, onClick: onItemClick,
}) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey)); }) : openDropdownMenu(id, keyboard, scrollKey));
}, },
onClose(id) { onClose(id) {

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Status from 'flavours/glitch/components/status'; import Status from 'flavours/glitch/components/status';
import { makeGetStatus } from 'flavours/glitch/selectors'; import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
import { import {
replyCompose, replyCompose,
mentionCompose, mentionCompose,
@ -61,6 +61,7 @@ const messages = defineMessages({
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
@ -84,11 +85,7 @@ const makeMapStateToProps = () => {
account: account || props.account, account: account || props.account,
settings: state.get('local_settings'), settings: state.get('local_settings'),
prepend: prepend || props.prepend, prepend: prepend || props.prepend,
pictureInPicture: getPictureInPicture(state, props),
pictureInPicture: {
inUse: state.getIn(['meta', 'layout']) !== 'mobile' && state.get('picture_in_picture').statusId === props.id,
available: state.getIn(['meta', 'layout']) !== 'mobile',
},
}; };
}; };

View file

@ -1,6 +1,3 @@
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'; import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
import 'intersection-observer'; import 'intersection-observer';
import 'requestidlecallback'; import 'requestidlecallback';
import objectFitImages from 'object-fit-images';
objectFitImages();

View file

@ -14,7 +14,7 @@ import { NavLink } from 'react-router-dom';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import AccountNoteContainer from '../containers/account_note_container'; import AccountNoteContainer from '../containers/account_note_container';
import FollowRequestNoteContainer from '../containers/follow_request_note_container'; import FollowRequestNoteContainer from '../containers/follow_request_note_container';
import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
const messages = defineMessages({ const messages = defineMessages({
@ -45,6 +45,7 @@ const messages = defineMessages({
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' }, domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
@ -52,6 +53,7 @@ const messages = defineMessages({
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' }, add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' },
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
@ -155,7 +157,7 @@ class Header extends ImmutablePureComponent {
render () { render () {
const { account, hidden, intl, domain } = this.props; const { account, hidden, intl, domain } = this.props;
const { signedIn } = this.context.identity; const { signedIn, permissions } = this.context.identity;
if (!account) { if (!account) {
return null; return null;
@ -187,7 +189,7 @@ class Header extends ImmutablePureComponent {
} }
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) { if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />; bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
} }
if (me !== account.get('id')) { if (me !== account.get('id')) {
@ -244,6 +246,7 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
@ -291,9 +294,14 @@ class Header extends ImmutablePureComponent {
} }
} }
if (account.get('id') !== me && (this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && accountAdminLink) { if (account.get('id') !== me && ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && accountAdminLink) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: accountAdminLink(account.get('id')) }); if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && accountAdminLink) {
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: accountAdminLink(account.get('id')) });
}
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: remoteDomain }), href: `/admin/instances/${remoteDomain}` });
}
} }
const content = { __html: account.get('note_emojified') }; const content = { __html: account.get('note_emojified') };

View file

@ -12,6 +12,7 @@ const messages = defineMessages({
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
@ -46,6 +47,7 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' }); menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });

View file

@ -310,7 +310,7 @@ class ComposeForm extends ImmutablePureComponent {
<ReplyIndicatorContainer /> <ReplyIndicatorContainer />
<div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}> <div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
<AutosuggestInput <AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)} placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={spoilerText} value={spoilerText}

View file

@ -2,7 +2,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Overlay from 'react-overlays/lib/Overlay'; import Overlay from 'react-overlays/Overlay';
// Components. // Components.
import IconButton from 'flavours/glitch/components/icon_button'; import IconButton from 'flavours/glitch/components/icon_button';
@ -45,7 +45,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
}; };
// Toggles opening and closing the dropdown. // Toggles opening and closing the dropdown.
handleToggle = ({ target, type }) => { handleToggle = ({ type }) => {
const { onModalOpen } = this.props; const { onModalOpen } = this.props;
const { open } = this.state; const { open } = this.state;
@ -59,11 +59,9 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
} }
} }
} else { } else {
const { top } = target.getBoundingClientRect();
if (this.state.open && this.activeElement) { if (this.state.open && this.activeElement) {
this.activeElement.focus({ preventScroll: true }); this.activeElement.focus({ preventScroll: true });
} }
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
this.setState({ open: !this.state.open, openedViaKeyboard: type !== 'click' }); this.setState({ open: !this.state.open, openedViaKeyboard: type !== 'click' });
} }
} }
@ -158,6 +156,18 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
}; };
} }
setTargetRef = c => {
this.target = c;
}
findTarget = () => {
return this.target;
}
handleOverlayEnter = (state) => {
this.setState({ placement: state.placement });
}
// Rendering. // Rendering.
render () { render () {
const { const {
@ -179,6 +189,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
<div <div
className={classNames('privacy-dropdown', placement, { active: open })} className={classNames('privacy-dropdown', placement, { active: open })}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
ref={this.setTargetRef}
> >
<div className={classNames('privacy-dropdown__value', { active })}> <div className={classNames('privacy-dropdown__value', { active })}>
<IconButton <IconButton
@ -204,18 +215,26 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
containerPadding={20} containerPadding={20}
placement={placement} placement={placement}
show={open} show={open}
target={this} flip
target={this.findTarget}
container={container} container={container}
popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}
> >
<DropdownMenu {({ props, placement }) => (
items={items} <div {...props}>
renderItemContents={renderItemContents} <div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
onChange={onChange} <DropdownMenu
onClose={this.handleClose} items={items}
value={value} renderItemContents={renderItemContents}
openedViaKeyboard={this.state.openedViaKeyboard} onChange={onChange}
closeOnChange={closeOnChange} onClose={this.handleClose}
/> value={value}
openedViaKeyboard={this.state.openedViaKeyboard}
closeOnChange={closeOnChange}
/>
</div>
</div>
)}
</Overlay> </Overlay>
</div> </div>
); );

View file

@ -1,7 +1,6 @@
// Package imports. // Package imports.
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import spring from 'react-motion/lib/spring';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import classNames from 'classnames'; import classNames from 'classnames';
@ -10,15 +9,8 @@ import Icon from 'flavours/glitch/components/icon';
// Utils. // Utils.
import { withPassive } from 'flavours/glitch/utils/dom_helpers'; import { withPassive } from 'flavours/glitch/utils/dom_helpers';
import Motion from '../../ui/util/optional_motion';
import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; import { assignHandlers } from 'flavours/glitch/utils/react_helpers';
// The spring to use with our motion.
const springMotion = spring(1, {
damping: 35,
stiffness: 400,
});
// The component. // The component.
export default class ComposerOptionsDropdownContent extends React.PureComponent { export default class ComposerOptionsDropdownContent extends React.PureComponent {
@ -44,7 +36,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
}; };
state = { state = {
mounted: false,
value: this.props.openedViaKeyboard ? this.props.items[0].name : undefined, value: this.props.openedViaKeyboard ? this.props.items[0].name : undefined,
}; };
@ -56,7 +47,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
} }
// Stores our node in `this.node`. // Stores our node in `this.node`.
handleRef = (node) => { setRef = (node) => {
this.node = node; this.node = node;
} }
@ -69,7 +60,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
} else { } else {
this.node.firstChild.focus({ preventScroll: true }); this.node.firstChild.focus({ preventScroll: true });
} }
this.setState({ mounted: true });
} }
// On unmounting, we remove our listeners. // On unmounting, we remove our listeners.
@ -191,7 +181,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
// Rendering. // Rendering.
render () { render () {
const { mounted } = this.state;
const { const {
items, items,
onChange, onChange,
@ -201,36 +190,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
// The result. // The result.
return ( return (
<Motion <div style={{ ...style }} role='listbox' ref={this.setRef}>
defaultStyle={{ {!!items && items.map((item, i) => this.renderItem(item, i))}
opacity: 0, </div>
scaleX: 0.85,
scaleY: 0.75,
}}
style={{
opacity: springMotion,
scaleX: springMotion,
scaleY: springMotion,
}}
>
{({ opacity, scaleX, scaleY }) => (
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// react-overlays
<div
className='privacy-dropdown__dropdown'
ref={this.handleRef}
role='listbox'
style={{
...style,
opacity: opacity,
transform: mounted ? `scale(${scaleX}, ${scaleY})` : null,
}}
>
{!!items && items.map((item, i) => this.renderItem(item, i))}
</div>
)}
</Motion>
); );
} }

View file

@ -2,7 +2,7 @@ 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 { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
import Overlay from 'react-overlays/lib/Overlay'; import Overlay from 'react-overlays/Overlay';
import classNames from 'classnames'; import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
@ -155,9 +155,6 @@ class EmojiPickerMenu extends React.PureComponent {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onPick: PropTypes.func.isRequired, onPick: PropTypes.func.isRequired,
style: PropTypes.object, style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
skinTone: PropTypes.number.isRequired, skinTone: PropTypes.number.isRequired,
onSkinTone: PropTypes.func.isRequired, onSkinTone: PropTypes.func.isRequired,
@ -327,14 +324,13 @@ class EmojiPickerDropdown extends React.PureComponent {
state = { state = {
active: false, active: false,
loading: false, loading: false,
placement: null,
}; };
setRef = (c) => { setRef = (c) => {
this.dropdown = c; this.dropdown = c;
} }
onShowDropdown = ({ target }) => { onShowDropdown = () => {
this.setState({ active: true }); this.setState({ active: true });
if (!EmojiPicker) { if (!EmojiPicker) {
@ -349,9 +345,6 @@ class EmojiPickerDropdown extends React.PureComponent {
this.setState({ loading: false, active: false }); this.setState({ loading: false, active: false });
}); });
} }
const { top } = target.getBoundingClientRect();
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
} }
onHideDropdown = () => { onHideDropdown = () => {
@ -385,7 +378,7 @@ class EmojiPickerDropdown extends React.PureComponent {
render () { render () {
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props; const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
const title = intl.formatMessage(messages.emoji); const title = intl.formatMessage(messages.emoji);
const { active, loading, placement } = this.state; const { active, loading } = this.state;
return ( return (
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}> <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
@ -397,16 +390,22 @@ class EmojiPickerDropdown extends React.PureComponent {
/>} />}
</div> </div>
<Overlay show={active} placement={placement} target={this.findTarget}> <Overlay show={active} placement={'bottom'} target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
<EmojiPickerMenu {({ props, placement })=> (
custom_emojis={this.props.custom_emojis} <div {...props} style={{ ...props.style, width: 299 }}>
loading={loading} <div className={`dropdown-animation ${placement}`}>
onClose={this.onHideDropdown} <EmojiPickerMenu
onPick={onPickEmoji} custom_emojis={this.props.custom_emojis}
onSkinTone={onSkinTone} loading={loading}
skinTone={skinTone} onClose={this.onHideDropdown}
frequentlyUsedEmojis={frequentlyUsedEmojis} onPick={onPickEmoji}
/> onSkinTone={onSkinTone}
skinTone={skinTone}
frequentlyUsedEmojis={frequentlyUsedEmojis}
/>
</div>
</div>
)}
</Overlay> </Overlay>
</div> </div>
); );

View file

@ -2,9 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl';
import TextIconButton from './text_icon_button'; import TextIconButton from './text_icon_button';
import Overlay from 'react-overlays/lib/Overlay'; import Overlay from 'react-overlays/Overlay';
import Motion from 'flavours/glitch/features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import classNames from 'classnames'; import classNames from 'classnames';
import { languages as preloadedLanguages } from 'flavours/glitch/initial_state'; import { languages as preloadedLanguages } from 'flavours/glitch/initial_state';
@ -22,10 +20,8 @@ const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
class LanguageDropdownMenu extends React.PureComponent { class LanguageDropdownMenu extends React.PureComponent {
static propTypes = { static propTypes = {
style: PropTypes.object,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired, frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired,
placement: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
@ -37,7 +33,6 @@ class LanguageDropdownMenu extends React.PureComponent {
}; };
state = { state = {
mounted: false,
searchValue: '', searchValue: '',
}; };
@ -50,7 +45,6 @@ class LanguageDropdownMenu extends React.PureComponent {
componentDidMount () { componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
this.setState({ mounted: true });
// Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need
// to wait for a frame before focusing // to wait for a frame before focusing
@ -222,29 +216,22 @@ class LanguageDropdownMenu extends React.PureComponent {
} }
render () { render () {
const { style, placement, intl } = this.props; const { intl } = this.props;
const { mounted, searchValue } = this.state; const { searchValue } = this.state;
const isSearching = searchValue !== ''; const isSearching = searchValue !== '';
const results = this.search(); const results = this.search();
return ( return (
<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 }) }}> <div ref={this.setRef}>
{({ opacity, scaleX, scaleY }) => ( <div className='emoji-mart-search'>
// It should not be transformed when mounting because the resulting <input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} />
// size will be used to determine the coordinate of the menu by <button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
// react-overlays </div>
<div className={`language-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
<div className='emoji-mart-search'>
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} />
<button className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
</div>
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}> <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
{results.map(this.renderItem)} {results.map(this.renderItem)}
</div> </div>
</div> </div>
)}
</Motion>
); );
} }
@ -266,14 +253,11 @@ class LanguageDropdown extends React.PureComponent {
placement: 'bottom', placement: 'bottom',
}; };
handleToggle = ({ target }) => { handleToggle = () => {
const { top } = target.getBoundingClientRect();
if (this.state.open && this.activeElement) { if (this.state.open && this.activeElement) {
this.activeElement.focus({ preventScroll: true }); this.activeElement.focus({ preventScroll: true });
} }
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
this.setState({ open: !this.state.open }); this.setState({ open: !this.state.open });
} }
@ -293,13 +277,25 @@ class LanguageDropdown extends React.PureComponent {
onChange(value); onChange(value);
} }
setTargetRef = c => {
this.target = c;
}
findTarget = () => {
return this.target;
}
handleOverlayEnter = (state) => {
this.setState({ placement: state.placement });
}
render () { render () {
const { value, intl, frequentlyUsedLanguages } = this.props; const { value, intl, frequentlyUsedLanguages } = this.props;
const { open, placement } = this.state; const { open, placement } = this.state;
return ( return (
<div className={classNames('privacy-dropdown', { active: open })}> <div className={classNames('privacy-dropdown', placement, { active: open })}>
<div className='privacy-dropdown__value'> <div className='privacy-dropdown__value' ref={this.setTargetRef} >
<TextIconButton <TextIconButton
className='privacy-dropdown__value-icon' className='privacy-dropdown__value-icon'
label={value && value.toUpperCase()} label={value && value.toUpperCase()}
@ -309,15 +305,20 @@ class LanguageDropdown extends React.PureComponent {
/> />
</div> </div>
<Overlay show={open} placement={placement} target={this}> <Overlay show={open} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
<LanguageDropdownMenu {({ props, placement }) => (
value={value} <div {...props}>
frequentlyUsedLanguages={frequentlyUsedLanguages} <div className={`dropdown-animation language-dropdown__dropdown ${placement}`} >
onClose={this.handleClose} <LanguageDropdownMenu
onChange={this.handleChange} value={value}
placement={placement} frequentlyUsedLanguages={frequentlyUsedLanguages}
intl={intl} onClose={this.handleClose}
/> onChange={this.handleChange}
intl={intl}
/>
</div>
</div>
)}
</Overlay> </Overlay>
</div> </div>
); );

View file

@ -3,13 +3,12 @@ import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import spring from 'react-motion/lib/spring';
import { import {
injectIntl, injectIntl,
FormattedMessage, FormattedMessage,
defineMessages, defineMessages,
} from 'react-intl'; } from 'react-intl';
import Overlay from 'react-overlays/lib/Overlay'; import Overlay from 'react-overlays/Overlay';
// Components. // Components.
import Icon from 'flavours/glitch/components/icon'; import Icon from 'flavours/glitch/components/icon';
@ -17,7 +16,6 @@ import Icon from 'flavours/glitch/components/icon';
// Utils. // Utils.
import { focusRoot } from 'flavours/glitch/utils/dom_helpers'; import { focusRoot } from 'flavours/glitch/utils/dom_helpers';
import { searchEnabled } from 'flavours/glitch/initial_state'; import { searchEnabled } from 'flavours/glitch/initial_state';
import Motion from '../../ui/util/optional_motion';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
@ -26,31 +24,20 @@ const messages = defineMessages({
class SearchPopout extends React.PureComponent { class SearchPopout extends React.PureComponent {
static propTypes = {
style: PropTypes.object,
};
render () { render () {
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, zIndex: 2 }}> <div className='search-popout'>
<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 }) }}> <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
{({ opacity, scaleX, scaleY }) => (
<div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
<h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
<ul> <ul>
<li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li> <li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li>
<li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> <li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
<li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> <li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
<li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li> <li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li>
</ul> </ul>
{extraInformation} {extraInformation}
</div>
)}
</Motion>
</div> </div>
); );
} }
@ -136,6 +123,10 @@ class Search extends React.PureComponent {
} }
} }
findTarget = () => {
return this.searchForm;
}
render () { render () {
const { intl, value, submitted } = this.props; const { intl, value, submitted } = this.props;
const { expanded } = this.state; const { expanded } = this.state;
@ -161,8 +152,14 @@ class Search extends React.PureComponent {
<Icon id='search' className={hasValue ? '' : 'active'} /> <Icon id='search' className={hasValue ? '' : 'active'} />
<Icon id='times-circle' className={hasValue ? 'active' : ''} /> <Icon id='times-circle' className={hasValue ? 'active' : ''} />
</div> </div>
<Overlay show={expanded && !hasValue} placement='bottom' target={this} container={this}> <Overlay show={expanded && !hasValue} placement='bottom' target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
<SearchPopout /> {({ props, placement }) => (
<div {...props} style={{ ...props.style, width: 285, zIndex: 2 }}>
<div className={`dropdown-animation ${placement}`}>
<SearchPopout />
</div>
</div>
)}
</Overlay> </Overlay>
</div> </div>
); );

View file

@ -43,13 +43,13 @@ export default class Upload extends ImmutablePureComponent {
{({ scale }) => ( {({ scale }) => (
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}> <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
<div className='compose-form__upload__actions'> <div className='compose-form__upload__actions'>
<button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button> <button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
{!!media.get('unattached') && (<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)} <button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
</div> </div>
{(media.get('description') || '').length === 0 && !!media.get('unattached') && ( {(media.get('description') || '').length === 0 && (
<div className='compose-form__upload__warning'> <div className='compose-form__upload__warning'>
<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button> <button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
</div> </div>
)} )}
</div> </div>

View file

@ -46,7 +46,7 @@ const mapDispatchToProps = (dispatch) => ({
}, },
onDoodleOpen() { onDoodleOpen() {
dispatch(openModal('DOODLE', { noEsc: true })); dispatch(openModal('DOODLE', { noEsc: true, noClose: true }));
}, },
}); });

View file

@ -0,0 +1,89 @@
import { debounce } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import ColumnHeader from 'flavours/glitch/components/column_header';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
import Column from 'flavours/glitch/features/ui/components/column';
import { Helmet } from 'react-helmet';
import Hashtag from 'flavours/glitch/components/hashtag';
import { expandFollowedHashtags, fetchFollowedHashtags } from 'flavours/glitch/actions/tags';
const messages = defineMessages({
heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
});
const mapStateToProps = state => ({
hashtags: state.getIn(['followed_tags', 'items']),
isLoading: state.getIn(['followed_tags', 'isLoading'], true),
hasMore: !!state.getIn(['followed_tags', 'next']),
});
export default @connect(mapStateToProps)
@injectIntl
class FollowedTags extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hashtags: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
multiColumn: PropTypes.bool,
};
componentDidMount() {
this.props.dispatch(fetchFollowedHashtags());
};
handleLoadMore = debounce(() => {
this.props.dispatch(expandFollowedHashtags());
}, 300, { leading: true });
render () {
const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props;
const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.' />;
return (
<Column bindToDocument={!multiColumn}>
<ColumnHeader
icon='hashtag'
title={intl.formatMessage(messages.heading)}
showBackButton
multiColumn={multiColumn}
/>
<ScrollableList
scrollKey='followed_tags'
emptyMessage={emptyMessage}
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
bindToDocument={!multiColumn}
>
{hashtags.map((hashtag) => (
<Hashtag
key={hashtag.get('name')}
name={hashtag.get('name')}
to={`/tags/${hashtag.get('name')}`}
withGraph={false}
// Taken from ImmutableHashtag. Should maybe refactor ImmutableHashtag to accept more options?
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
/>
))}
</ScrollableList>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}

View file

@ -13,7 +13,6 @@ const messages = defineMessages({
general: { id: 'settings.general', defaultMessage: 'General' }, general: { id: 'settings.general', defaultMessage: 'General' },
compose: { id: 'settings.compose_box_opts', defaultMessage: 'Compose box' }, compose: { id: 'settings.compose_box_opts', defaultMessage: 'Compose box' },
content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' }, content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' },
filters: { id: 'settings.filters', defaultMessage: 'Filters' },
collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' }, collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' },
media: { id: 'settings.media', defaultMessage: 'Media' }, media: { id: 'settings.media', defaultMessage: 'Media' },
preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' }, preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' },

View file

@ -7,7 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import { me, maxReactions } from 'flavours/glitch/initial_state'; import { me, maxReactions } from 'flavours/glitch/initial_state';
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
import classNames from 'classnames'; import classNames from 'classnames';
import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container'; import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container';
const messages = defineMessages({ const messages = defineMessages({
@ -36,6 +36,7 @@ const messages = defineMessages({
embed: { id: 'status.embed', defaultMessage: 'Embed' }, embed: { id: 'status.embed', defaultMessage: 'Embed' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
}); });
@ -186,19 +187,19 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) { if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
menu.push(null); menu.push(null);
if (accountAdminLink !== undefined) { if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
menu.push({ if (accountAdminLink !== undefined) {
text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: accountAdminLink(status.getIn(['account', 'id'])) });
href: accountAdminLink(status.getIn(['account', 'id'])), }
}); if (statusAdminLink !== undefined) {
menu.push({ text: intl.formatMessage(messages.admin_status), href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')) });
}
} }
if (statusAdminLink !== undefined) { if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
menu.push({ const domain = status.getIn(['account', 'acct']).split('@')[1];
text: intl.formatMessage(messages.admin_status), menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')),
});
} }
} }
} }

View file

@ -43,7 +43,10 @@ class DetailedStatus extends ImmutablePureComponent {
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
compact: PropTypes.bool, compact: PropTypes.bool,
showMedia: PropTypes.bool, showMedia: PropTypes.bool,
usingPiP: PropTypes.bool, pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
onToggleMediaVisibility: PropTypes.func, onToggleMediaVisibility: PropTypes.func,
onReactionAdd: PropTypes.func.isRequired, onReactionAdd: PropTypes.func.isRequired,
onReactionRemove: PropTypes.func.isRequired, onReactionRemove: PropTypes.func.isRequired,
@ -124,7 +127,7 @@ class DetailedStatus extends ImmutablePureComponent {
render () { render () {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const { expanded, onToggleHidden, settings, usingPiP, intl } = this.props; const { expanded, onToggleHidden, settings, pictureInPicture, intl } = this.props;
const outerStyle = { boxSizing: 'border-box' }; const outerStyle = { boxSizing: 'border-box' };
const { compact } = this.props; const { compact } = this.props;
@ -157,7 +160,7 @@ class DetailedStatus extends ImmutablePureComponent {
outerStyle.height = `${this.state.height}px`; outerStyle.height = `${this.state.height}px`;
} }
if (usingPiP) { if (pictureInPicture.get('inUse')) {
media.push(<PictureInPicturePlaceholder />); media.push(<PictureInPicturePlaceholder />);
mediaIcons.push('video-camera'); mediaIcons.push('video-camera');
} else if (status.get('media_attachments').size > 0) { } else if (status.get('media_attachments').size > 0) {

View file

@ -43,7 +43,7 @@ import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initReport } from 'flavours/glitch/actions/reports'; import { initReport } from 'flavours/glitch/actions/reports';
import { initBoostModal } from 'flavours/glitch/actions/boosts'; import { initBoostModal } from 'flavours/glitch/actions/boosts';
import { makeGetStatus } from 'flavours/glitch/selectors'; import { makeCustomEmojiMap, makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import ColumnBackButton from 'flavours/glitch/components/column_back_button'; import ColumnBackButton from 'flavours/glitch/components/column_back_button';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
@ -69,11 +69,12 @@ const messages = defineMessages({
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
tootHeading: { id: 'column.toot', defaultMessage: 'Toots and replies' }, tootHeading: { id: 'account.posts_with_replies', defaultMessage: 'Posts and replies' },
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const getAncestorsIds = createSelector([ const getAncestorsIds = createSelector([
(_, { id }) => id, (_, { id }) => id,
@ -131,11 +132,12 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId }); const status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List();
let ancestorsIds = Immutable.List();
let descendantsIds = Immutable.List(); let descendantsIds = Immutable.List();
if (status) { if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
descendantsIds = getDescendantsIds(state, { id: status.get('id') }); descendantsIds = getDescendantsIds(state, { id: status.get('id') });
} }
@ -147,7 +149,7 @@ const makeMapStateToProps = () => {
settings: state.get('local_settings'), settings: state.get('local_settings'),
askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0, askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']), domain: state.getIn(['meta', 'domain']),
usingPiP: state.get('picture_in_picture').statusId === props.params.statusId, pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
}; };
}; };
@ -192,7 +194,10 @@ class Status extends ImmutablePureComponent {
askReplyConfirmation: PropTypes.bool, askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
usingPiP: PropTypes.bool, pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
}; };
state = { state = {
@ -619,7 +624,7 @@ class Status extends ImmutablePureComponent {
render () { render () {
let ancestors, descendants; let ancestors, descendants;
const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, usingPiP } = this.props; const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen } = this.state; const { fullscreen } = this.state;
if (isLoading) { if (isLoading) {
@ -699,7 +704,7 @@ class Status extends ImmutablePureComponent {
domain={domain} domain={domain}
showMedia={this.state.showMedia} showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility} onToggleMediaVisibility={this.handleToggleMediaVisibility}
usingPiP={usingPiP} pictureInPicture={pictureInPicture}
/> />
<ActionBar <ActionBar

View file

@ -320,7 +320,7 @@ class FocalPointModal extends ImmutablePureComponent {
<React.Fragment> <React.Fragment>
<label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label> <label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
<Button disabled={isUploadingThumbnail} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} /> <Button disabled={isUploadingThumbnail || !media.get('unattached')} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
<label> <label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span> <span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>

View file

@ -52,6 +52,8 @@ class LinkFooter extends React.PureComponent {
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS); const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
const canProfileDirectory = profileDirectory; const canProfileDirectory = profileDirectory;
const DividingCircle = <span aria-hidden>{' · '}</span>;
return ( return (
<div className='link-footer'> <div className='link-footer'>
<p> <p>
@ -60,17 +62,17 @@ class LinkFooter extends React.PureComponent {
<Link key='about' to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link> <Link key='about' to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
{canInvite && ( {canInvite && (
<> <>
{' · '} {DividingCircle}
<a key='invites' href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a> <a key='invites' href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
</> </>
)} )}
{canProfileDirectory && ( {canProfileDirectory && (
<> <>
{' · '} {DividingCircle}
<Link key='directory' to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link> <Link key='directory' to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
</> </>
)} )}
{' · '} {DividingCircle}
<Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link> <Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
</p> </p>
@ -78,13 +80,13 @@ class LinkFooter extends React.PureComponent {
<strong>Mastodon, queer.af edition</strong>: <strong>Mastodon, queer.af edition</strong>:
{' '} {' '}
<a href='https://joinmastodon.org' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a> <a href='https://joinmastodon.org' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a>
{' · '} {DividingCircle}
<a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='footer.get_app' defaultMessage='Get the app' /></a> <a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='footer.get_app' defaultMessage='Get the app' /></a>
{' · '} {DividingCircle}
<Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link> <Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link>
{' · '} {DividingCircle}
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a> <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
{' · '} {DividingCircle}
v{version} v{version}
</p> </p>
</div> </div>

View file

@ -116,13 +116,16 @@ export default class ModalRoot extends React.PureComponent {
this._modal = c; this._modal = c;
} }
// prevent closing of modal when clicking the overlay
noop = () => {}
render () { render () {
const { type, props, ignoreFocus } = this.props; const { type, props, ignoreFocus } = this.props;
const { backgroundColor } = this.state; const { backgroundColor } = this.state;
const visible = !!type; const visible = !!type;
return ( return (
<Base backgroundColor={backgroundColor} onClose={this.handleClose} noEsc={props ? props.noEsc : false} ignoreFocus={ignoreFocus}> <Base backgroundColor={backgroundColor} onClose={props && props.noClose ? this.noop : this.handleClose} noEsc={props ? props.noEsc : false} ignoreFocus={ignoreFocus}>
{visible && ( {visible && (
<> <>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>

View file

@ -30,7 +30,7 @@ const SignInBanner = () => {
return ( return (
<div className='sign-in-banner'> <div className='sign-in-banner'>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.' /></p> <p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.' /></p>
<a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
{signupButton} {signupButton}
</div> </div>

View file

@ -50,6 +50,7 @@ export default class VideoModal extends ImmutablePureComponent {
autoPlay={options.autoPlay} autoPlay={options.autoPlay}
volume={options.defaultVolume} volume={options.defaultVolume}
onCloseVideo={onClose} onCloseVideo={onClose}
autoFocus
detailed detailed
alt={media.get('description')} alt={media.get('description')}
/> />

View file

@ -42,6 +42,7 @@ import {
FollowRequests, FollowRequests,
FavouritedStatuses, FavouritedStatuses,
BookmarkedStatuses, BookmarkedStatuses,
FollowedTags,
ListTimeline, ListTimeline,
Blocks, Blocks,
DomainBlocks, DomainBlocks,
@ -56,7 +57,7 @@ import {
PrivacyPolicy, PrivacyPolicy,
} from './util/async-components'; } from './util/async-components';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import initialState, { me, owner, singleUserMode, showTrends } from '../../initial_state'; import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state';
import { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding'; import { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
@ -177,7 +178,7 @@ class SwitchingColumnsArea extends React.PureComponent {
} }
} else if (singleUserMode && owner && initialState?.accounts[owner]) { } else if (singleUserMode && owner && initialState?.accounts[owner]) {
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />; redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
} else if (showTrends) { } else if (showTrends && trendsAsLanding) {
redirect = <Redirect from='/' to='/explore' exact />; redirect = <Redirect from='/' to='/explore' exact />;
} else { } else {
redirect = <Redirect from='/' to='/about' exact />; redirect = <Redirect from='/' to='/about' exact />;
@ -230,6 +231,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
<WrappedRoute path='/blocks' component={Blocks} content={children} /> <WrappedRoute path='/blocks' component={Blocks} content={children} />
<WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} /> <WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} />
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
<WrappedRoute path='/mutes' component={Mutes} content={children} /> <WrappedRoute path='/mutes' component={Mutes} content={children} />
<WrappedRoute path='/lists' component={Lists} content={children} /> <WrappedRoute path='/lists' component={Lists} content={children} />
<WrappedRoute path='/getting-started-misc' component={GettingStartedMisc} content={children} /> <WrappedRoute path='/getting-started-misc' component={GettingStartedMisc} content={children} />

View file

@ -98,6 +98,10 @@ export function FavouritedStatuses () {
return import(/* webpackChunkName: "flavours/glitch/async/favourited_statuses" */'flavours/glitch/features/favourited_statuses'); return import(/* webpackChunkName: "flavours/glitch/async/favourited_statuses" */'flavours/glitch/features/favourited_statuses');
} }
export function FollowedTags () {
return import(/* webpackChunkName: "flavours/glitch/async/followed_tags" */'flavours/glitch/features/followed_tags');
}
export function BookmarkedStatuses () { export function BookmarkedStatuses () {
return import(/* webpackChunkName: "flavours/glitch/async/bookmarked_statuses" */'flavours/glitch/features/bookmarked_statuses'); return import(/* webpackChunkName: "flavours/glitch/async/bookmarked_statuses" */'flavours/glitch/features/bookmarked_statuses');
} }

View file

@ -124,6 +124,7 @@ class Video extends React.PureComponent {
volume: PropTypes.number, volume: PropTypes.number,
muted: PropTypes.bool, muted: PropTypes.bool,
componentIndex: PropTypes.number, componentIndex: PropTypes.number,
autoFocus: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -537,7 +538,7 @@ class Video extends React.PureComponent {
} }
render () { render () {
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, editable, blurhash } = this.props; const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = Math.min((currentTime / duration) * 100, 100); const progress = Math.min((currentTime / duration) * 100, 100);
const playerStyle = {}; const playerStyle = {};
@ -635,7 +636,7 @@ class Video extends React.PureComponent {
<div className='video-player__buttons-bar'> <div className='video-player__buttons-bar'>
<div className='video-player__buttons left'> <div className='video-player__buttons left'>
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={autoFocus}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> <div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>

View file

@ -76,6 +76,7 @@
* @property {boolean} timeline_preview * @property {boolean} timeline_preview
* @property {string} title * @property {string} title
* @property {boolean} trends * @property {boolean} trends
* @property {boolean} trends_as_landing_page
* @property {boolean} unfollow_modal * @property {boolean} unfollow_modal
* @property {boolean} use_blurhash * @property {boolean} use_blurhash
* @property {boolean=} use_pending_items * @property {boolean=} use_pending_items
@ -137,6 +138,7 @@ export const singleUserMode = getMeta('single_user_mode');
export const source_url = getMeta('source_url'); export const source_url = getMeta('source_url');
export const timelinePreview = getMeta('timeline_preview'); export const timelinePreview = getMeta('timeline_preview');
export const title = getMeta('title'); export const title = getMeta('title');
export const trendsAsLanding = getMeta('trends_as_landing_page');
export const unfollowModal = getMeta('unfollow_modal'); export const unfollowModal = getMeta('unfollow_modal');
export const useBlurhash = getMeta('use_blurhash'); export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items'); export const usePendingItems = getMeta('use_pending_items');

View file

@ -23,15 +23,14 @@ function loadPolyfills() {
); );
// Latest version of Firefox and Safari do not have IntersectionObserver. // Latest version of Firefox and Safari do not have IntersectionObserver.
// Edge does not have requestIdleCallback and object-fit CSS property. // Edge does not have requestIdleCallback.
// This avoids shipping them all the polyfills. // This avoids shipping them all the polyfills.
const needsExtraPolyfills = !( const needsExtraPolyfills = !(
window.AbortController && window.AbortController &&
window.IntersectionObserver && window.IntersectionObserver &&
window.IntersectionObserverEntry && window.IntersectionObserverEntry &&
'isIntersecting' in IntersectionObserverEntry.prototype && 'isIntersecting' in IntersectionObserverEntry.prototype &&
window.requestIdleCallback && window.requestIdleCallback
'object-fit' in (new Image()).style
); );
return Promise.all([ return Promise.all([

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/af.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/ar.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,6 @@
{
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/ast.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,6 @@
{
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -0,0 +1 @@
{}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/bg.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,6 @@
{
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/bn.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,6 @@
{
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/br.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,6 @@
{
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -0,0 +1 @@
{}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/ca.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,6 @@
{
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/ckb.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,6 @@
{
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/co.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,6 @@
{
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -1,180 +0,0 @@
import inherited from 'mastodon/locales/cs.json';
const messages = {
'about.fork_disclaimer': 'Glitch-soc je svobodný software s otevřeným zdrojovým kódem založený na Mastodonu.',
'settings.layout_opts': 'Možnosti rozvržení',
'settings.layout': 'Rozložení:',
'layout.current_is': 'Nastavené rozložení je:',
'layout.auto': 'Automatické',
'layout.desktop': 'Desktop',
'layout.mobile': 'Mobil',
'layout.hint.auto': 'Vybrat rozložení automaticky v závislosti na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.',
'layout.hint.desktop': 'Použít vícesloupcové rozložení nezávisle na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.',
'layout.hint.single': 'Použít jednosloupcové rozložení nezávisle na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.',
'navigation_bar.app_settings': 'Nastavení aplikace',
'navigation_bar.featured_users': 'Vybraní uživatelé',
'endorsed_accounts_editor.endorsed_accounts': 'Vybrané účty',
'navigation_bar.info': 'Rozšířené informace',
'navigation_bar.misc': 'Různé',
'navigation_bar.keyboard_shortcuts': 'Klávesové zkratky',
'getting_started.onboarding': 'Ukaž mi to tu',
'onboarding.skip': 'Přeskočit',
'onboarding.next': 'Další',
'onboarding.done': 'Hotovo',
'onboarding.page_one.federation': '{domain} je \'instance\' Mastodonu. Mastodon je síť nezávislých serverů, které jsou spolu propojené do jedné velké sociální sítě. Těmto serverům říkáme instance.',
'onboarding.page_one.handle': 'Jste na instanci {domain}, takže celá adresa vašeho profilu je {handle}',
'onboarding.page_one.welcome': 'Vítá vás {domain}!',
'onboarding.page_two.compose': 'Příspěvky se píší v levém sloupci. Pomocí ikon pod příspěvkem k němu můžete připojit obrázky, změnit úroveň soukromí nebo přidat varování o obsahu.',
'onboarding.page_three.search': 'Pomocí vyhledávací lišty můžete hledat lidi nebo hashtagy. Pokud hledáte někoho z jiné instance, musíte použít celou adresu jeho profilu.',
'onboarding.page_three.profile': 'Upravte si svůj profil a nastavte si profilový obrázek, jméno, a krátký text o sobě. Naleznete tam i další možnosti nastavení.',
'onboarding.page_four.home': 'Domovská časová osa zobrazuje příspěvky od lidí, které sledujete.',
'onboarding.page_four.notifications': 'Notifikace se zobrazí, když s vámi někdo interaguje.',
'onboarding.page_five.public_timelines': 'Místní časová osa zobrazuje veřejné příspěvky všech uživatelů instance {domain}. Federovaná časová osa zobrazí příspěvky od všech, koho uživatelé instance {domain} sledují. Tyto veřejné časové osy jsou skvělý způsob, jak objevit nové lidi.',
'onboarding.page_six.almost_done': 'Skoro hotovo...',
'onboarding.page_six.github': 'Na serveru {domain} běží Glitchsoc. Glitchsoc je přátelský {fork} programu {Mastodon}, a je kompatibilní s jakoukoliv jinou mastodoní instancí nebo aplikací. Glitchsoc je zcela svobodný a má otevřený zdrojový kód. Na stránce {github} můžete hlásit chyby, žádat o nové funkce, nebo ke kódu vlastnoručně přispět.',
'onboarding.page_six.apps_available': 'Jsou dostupné {apps} pro iOS, Android i jiné platformy.',
'onboarding.page_six.various_app': 'mobilní aplikace',
'onboarding.page_six.appetoot': 'Veselé mastodonění!',
'settings.auto_collapse': 'Automaticky sbalit',
'settings.auto_collapse_all': 'Všechno',
'settings.auto_collapse_lengthy': 'Dlouhé příspěvky',
'settings.auto_collapse_media': 'Příspěvky s přílohami',
'settings.auto_collapse_notifications': 'Oznámení',
'settings.auto_collapse_reblogs': 'Boosty',
'settings.auto_collapse_replies': 'Odpovědi',
'settings.show_action_bar': 'Zobrazit ve sbalených příspěvcích tlačítka s akcemi',
'settings.close': 'Zavřít',
'settings.collapsed_statuses': 'Sbalené příspěvky',
'settings.confirm_boost_missing_media_description': 'Zobrazit potvrzovací dialog před boostnutím příspěvku s chybějícími popisky obrázků',
'boost_modal.missing_description': 'Příspěvek obsahuje obrázky bez popisků',
'settings.enable_collapsed': 'Povolit sbalené příspěvky',
'settings.enable_collapsed_hint': 'U sbalených příspěvků je část jejich obsahu skrytá, aby zabraly méně místa na obrazovce. (Tohle není stejná funkce jako varování o obsahu.)',
'settings.general': 'Obecné',
'settings.hicolor_privacy_icons': 'Barevné ikony soukromí',
'settings.hicolor_privacy_icons.hint': 'Zobrazit ikony úrovně soukromí příspěvků v jasných, snadno rozlišitelných barvách',
'settings.image_backgrounds': 'Obrázkové pozadí',
'settings.image_backgrounds_media': 'Náhled médií ve sbalených příspěvcích',
'settings.image_backgrounds_media_hint': 'Pokud jsou k příspěvku přiložena média, použije se první z nich jako pozadí',
'settings.image_backgrounds_users': 'Nastavit sbaleným příspěvkům obrázkové pozadí',
'settings.inline_preview_cards': 'Zobrazit v časové ose náhledy externích odkazů',
'settings.media': 'Média',
'settings.media_letterbox': 'Neořezávat obrázky',
'settings.media_letterbox_hint': 'Místo výřezu obrázku zobrazit obrázek celý, doplněný podle potřeby o prázdné okraje',
'settings.media_fullwidth': 'Zobrazit náhledy v plné šířce',
'settings.notifications_opts': 'Možnosti oznámení',
'settings.notifications.tab_badge': 'Zobrazit počet nepřečtených oznámení',
'settings.notifications.tab_badge.hint': 'Počet nepřečtených oznámení se viditelně zobrazí na hlavní stránce (pokud není seznam oznámení viditelný)',
'settings.notifications.favicon_badge': 'Zobrazit počet na ikoně serveru',
'settings.notifications.favicon_badge.hint': 'Zobrazí počet nepřečtených oznámení na ikoně serveru',
'settings.preferences': 'Předvolby',
'settings.rewrite_mentions': 'Přepsat zmínky v zobrazených příspěvcích',
'settings.rewrite_mentions_no': 'Nepřepisovat zmínky',
'settings.rewrite_mentions_acct': 'Přepsat uživatelským jménem a doménou (pokud je účet na jiném serveru)',
'settings.rewrite_mentions_username': 'Přepsat uživatelským jménem',
'settings.show_reply_counter': 'Zobrazit odhad počtu odpovědí',
'settings.status_icons': 'Ikony u příspěvků',
'settings.status_icons_language': 'Indikace jazyk',
'settings.status_icons_reply': 'Indikace odpovědi',
'settings.status_icons_local_only': 'Indikace lokálního příspěvku',
'settings.status_icons_media': 'Indikace obrázků a anket',
'settings.status_icons_visibility': 'Indikace úrovně soukromí',
'settings.tag_misleading_links': 'Označit zavádějící odkazy',
'settings.tag_misleading_links.hint': 'Zobrazit skutečný cíl u každého odkazu, který ho explicitně nezmiňuje',
'settings.wide_view': 'Široké sloupce (pouze v režimu Desktop)',
'settings.wide_view_hint': 'Sloupce se roztáhnout, aby lépe vyplnily dostupný prostor.',
'settings.navbar_under': 'Navigační lišta vespod (pouze v režimu Mobil)',
'settings.compose_box_opts': 'Editační pole',
'settings.always_show_spoilers_field': 'Vždy zobrazit pole pro varování o obsahu',
'settings.prepend_cw_re': 'Při odpovídání přidat před varování o obsahu “re: ”',
'settings.preselect_on_reply': 'Při odpovědi označit uživatelská jména',
'settings.preselect_on_reply_hint': 'Při odpovídání na konverzaci s více účastníky se jména všech kromě prvního označí, aby šla jednoduše smazat',
'settings.confirm_missing_media_description': 'Zobrazit potvrzovací dialog při odesílání příspěvku, ve kterém chybí popisky obrázků',
'settings.confirm_before_clearing_draft': 'Zobrazit potvrzovací dialog před přepsáním právě vytvářené zprávy',
'settings.show_content_type_choice': 'Zobrazit volbu formátu příspěvku',
'settings.side_arm': 'Vedlejší odesílací tlačítko:',
'settings.side_arm.none': 'Žádné',
'settings.side_arm_reply_mode': 'Při odpovídání na příspěvek by vedlejší odesílací tlačítko mělo:',
'settings.side_arm_reply_mode.keep': 'Použít svou nastavenou úroveň soukromí',
'settings.side_arm_reply_mode.copy': 'Použít úroveň soukromí příspěvku, na který odpovídáte',
'settings.side_arm_reply_mode.restrict': 'Zvýšit úroveň soukromí nejméně na úroveň příspěvku, na který odpovídáte',
'settings.content_warnings': 'Varování o obsahu',
'settings.content_warnings_shared_state': 'Zobrazit/schovat všechny kopie naráz',
'settings.content_warnings_shared_state_hint': 'Tlačítko varování o obsahu bude mít efekt na všechny kopie příspěvku naráz, stejně jako na běžném Mastodonu. Nebude pak možné automaticky sbalit jakoukoliv kopii příspěvku, která má rozbalené varování o obsahu',
'settings.content_warnings_media_outside': 'Zobrazit obrázky a videa mimo varování o obsahu',
'settings.content_warnings_media_outside_hint': 'Obrázky a videa z příspěvku s varováním o obsahu se zobrazí se separátním přepínačem zobrazení, stejně jako na běžném Mastodonu.',
'settings.content_warnings_unfold_opts': 'Možnosti automatického rozbalení',
'settings.enable_content_warnings_auto_unfold': 'Vždy rozbalit příspěvky označené varováním o obsahu',
'settings.deprecated_setting': 'Tato možnost se nyní nastavuje v {settings_page_link}',
'settings.shared_settings_link': 'předvolbách Mastodonu',
'settings.content_warnings_filter': 'Tato varování o obsahu automaticky nerozbalovat:',
'settings.content_warnings.regexp': 'Regulární výraz',
'settings.media_reveal_behind_cw': 'Automaticky zobrazit média označená varováním o obsahu',
'settings.pop_in_player': 'Povolit plovoucí okno přehrávače',
'settings.pop_in_position': 'Pozice plovoucího okna:',
'settings.pop_in_left': 'Vlevo',
'settings.pop_in_right': 'Vpravo',
'status.collapse': 'Sbalit',
'status.uncollapse': 'Rozbalit',
'status.in_reply_to': 'Tento příspěvek je odpověď',
'status.has_preview_card': 'Obsahuje náhled odkazu',
'status.has_pictures': 'Obsahuje obrázky',
'status.is_poll': 'Tento příspěvek je anketa',
'status.has_video': 'Obsahuje video',
'status.has_audio': 'Obsahuje audio',
'status.local_only': 'Viditelné pouze z vaší instance',
'media_gallery.sensitive': 'Citlivý obsah',
'favourite_modal.combo': 'Příště můžete pro přeskočení stisknout {combo}',
'home.column_settings.show_direct': 'Zobrazit přímé zprávy',
'notification_purge.start': 'Čistící režim',
'notifications.mark_as_read': 'Označit všechna oznámení jako přečtená',
'notification.markForDeletion': 'Označit pro smazání',
'notifications.clear': 'Vymazat všechna oznámení',
'notifications.marked_clear_confirmation': 'Určitě chcete trvale smazat všechna vybraná oznámení?',
'notifications.marked_clear': 'Smazat vybraná oznámení',
'notification_purge.btn_all': 'Vybrat\nvše',
'notification_purge.btn_none': 'Nevybrat\nnic',
'notification_purge.btn_invert': 'Obrátit\nvýběr',
'notification_purge.btn_apply': 'Smazat\nvybrané',
'compose.attach.upload': 'Nahrát soubor',
'compose.attach.doodle': 'Něco namalovat',
'compose.attach': 'Připojit...',
'advanced_options.local-only.short': 'Lokální příspěvek',
'advanced_options.local-only.long': 'Neposílat na jiné servery',
'advanced_options.local-only.tooltip': 'Tento příspěvek je pouze lokální',
'advanced_options.icon_title': 'Pokročilá nastavení',
'advanced_options.threaded_mode.short': 'Režim vlákna',
'advanced_options.threaded_mode.long': 'Po odeslání automaticky otevře pole pro odpověď',
'advanced_options.threaded_mode.tooltip': 'Režim vlákna je zapnutý',
'home.column_settings.advanced': 'Pokročilé',
'home.column_settings.filter_regex': 'Filtrovat podle regulárních výrazů',
'compose_form.poll.single_choice': 'Povolit jednu odpověď',
'compose_form.poll.multiple_choices': 'Povolit více odpovědí',
'compose.content-type.plain': 'Prostý text',
'content-type.change': 'Formát příspěvku',
'compose_form.spoiler': 'Přidat varování o obsahu',
'direct.group_by_conversations': 'Seskupit do konverzací',
'column.toot': 'Příspěvky a odpovědi',
'confirmation_modal.do_not_ask_again': 'Příště se už neptat',
'keyboard_shortcuts.bookmark': 'Přidat do záložek',
'keyboard_shortcuts.toggle_collapse': 'Sbalit/rozbalit příspěvek',
'keyboard_shortcuts.secondary_toot': 'Odeslat příspěvek s druhotným nastavením soukromí',
'column.subheading': 'Různé',
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,153 @@
{
"about.fork_disclaimer": "Glitch-soc je svobodný software s otevřeným zdrojovým kódem založený na Mastodonu.",
"advanced_options.icon_title": "Pokročilá nastavení",
"advanced_options.local-only.long": "Neposílat na jiné servery",
"advanced_options.local-only.short": "Lokální příspěvek",
"advanced_options.local-only.tooltip": "Tento příspěvek je pouze lokální",
"advanced_options.threaded_mode.long": "Po odeslání automaticky otevře pole pro odpověď",
"advanced_options.threaded_mode.short": "Režim vlákna",
"advanced_options.threaded_mode.tooltip": "Režim vlákna je zapnutý",
"boost_modal.missing_description": "Příspěvek obsahuje obrázky bez popisků",
"column.subheading": "Různé",
"compose.attach": "Připojit...",
"compose.attach.doodle": "Něco namalovat",
"compose.attach.upload": "Nahrát soubor",
"compose.content-type.plain": "Prostý text",
"compose_form.poll.multiple_choices": "Povolit více odpovědí",
"compose_form.poll.single_choice": "Povolit jednu odpověď",
"compose_form.spoiler": "Přidat varování o obsahu",
"confirmation_modal.do_not_ask_again": "Příště se už neptat",
"content-type.change": "Formát příspěvku",
"direct.group_by_conversations": "Seskupit do konverzací",
"endorsed_accounts_editor.endorsed_accounts": "Vybrané účty",
"favourite_modal.combo": "Příště můžete pro přeskočení stisknout {combo}",
"getting_started.onboarding": "Ukaž mi to tu",
"home.column_settings.advanced": "Pokročilé",
"home.column_settings.filter_regex": "Filtrovat podle regulárních výrazů",
"home.column_settings.show_direct": "Zobrazit přímé zprávy",
"keyboard_shortcuts.bookmark": "Přidat do záložek",
"keyboard_shortcuts.secondary_toot": "Odeslat příspěvek s druhotným nastavením soukromí",
"keyboard_shortcuts.toggle_collapse": "Sbalit/rozbalit příspěvek",
"layout.auto": "Automatické",
"layout.hint.auto": "Vybrat rozložení automaticky v závislosti na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.",
"layout.hint.desktop": "Použít vícesloupcové rozložení nezávisle na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.",
"layout.hint.single": "Použít jednosloupcové rozložení nezávisle na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.",
"media_gallery.sensitive": "Citlivý obsah",
"navigation_bar.app_settings": "Nastavení aplikace",
"navigation_bar.featured_users": "Vybraní uživatelé",
"navigation_bar.info": "Rozšířené informace",
"navigation_bar.keyboard_shortcuts": "Klávesové zkratky",
"navigation_bar.misc": "Různé",
"notification.markForDeletion": "Označit pro smazání",
"notification_purge.btn_all": "Vybrat\nvše",
"notification_purge.btn_apply": "Smazat\nvybrané",
"notification_purge.btn_invert": "Obrátit\nvýběr",
"notification_purge.btn_none": "Nevybrat\nnic",
"notification_purge.start": "Čistící režim",
"notifications.marked_clear": "Smazat vybraná oznámení",
"notifications.marked_clear_confirmation": "Určitě chcete trvale smazat všechna vybraná oznámení?",
"onboarding.done": "Hotovo",
"onboarding.next": "Další",
"onboarding.page_five.public_timelines": "Místní časová osa zobrazuje veřejné příspěvky všech uživatelů instance {domain}. Federovaná časová osa zobrazí příspěvky od všech, koho uživatelé instance {domain} sledují. Tyto veřejné časové osy jsou skvělý způsob, jak objevit nové lidi.",
"onboarding.page_four.home": "Domovská časová osa zobrazuje příspěvky od lidí, které sledujete.",
"onboarding.page_four.notifications": "Notifikace se zobrazí, když s vámi někdo interaguje.",
"onboarding.page_one.federation": "{domain} je 'instance' Mastodonu. Mastodon je síť nezávislých serverů, které jsou spolu propojené do jedné velké sociální sítě. Těmto serverům říkáme instance.",
"onboarding.page_one.handle": "Jste na instanci {domain}, takže celá adresa vašeho profilu je {handle}",
"onboarding.page_one.welcome": "Vítá vás {domain}!",
"onboarding.page_six.almost_done": "Skoro hotovo...",
"onboarding.page_six.appetoot": "Veselé mastodonění!",
"onboarding.page_six.apps_available": "Jsou dostupné {apps} pro iOS, Android i jiné platformy.",
"onboarding.page_six.github": "Na serveru {domain} běží Glitchsoc. Glitchsoc je přátelský {fork} programu {Mastodon}, a je kompatibilní s jakoukoliv jinou mastodoní instancí nebo aplikací. Glitchsoc je zcela svobodný a má otevřený zdrojový kód. Na stránce {github} můžete hlásit chyby, žádat o nové funkce, nebo ke kódu vlastnoručně přispět.",
"onboarding.page_six.various_app": "mobilní aplikace",
"onboarding.page_three.profile": "Upravte si svůj profil a nastavte si profilový obrázek, jméno, a krátký text o sobě. Naleznete tam i další možnosti nastavení.",
"onboarding.page_three.search": "Pomocí vyhledávací lišty můžete hledat lidi nebo hashtagy. Pokud hledáte někoho z jiné instance, musíte použít celou adresu jeho profilu.",
"onboarding.page_two.compose": "Příspěvky se píší v levém sloupci. Pomocí ikon pod příspěvkem k němu můžete připojit obrázky, změnit úroveň soukromí nebo přidat varování o obsahu.",
"onboarding.skip": "Přeskočit",
"settings.always_show_spoilers_field": "Vždy zobrazit pole pro varování o obsahu",
"settings.auto_collapse": "Automaticky sbalit",
"settings.auto_collapse_all": "Všechno",
"settings.auto_collapse_lengthy": "Dlouhé příspěvky",
"settings.auto_collapse_media": "Příspěvky s přílohami",
"settings.auto_collapse_notifications": "Oznámení",
"settings.auto_collapse_reblogs": "Boosty",
"settings.auto_collapse_replies": "Odpovědi",
"settings.close": "Zavřít",
"settings.collapsed_statuses": "Sbalené příspěvky",
"settings.compose_box_opts": "Editační pole",
"settings.confirm_before_clearing_draft": "Zobrazit potvrzovací dialog před přepsáním právě vytvářené zprávy",
"settings.confirm_boost_missing_media_description": "Zobrazit potvrzovací dialog před boostnutím příspěvku s chybějícími popisky obrázků",
"settings.confirm_missing_media_description": "Zobrazit potvrzovací dialog při odesílání příspěvku, ve kterém chybí popisky obrázků",
"settings.content_warnings": "Varování o obsahu",
"settings.content_warnings.regexp": "Regulární výraz",
"settings.content_warnings_filter": "Tato varování o obsahu automaticky nerozbalovat:",
"settings.content_warnings_media_outside": "Zobrazit obrázky a videa mimo varování o obsahu",
"settings.content_warnings_media_outside_hint": "Obrázky a videa z příspěvku s varováním o obsahu se zobrazí se separátním přepínačem zobrazení, stejně jako na běžném Mastodonu.",
"settings.content_warnings_shared_state": "Zobrazit/schovat všechny kopie naráz",
"settings.content_warnings_shared_state_hint": "Tlačítko varování o obsahu bude mít efekt na všechny kopie příspěvku naráz, stejně jako na běžném Mastodonu. Nebude pak možné automaticky sbalit jakoukoliv kopii příspěvku, která má rozbalené varování o obsahu",
"settings.content_warnings_unfold_opts": "Možnosti automatického rozbalení",
"settings.deprecated_setting": "Tato možnost se nyní nastavuje v {settings_page_link}",
"settings.enable_collapsed": "Povolit sbalené příspěvky",
"settings.enable_collapsed_hint": "U sbalených příspěvků je část jejich obsahu skrytá, aby zabraly méně místa na obrazovce. (Tohle není stejná funkce jako varování o obsahu.)",
"settings.enable_content_warnings_auto_unfold": "Vždy rozbalit příspěvky označené varováním o obsahu",
"settings.general": "Obecné",
"settings.hicolor_privacy_icons": "Barevné ikony soukromí",
"settings.hicolor_privacy_icons.hint": "Zobrazit ikony úrovně soukromí příspěvků v jasných, snadno rozlišitelných barvách",
"settings.image_backgrounds": "Obrázkové pozadí",
"settings.image_backgrounds_media": "Náhled médií ve sbalených příspěvcích",
"settings.image_backgrounds_media_hint": "Pokud jsou k příspěvku přiložena média, použije se první z nich jako pozadí",
"settings.image_backgrounds_users": "Nastavit sbaleným příspěvkům obrázkové pozadí",
"settings.inline_preview_cards": "Zobrazit v časové ose náhledy externích odkazů",
"settings.layout": "Rozložení:",
"settings.layout_opts": "Možnosti rozvržení",
"settings.media": "Média",
"settings.media_fullwidth": "Zobrazit náhledy v plné šířce",
"settings.media_letterbox": "Neořezávat obrázky",
"settings.media_letterbox_hint": "Místo výřezu obrázku zobrazit obrázek celý, doplněný podle potřeby o prázdné okraje",
"settings.media_reveal_behind_cw": "Automaticky zobrazit média označená varováním o obsahu",
"settings.notifications.favicon_badge": "Zobrazit počet na ikoně serveru",
"settings.notifications.favicon_badge.hint": "Zobrazí počet nepřečtených oznámení na ikoně serveru",
"settings.notifications.tab_badge": "Zobrazit počet nepřečtených oznámení",
"settings.notifications.tab_badge.hint": "Počet nepřečtených oznámení se viditelně zobrazí na hlavní stránce (pokud není seznam oznámení viditelný)",
"settings.notifications_opts": "Možnosti oznámení",
"settings.pop_in_left": "Vlevo",
"settings.pop_in_player": "Povolit plovoucí okno přehrávače",
"settings.pop_in_position": "Pozice plovoucího okna:",
"settings.pop_in_right": "Vpravo",
"settings.preferences": "Předvolby",
"settings.prepend_cw_re": "Při odpovídání přidat před varování o obsahu “re: ”",
"settings.preselect_on_reply": "Při odpovědi označit uživatelská jména",
"settings.preselect_on_reply_hint": "Při odpovídání na konverzaci s více účastníky se jména všech kromě prvního označí, aby šla jednoduše smazat",
"settings.rewrite_mentions": "Přepsat zmínky v zobrazených příspěvcích",
"settings.rewrite_mentions_acct": "Přepsat uživatelským jménem a doménou (pokud je účet na jiném serveru)",
"settings.rewrite_mentions_no": "Nepřepisovat zmínky",
"settings.rewrite_mentions_username": "Přepsat uživatelským jménem",
"settings.shared_settings_link": "předvolbách Mastodonu",
"settings.show_action_bar": "Zobrazit ve sbalených příspěvcích tlačítka s akcemi",
"settings.show_content_type_choice": "Zobrazit volbu formátu příspěvku",
"settings.show_reply_counter": "Zobrazit odhad počtu odpovědí",
"settings.side_arm": "Vedlejší odesílací tlačítko:",
"settings.side_arm.none": "Žádné",
"settings.side_arm_reply_mode": "Při odpovídání na příspěvek by vedlejší odesílací tlačítko mělo:",
"settings.side_arm_reply_mode.copy": "Použít úroveň soukromí příspěvku, na který odpovídáte",
"settings.side_arm_reply_mode.keep": "Použít svou nastavenou úroveň soukromí",
"settings.side_arm_reply_mode.restrict": "Zvýšit úroveň soukromí nejméně na úroveň příspěvku, na který odpovídáte",
"settings.status_icons": "Ikony u příspěvků",
"settings.status_icons_language": "Indikace jazyk",
"settings.status_icons_local_only": "Indikace lokálního příspěvku",
"settings.status_icons_media": "Indikace obrázků a anket",
"settings.status_icons_reply": "Indikace odpovědi",
"settings.status_icons_visibility": "Indikace úrovně soukromí",
"settings.tag_misleading_links": "Označit zavádějící odkazy",
"settings.tag_misleading_links.hint": "Zobrazit skutečný cíl u každého odkazu, který ho explicitně nezmiňuje",
"settings.wide_view": "Široké sloupce (pouze v režimu Desktop)",
"settings.wide_view_hint": "Sloupce se roztáhnout, aby lépe vyplnily dostupný prostor.",
"status.collapse": "Sbalit",
"status.has_audio": "Obsahuje audio",
"status.has_pictures": "Obsahuje obrázky",
"status.has_preview_card": "Obsahuje náhled odkazu",
"status.has_video": "Obsahuje video",
"status.in_reply_to": "Tento příspěvek je odpověď",
"status.is_poll": "Tento příspěvek je anketa",
"status.local_only": "Viditelné pouze z vaší instance",
"status.uncollapse": "Rozbalit"
}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/cy.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,6 @@
{
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/da.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,6 @@
{
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -0,0 +1,206 @@
{
"about.fork_disclaimer": "Glitch-soc ist freie, quelloffene Software geforkt von Mastodon.",
"account.add_account_note": "Notiz für @{name} hinzufügen",
"account.disclaimer_full": "Die folgenden Informationen könnten das Profil des Nutzers unvollständig wiedergeben.",
"account.follows": "Folgt",
"account.joined": "Beigetreten am {date}",
"account.suspended_disclaimer_full": "Dieser Nutzer wurde durch einen Moderator gesperrt.",
"account.view_full_profile": "Vollständiges Profil anzeigen",
"account_note.cancel": "Abbrechen",
"account_note.edit": "Bearbeiten",
"account_note.glitch_placeholder": "Kein Kommentar angegeben",
"account_note.save": "Speichern",
"advanced_options.icon_title": "Erweiterte Optionen",
"advanced_options.local-only.long": "Nicht auf anderen Instanzen posten",
"advanced_options.local-only.short": "Nur lokal",
"advanced_options.local-only.tooltip": "Dieser Post ist nur lokal",
"advanced_options.threaded_mode.long": "Öffnet automatisch eine Antwort beim Schreiben",
"advanced_options.threaded_mode.short": "Thread-Modus",
"advanced_options.threaded_mode.tooltip": "Thread-Modus aktiviert",
"boost_modal.missing_description": "Dieser Toot enthält Medien ohne Beschreibung",
"column.favourited_by": "Favorisiert von",
"column.heading": "Sonstiges",
"column.reblogged_by": "Geteilt von",
"column.subheading": "Sonstige Optionen",
"column_header.profile": "Profil",
"column_subheading.lists": "Listen",
"column_subheading.navigation": "Navigation",
"community.column_settings.allow_local_only": "Nur-lokale Toots anzeigen",
"compose.attach": "Anhängen...",
"compose.attach.doodle": "Etwas zeichnen",
"compose.attach.upload": "Eine Datei hochladen",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",
"compose.content-type.plain": "Unformatierter Text",
"compose_form.poll.multiple_choices": "Mehrfachauswahl erlauben",
"compose_form.poll.single_choice": "Eine Auswahl erlauben",
"compose_form.spoiler": "Text hinter Warnung verbergen",
"confirmation_modal.do_not_ask_again": "Nicht erneut nach Bestätigung fragen",
"confirmations.deprecated_settings.confirm": "Mastodon-Einstellungen verwenden",
"confirmations.deprecated_settings.message": "Einige der von dir verwendeten, glitch-soc-spezifischen {app_settings} wurden durch Mastodon {preferences} ersetzt und werden überschrieben:",
"confirmations.missing_media_description.confirm": "Trotzdem absenden",
"confirmations.missing_media_description.edit": "Anhänge bearbeiten",
"confirmations.missing_media_description.message": "Mindestens einem Anhang fehlt eine Beschreibung. Denke darüber nach, alle Anhänge für Sehbeeinträchtigte zu beschreiben, bevor du den Toot absendest.",
"confirmations.unfilter.author": "Urheber",
"confirmations.unfilter.confirm": "Anzeigen",
"confirmations.unfilter.edit_filter": "Filter bearbeiten",
"confirmations.unfilter.filters": "Passende{count, plural, one {r} other {}} Filter",
"content-type.change": "Inhaltstyp",
"direct.group_by_conversations": "Nach Unterhaltung gruppieren",
"endorsed_accounts_editor.endorsed_accounts": "Empfohlene Konten",
"favourite_modal.combo": "Mit {combo} wird dieses Fenster beim nächsten Mal nicht mehr angezeigt",
"getting_started.onboarding": "Führe mich herum",
"home.column_settings.advanced": "Erweitert",
"home.column_settings.filter_regex": "Mit regulären Ausdrücken herausfiltern",
"home.column_settings.show_direct": "Direktnachrichten anzeigen",
"home.settings": "Spalteneinstellungen",
"keyboard_shortcuts.bookmark": "zu Lesezeichen hinzufügen",
"keyboard_shortcuts.secondary_toot": "Toot mit sekundärer Privatsphäreeinstellung absenden",
"keyboard_shortcuts.toggle_collapse": "Toots ein-/ausklappen",
"tooltips.reactions": "Reaktionen",
"layout.auto": "Automatisch",
"layout.desktop": "Desktop",
"layout.hint.auto": "Automatisch das Layout anhand der Einstellung \"Erweitertes Webinterface verwenden\" und Bildschirmgröße auswählen.",
"layout.hint.desktop": "Das mehrspaltige Layout verwenden, unabhängig von der Einstellung \"Erweitertes Webinterface verwenden\".",
"layout.hint.single": "Das einspaltige Layout verwenden, unabhängig von der Einstellung \"Erweitertes Webinterface verwenden\".",
"layout.single": "Mobil",
"media_gallery.sensitive": "Empfindlich",
"moved_to_warning": "Dieses Konto ist als verschoben zu {moved_to_link} markiert und akzeptiert daher keine neuen Follower.",
"navigation_bar.app_settings": "App-Einstellungen",
"navigation_bar.featured_users": "Empfohlene Nutzer",
"navigation_bar.info": "Erweiterte Informationen",
"navigation_bar.keyboard_shortcuts": "Tastaturkürzel",
"navigation_bar.misc": "Sonstiges",
"notification.markForDeletion": "Zum Entfernen auswählen",
"notification.reaction": "{name} hat auf deinen Beitrag reagiert",
"notifications.column_settings.reaction": "Reaktionen:",
"notification_purge.btn_all": "Alle\nauswählen",
"notification_purge.btn_apply": "Ausgewählte\nentfernen",
"notification_purge.btn_invert": "Auswahl\numkehren",
"notification_purge.btn_none": "Auswahl\naufheben",
"notification_purge.start": "Benachrichtigungen-Aufräumen-Modus starten",
"notifications.marked_clear": "Ausgewählte Benachrichtigungen entfernen",
"notifications.marked_clear_confirmation": "Möchtest du wirklich alle auswählten Benachrichtigungen für immer entfernen?",
"onboarding.done": "Fertig",
"onboarding.next": "Weiter",
"onboarding.page_five.public_timelines": "Die lokale Timeline zeigt öffentliche Posts von allen auf {domain}. Die föderierte Timeline zeigt öffentliche Posts von allen, denen Leute auf {domain} folgen. Das sind die öffentlichen Timelines, eine tolle Möglichkeit, neue Leute zu entdecken.",
"onboarding.page_four.home": "Die Startseite zeigt Posts von Leuten an, denen du folgst.",
"onboarding.page_four.notifications": "Die Benachrichtigungs-Spalte zeigt an, wenn jemand mit dir interagiert.",
"onboarding.page_one.federation": "{domain} ist eine \"Instanz\" von Mastodon. Mastodon ist ein Netzwerk aus unabhängigen Servern, die zusammen ein größeres soziales Netzwerk bilden. Diese Server nennen wir Instanzen.",
"onboarding.page_one.handle": "Du bist auf {domain}, also ist dein vollständiger Nutzername {handle}",
"onboarding.page_one.welcome": "Willkommen auf {domain}!",
"onboarding.page_six.admin": "Dein Instanz-Admin ist {admin}.",
"onboarding.page_six.almost_done": "Fast geschafft...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "Es gibt {apps} für iOS, Android und andere Plattformen.",
"onboarding.page_six.github": "{domain} läuft auf glitch-soc. glitch-soc ist ein freundlicher {fork} von {Mastodon}, und ist mit jeder Mastodon-App oder -Instanz kompatibel. glitch-soc ist komplett frei und quelloffen. Auf {github} kannst du Fehler melden, Features anfragen oder Code beitragen.",
"onboarding.page_six.guidelines": "Community-Richtlinien",
"onboarding.page_six.read_guidelines": "Bitte lies die {guidelines} von {domain}!",
"onboarding.page_six.various_app": "mobile Apps",
"onboarding.page_three.profile": "Bearbeite dein Profil, um deinen Avatar, \"Über mich\" und den Anzeigenamen zu ändern. Dort findest du auch andere Einstellungen.",
"onboarding.page_three.search": "Benutze die Suchleiste, um Leute zu finden und Hashtags anzusehen, wie etwa {illustration} und {introductions}. Um nach einer Person zu suchen, die nicht auf dieser Instanz ist, benutze deren vollständigen Nutzername.",
"onboarding.page_two.compose": "Schreibe Posts in der Verfassen-Spalte. Mit den Symbolen unten kannst du Bilder hochladen, Privatsphäre-Einstellungen ändern, und Inhaltswarnungen hinzufügen.",
"onboarding.skip": "Überspringen",
"settings.always_show_spoilers_field": "Das Inhaltswarnungs-Feld immer aktivieren",
"settings.auto_collapse": "Automatisches Einklappen",
"settings.auto_collapse_all": "Alles",
"settings.auto_collapse_lengthy": "Lange Toots",
"settings.auto_collapse_media": "Toots mit Anhängen",
"settings.auto_collapse_notifications": "Benachrichtigungen",
"settings.auto_collapse_reblogs": "Geteilte Toots",
"settings.auto_collapse_replies": "Antworten",
"settings.close": "Schließen",
"settings.collapsed_statuses": "Eingeklappte Toots",
"settings.compose_box_opts": "Verfassen-Box",
"settings.confirm_before_clearing_draft": "Zeige einen Bestätigungsdialog, bevor der derzeitige Entwurf verworfen wird",
"settings.confirm_boost_missing_media_description": "Zeige einen Bestätigungsdialog, bevor Toots mit Anhängen ohne Beschreibung geteilt werden",
"settings.confirm_missing_media_description": "Zeige einen Bestätigungsdialog, bevor Toots mit Anhängen ohne Beschreibung abgesendet werden",
"settings.content_warnings": "Content warnings",
"settings.content_warnings.regexp": "Regulärer Ausdruck",
"settings.content_warnings_filter": "Inhaltswarnungen, die nicht ausgeklappt werden sollen:",
"settings.content_warnings_media_outside": "Medienanhänge außerhalb von Inhaltswarnungen anzeigen",
"settings.content_warnings_media_outside_hint": "Das ursprüngliche Verhalten von Mastodon wiederherstellen, in welchem Inhaltswarnungen keine Auswirkungen auf Anhänge haben",
"settings.content_warnings_shared_state": "Inhalt aller Kopien auf einmal ein-/ausblenden",
"settings.content_warnings_shared_state_hint": "Das ursprüngliche Verhalten von Mastodon wiederhertstellen, in welchem der Inhaltswarnungs-Knopf alle Kopien eines Posts auf einmal ein-/ausklappt. Das wird das automatische Einklappen jedweder Kopie eines Toots mit ausgeklappter Inhaltswarnung",
"settings.content_warnings_unfold_opts": "Optionen zum automatischen Ausklappen",
"settings.deprecated_setting": "Diese Einstellung wird nun von Mastodons {settings_page_link} gesteuert",
"settings.enable_collapsed": "Eingeklappte Toots aktivieren",
"settings.enable_collapsed_hint": "Eingeklappte Posts haben einen Teil ihres Inhalts verborgen, um weniger Platz am Bildschirm einzunehmen. Das passiert unabhängig von der Inhaltswarnfunktion",
"settings.enter_amount_prompt": "Gib eine Zahl ein",
"settings.enable_content_warnings_auto_unfold": "Inhaltswarnungen automatisch ausklappen",
"settings.general": "Allgemein",
"settings.hicolor_privacy_icons": "Eingefärbte Privatsphäre-Symbole",
"settings.hicolor_privacy_icons.hint": "Zeige Privatsphäre-Symbole in hellen und leicht zu unterscheidenden Farben",
"settings.image_backgrounds": "Bildhintergründe",
"settings.image_backgrounds_media": "Vorschau eingeklappter Toot-Anhänge",
"settings.image_backgrounds_media_hint": "Wenn der Post Anhänge hat, wird der erste als Hintergrund verwendet",
"settings.image_backgrounds_users": "Eingeklappten Toots einen Bild-Hintergrund geben",
"settings.inline_preview_cards": "Eingebettete Vorschaukarten für externe Links",
"settings.layout": "Layout:",
"settings.layout_opts": "Layout-Optionen",
"settings.media": "Medien",
"settings.media_fullwidth": "Medienvorschau in voller Breite",
"settings.num_visible_reactions": "Anzahl sichtbarer Reaktionen",
"settings.media_letterbox": "Mediengröße anpassen",
"settings.media_letterbox_hint": "Medien runterskalieren und einpassen um die Bildbehälter zu füllen anstatt zu strecken und zuzuschneiden",
"settings.media_reveal_behind_cw": "Empfindliche Medien hinter Inhaltswarnungen standardmäßig anzeigen",
"settings.notifications.favicon_badge": "Favicon-Badge für ungelesene Benachrichtigungen",
"settings.notifications.favicon_badge.hint": "Ein Badge für ungelesene Benachrichtigungen zum Favicon hinzufügen",
"settings.notifications.tab_badge": "Badge für ungelesene Benachrichtigungen",
"settings.notifications.tab_badge.hint": "Ein Badge für ungelesene Benachrichtigungen in den Spaltensymbolen anzeigen, wenn die Benachrichtigungen nicht offen sind",
"settings.notifications_opts": "Benachrichtigungsoptionen",
"settings.pop_in_left": "Links",
"settings.pop_in_player": "Pop-In-Player aktivieren",
"settings.pop_in_position": "Position des Pop-In-Players:",
"settings.pop_in_right": "Rechts",
"settings.preferences": "Preferences",
"settings.prepend_cw_re": "\"re: \" beim Antworten an Inhaltswarnung voranstellen",
"settings.preselect_on_reply": "Nutzernamen bei Antwort vorauswählen",
"settings.preselect_on_reply_hint": "Beim Antworten auf eine Konversation alle Nutzernamen auswählen, die nach dem ersten kommen",
"settings.rewrite_mentions": "Erwähnungen in angezeigten Status umschreiben",
"settings.rewrite_mentions_acct": "Mit Nutzernamen und Domain umschreiben (wenn das Konto auf einer anderen Instanz ist)",
"settings.rewrite_mentions_no": "Erwähnungen nicht umschreiben",
"settings.rewrite_mentions_username": "Mit Nutzername umschreiben",
"settings.shared_settings_link": "Nutzereinstellungen",
"settings.show_action_bar": "Aktions-Knöpfe in eingeklappten Toots anzeigen",
"settings.show_content_type_choice": "Auswahl für die Inhaltsart beim Verfassen von Toots anzeigen",
"settings.show_reply_counter": "Schätzung der Antwortanzahl anzeigen",
"settings.side_arm": "Sekundärer Toot-Knopf:",
"settings.side_arm.none": "Nichts",
"settings.side_arm_reply_mode": "Beim Antworten auf einen Toot sollte der sekundäre Toot-Knopf:",
"settings.side_arm_reply_mode.copy": "Privatsphäre-Einstellung des zu beantwortenden Toot verwenden",
"settings.side_arm_reply_mode.keep": "Die eingestellte Privatsphäre beibehalten",
"settings.side_arm_reply_mode.restrict": "Privatsphäre-Einstellung auf die des zu beantwortenden Toot beschränken",
"settings.status_icons": "Toot-Symbole",
"settings.status_icons_language": "Sprach-Indikator",
"settings.status_icons_local_only": "\"nur Lokal\"-Indikator",
"settings.status_icons_media": "Medien- und Umfragen-Indikatoren",
"settings.status_icons_reply": "Antwort-Indikator",
"settings.status_icons_visibility": "Toot-Privatsphäre-Indikator",
"settings.swipe_to_change_columns": "Das Wechseln der Spalte durch Wischen erlauben (nur für die mobile Ansicht)",
"settings.tag_misleading_links": "Irreführende Links markieren",
"settings.tag_misleading_links.hint": "Füge eine visuelle Indikation mit dem Ziel-Host des Links zu jedem Link hinzu, bei dem dieser nicht explizit genannt wird",
"settings.wide_view": "Breite Ansicht (nur für den Desktop-Modus)",
"settings.wide_view_hint": "Verbreitert Spalten, um den verfügbaren Platz besser zu füllen.",
"status.collapse": "Einklappen",
"status.react": "Reagieren",
"status.has_audio": "Hat angehängte Audiodateien",
"status.has_pictures": "Hat angehängte Bilder",
"status.has_preview_card": "Hat eine Vorschaukarte",
"status.has_video": "Hat angehängte Videos",
"status.in_reply_to": "Dieser Toot ist eine Antwort",
"status.is_poll": "Dieser Toot ist eine Umfrage",
"status.local_only": "Nur auf deiner Instanz sichtbar",
"status.sensitive_toggle": "Zum Anzeigen klicken",
"status.uncollapse": "Ausklappen",
"web_app_crash.change_your_settings": "Deine {settings} ändern",
"web_app_crash.content": "Du kannst folgende Dinge ausprobieren:",
"web_app_crash.debug_info": "Debug-Informationen",
"web_app_crash.disable_addons": "Browser-Add-ons oder eingebaute Übersetzungswerkzeuge deaktivieren",
"web_app_crash.issue_tracker": "Issue-Tracker",
"web_app_crash.reload": "neu laden",
"web_app_crash.reload_page": "Die Seite {reload}",
"web_app_crash.report_issue": "Einen Fehler im {issuetracker} melden",
"web_app_crash.settings": "Einstellungen",
"web_app_crash.title": "Es tut uns leid, aber mit der Mastodon-App ist etwas schiefgelaufen."
}

File diff suppressed because it is too large Load diff

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/el.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,6 @@
{
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -0,0 +1 @@
{}

View file

@ -5,12 +5,36 @@ const messages = {
'layout.auto': 'Auto', 'layout.auto': 'Auto',
'layout.current_is': 'Your current layout is:', 'layout.current_is': 'Your current layout is:',
'layout.desktop': 'Desktop', 'layout.desktop': 'Desktop',
'layout.mobile': 'Mobile', 'layout.single': 'Mobile',
'layout.hint.auto': 'Automatically chose layout based on “Enable advanced web interface” setting and screen size.',
'layout.hint.desktop': 'Use multiple-column layout regardless of the “Enable advanced web interface” setting or screen size.',
'layout.hint.single': 'Use single-column layout regardless of the “Enable advanced web interface” setting or screen size.',
'navigation_bar.app_settings': 'App settings', 'navigation_bar.app_settings': 'App settings',
'navigation_bar.misc': 'Misc',
'navigation_bar.keyboard_shortcuts': 'Keyboard shortcuts',
'navigation_bar.info': 'Extended information',
'navigation_bar.featured_users': 'Featured users',
'getting_started.onboarding': 'Show me around', 'getting_started.onboarding': 'Show me around',
'onboarding.next': 'Next',
'onboarding.done': 'Done',
'onboarding.skip': 'Skip',
'onboarding.page_one.federation': '{domain} is an \'instance\' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.', 'onboarding.page_one.federation': '{domain} is an \'instance\' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.',
'onboarding.page_one.welcome': 'Welcome to {domain}!', 'onboarding.page_one.welcome': 'Welcome to {domain}!',
'onboarding.page_one.handle': 'You are on {domain}, so your full handle is {handle}',
'onboarding.page_two.compose': 'Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.',
'onboarding.page_three.search': 'Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.',
'onboarding.page_three.profile': 'Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.',
'onboarding.page_four.home': 'The home timeline shows posts from people you follow.',
'onboarding.page_four.notifications': 'The notifications column shows when someone interacts with you.',
'onboarding.page_five.public_timelines': 'The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.',
'onboarding.page_six.admin': 'Your instance\'s admin is {admin}.',
'onboarding.page_six.read_guidelines': 'Please read {domain}\'s {guidelines}!',
'onboarding.page_six.guidelines': 'community guidelines',
'onboarding.page_six.almost_done': 'Almost done...',
'onboarding.page_six.github': '{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}, and is compatible with any Mastodon instance or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.', 'onboarding.page_six.github': '{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}, and is compatible with any Mastodon instance or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.',
'onboarding.page_six.apps_available': 'There are {apps} available for iOS, Android and other platforms.',
'onboarding.page_six.various_app': 'mobile apps',
'onboarding.page_six.appetoot': 'Bon Appetoot!',
'settings.auto_collapse': 'Automatic collapsing', 'settings.auto_collapse': 'Automatic collapsing',
'settings.auto_collapse_all': 'Everything', 'settings.auto_collapse_all': 'Everything',
'settings.auto_collapse_lengthy': 'Lengthy toots', 'settings.auto_collapse_lengthy': 'Lengthy toots',
@ -22,19 +46,100 @@ const messages = {
'settings.close': 'Close', 'settings.close': 'Close',
'settings.collapsed_statuses': 'Collapsed toots', 'settings.collapsed_statuses': 'Collapsed toots',
'settings.enable_collapsed': 'Enable collapsed toots', 'settings.enable_collapsed': 'Enable collapsed toots',
'settings.enable_collapsed_hint': 'Collapsed posts have parts of their contents hidden to take up less screen space. This is distinct from the Content Warning feature',
'settings.general': 'General', 'settings.general': 'General',
'settings.compose_box_opts': 'Compose box',
'settings.side_arm': 'Secondary toot button:',
'settings.side_arm.none': 'None',
'settings.side_arm_reply_mode': 'When replying to a toot, the secondary toot button should:',
'settings.side_arm_reply_mode.keep': 'Keep its set privacy',
'settings.side_arm_reply_mode.copy': 'Copy privacy setting of the toot being replied to',
'settings.side_arm_reply_mode.restrict': 'Restrict privacy setting to that of the toot being replied to',
'settings.always_show_spoilers_field': 'Always enable the Content Warning field',
'settings.prepend_cw_re': 'Prepend “re: ” to content warnings when replying',
'settings.preselect_on_reply': 'Pre-select usernames on reply',
'settings.preselect_on_reply_hint': 'When replying to a conversation with multiple participants, pre-select usernames past the first',
'settings.confirm_missing_media_description': 'Show confirmation dialog before sending toots lacking media descriptions',
'settings.confirm_before_clearing_draft': 'Show confirmation dialog before overwriting the message being composed',
'settings.show_content_type_choice': 'Show content-type choice when authoring toots',
'settings.content_warnings': 'Content Warnings',
'settings.content_warnings.regexp': 'Regular expression',
'settings.content_warnings_shared_state': 'Show/hide content of all copies at once',
'settings.content_warnings_shared_state_hint': 'Reproduce upstream Mastodon behavior by having the Content Warning button affect all copies of a post at once. This will prevent automatic collapsing of any copy of a toot with unfolded CW',
'settings.content_warnings_media_outside': 'Display media attachments outside content warnings',
'settings.content_warnings_media_outside_hint': 'Reproduce upstream Mastodon behavior by having the Content Warning toggle not affect media attachments',
'settings.content_warnings_unfold_opts': 'Auto-unfolding options',
'settings.enable_content_warnings_auto_unfold': 'Automatically unfold content-warnings',
'settings.deprecated_setting': 'This setting is now controlled from Mastodon\'s {settings_page_link}',
'settings.shared_settings_link': 'user preferences',
'settings.content_warnings_filter': 'Content warnings to not automatically unfold:',
'settings.layout_opts': 'Layout options',
'settings.rewrite_mentions_no': 'Do not rewrite mentions',
'settings.rewrite_mentions_acct': 'Rewrite with username and domain (when the account is remote)',
'settings.rewrite_mentions_username': 'Rewrite with username',
'settings.show_reply_counter': 'Display an estimate of the reply count',
'settings.hicolor_privacy_icons': 'High color privacy icons',
'settings.hicolor_privacy_icons.hint': 'Display privacy icons in bright and easily distinguishable colors',
'settings.confirm_boost_missing_media_description': 'Show confirmation dialog before boosting toots lacking media descriptions',
'settings.tag_misleading_links': 'Tag misleading links',
'settings.tag_misleading_links.hint': 'Add a visual indication with the link target host to every link not mentioning it explicitly',
'settings.rewrite_mentions': 'Rewrite mentions in displayed statuses',
'settings.notifications_opts': 'Notifications options',
'settings.notifications.tab_badge': 'Unread notifications badge',
'settings.notifications.tab_badge.hint': 'Display a badge for unread notifications in the column icons when the notifications column isn\'t open',
'settings.notifications.favicon_badge': 'Unread notifications favicon badge',
'settings.notifications.favicon_badge.hint': 'Add a badge for unread notifications to the favicon',
'settings.status_icons': 'Toot icons',
'settings.status_icons_language': 'Language indicator',
'settings.status_icons_reply': 'Reply indicator',
'settings.status_icons_local_only': 'Local-only indicator',
'settings.status_icons_media': 'Media and poll indicators',
'settings.status_icons_visibility': 'Toot privacy indicator',
'settings.layout': 'Layout:',
'settings.image_backgrounds': 'Image backgrounds', 'settings.image_backgrounds': 'Image backgrounds',
'settings.image_backgrounds_media': 'Preview collapsed toot media', 'settings.image_backgrounds_media': 'Preview collapsed toot media',
'settings.image_backgrounds_media_hint': 'If the post has any media attachment, use the first one as a background',
'settings.image_backgrounds_users': 'Give collapsed toots an image background', 'settings.image_backgrounds_users': 'Give collapsed toots an image background',
'settings.media': 'Media', 'settings.media': 'Media',
'settings.media_letterbox': 'Letterbox media', 'settings.media_letterbox': 'Letterbox media',
'settings.media_letterbox_hint': 'Scale down and letterbox media to fill the image containers instead of stretching and cropping them',
'settings.media_fullwidth': 'Full-width media previews', 'settings.media_fullwidth': 'Full-width media previews',
'settings.inline_preview_cards': 'Inline preview cards for external links',
'settings.media_reveal_behind_cw': 'Reveal sensitive media behind a CW by default',
'settings.pop_in_player': 'Enable pop-in player',
'settings.pop_in_position': 'Pop-in player position:',
'settings.pop_in_left': 'Left',
'settings.pop_in_right': 'Right',
'settings.preferences': 'User preferences', 'settings.preferences': 'User preferences',
'settings.wide_view': 'Wide view (Desktop mode only)', 'settings.wide_view': 'Wide view (Desktop mode only)',
'settings.wide_view_hint': 'Stretches columns to better fill the available space.',
'settings.navbar_under': 'Navbar at the bottom (Mobile only)', 'settings.navbar_under': 'Navbar at the bottom (Mobile only)',
'status.collapse': 'Collapse', 'status.collapse': 'Collapse',
'status.react': 'React', 'status.react': 'React',
'status.uncollapse': 'Uncollapse', 'status.uncollapse': 'Uncollapse',
'status.in_reply_to': 'This toot is a reply',
'status.has_preview_card': 'Features an attached preview card',
'status.has_pictures': 'Features attached pictures',
'status.is_poll': 'This toot is a poll',
'status.has_video': 'Features attached videos',
'status.has_audio': 'Features attached audio files',
'status.local_only': 'Only visible from your instance',
'content_type.change': 'Content type',
'compose.content-type.html': 'HTML',
'compose.content-type.markdown': 'Markdown',
'compose.content-type.plain': 'Plain text',
'compose_form.poll.single_choice': 'Allow one choice',
'compose_form.poll.multiple_choices': 'Allow multiple choices',
'compose_form.spoiler': 'Hide text behind warning',
'column.toot': 'Toots and replies',
'column_header.profile': 'Profile',
'column.heading': 'Misc',
'column.subheading': 'Miscellaneous options',
'column_subheading.navigation': 'Navigation',
'column_subheading.lists': 'Lists',
'media_gallery.sensitive': 'Sensitive', 'media_gallery.sensitive': 'Sensitive',
@ -49,6 +154,7 @@ const messages = {
'notifications.marked_clear_confirmation': 'Are you sure you want to permanently clear all selected notifications?', 'notifications.marked_clear_confirmation': 'Are you sure you want to permanently clear all selected notifications?',
'notifications.marked_clear': 'Clear selected notifications', 'notifications.marked_clear': 'Clear selected notifications',
'notification_purge.start': 'Enter notification cleaning mode',
'notification_purge.btn_all': 'Select\nall', 'notification_purge.btn_all': 'Select\nall',
'notification_purge.btn_none': 'Select\nnone', 'notification_purge.btn_none': 'Select\nnone',
'notification_purge.btn_invert': 'Invert\nselection', 'notification_purge.btn_invert': 'Invert\nselection',
@ -66,6 +172,25 @@ const messages = {
'advanced_options.threaded_mode.long': 'Automatically opens a reply on posting', 'advanced_options.threaded_mode.long': 'Automatically opens a reply on posting',
'advanced_options.threaded_mode.tooltip': 'Threaded mode enabled', 'advanced_options.threaded_mode.tooltip': 'Threaded mode enabled',
'endorsed_accounts_editor.endorsed_accounts': 'Featured accounts',
'account.add_account_note': 'Add note for @{name}',
'account_note.cancel': 'Cancel',
'account_note.save': 'Save',
'account_note.edit': 'Edit',
'account_note.glitch_placeholder': 'No comment provided',
'account.joined': 'Joined {date}',
'account.follows': 'Follows',
'home.column_settings.advanced': 'Advanced',
'home.column_settings.filter_regex': 'Filter out by regular expressions',
'direct.group_by_conversations': 'Group by conversation',
'community.column_settings.allow_local_only': 'Show local-only toots',
'keyboard_shortcuts.bookmark': 'to bookmark',
'keyboard_shortcuts.toggle_collapse': 'to collapse/uncollapse toots',
'keyboard_shortcuts.secondary_toot': 'to send toot using secondary privacy setting',
'tooltips.reactions': 'Reactions', 'tooltips.reactions': 'Reactions',
}; };

View file

@ -0,0 +1,206 @@
{
"about.fork_disclaimer": "Glitch-soc is free open source software forked from Mastodon.",
"account.add_account_note": "Add note for @{name}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.follows": "Follows",
"account.joined": "Joined {date}",
"account.suspended_disclaimer_full": "This user has been suspended by a moderator.",
"account.view_full_profile": "View full profile",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.glitch_placeholder": "No comment provided",
"account_note.save": "Save",
"advanced_options.icon_title": "Advanced options",
"advanced_options.local-only.long": "Do not post to other instances",
"advanced_options.local-only.short": "Local-only",
"advanced_options.local-only.tooltip": "This post is local-only",
"advanced_options.threaded_mode.long": "Automatically opens a reply on posting",
"advanced_options.threaded_mode.short": "Threaded mode",
"advanced_options.threaded_mode.tooltip": "Threaded mode enabled",
"boost_modal.missing_description": "This toot contains some media without description",
"column.favourited_by": "Favourited by",
"column.heading": "Misc",
"column.reblogged_by": "Boosted by",
"column.subheading": "Miscellaneous options",
"column_header.profile": "Profile",
"column_subheading.lists": "Lists",
"column_subheading.navigation": "Navigation",
"community.column_settings.allow_local_only": "Show local-only toots",
"compose.attach": "Attach...",
"compose.attach.doodle": "Draw something",
"compose.attach.upload": "Upload a file",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",
"compose.content-type.plain": "Plain text",
"compose_form.poll.multiple_choices": "Allow multiple choices",
"compose_form.poll.single_choice": "Allow one choice",
"compose_form.spoiler": "Hide text behind warning",
"confirmation_modal.do_not_ask_again": "Do not ask for confirmation again",
"confirmations.deprecated_settings.confirm": "Use Mastodon preferences",
"confirmations.deprecated_settings.message": "Some of the glitch-soc device-specific {app_settings} you are using have been replaced by Mastodon {preferences} and will be overriden:",
"confirmations.missing_media_description.confirm": "Send anyway",
"confirmations.missing_media_description.edit": "Edit media",
"confirmations.missing_media_description.message": "At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.",
"confirmations.unfilter.author": "Author",
"confirmations.unfilter.confirm": "Show",
"confirmations.unfilter.edit_filter": "Edit filter",
"confirmations.unfilter.filters": "Matching {count, plural, one {filter} other {filters}}",
"content-type.change": "Content type",
"direct.group_by_conversations": "Group by conversation",
"endorsed_accounts_editor.endorsed_accounts": "Featured accounts",
"favourite_modal.combo": "You can press {combo} to skip this next time",
"getting_started.onboarding": "Show me around",
"home.column_settings.advanced": "Advanced",
"home.column_settings.filter_regex": "Filter out by regular expressions",
"home.column_settings.show_direct": "Show DMs",
"home.settings": "Column settings",
"keyboard_shortcuts.bookmark": "to bookmark",
"keyboard_shortcuts.secondary_toot": "to send toot using secondary privacy setting",
"keyboard_shortcuts.toggle_collapse": "to collapse/uncollapse toots",
"tooltips.reactions": "Reactions",
"layout.auto": "Auto",
"layout.desktop": "Desktop",
"layout.hint.auto": "Automatically chose layout based on “Enable advanced web interface” setting and screen size.",
"layout.hint.desktop": "Use multiple-column layout regardless of the “Enable advanced web interface” setting or screen size.",
"layout.hint.single": "Use single-column layout regardless of the “Enable advanced web interface” setting or screen size.",
"layout.single": "Mobile",
"media_gallery.sensitive": "Sensitive",
"moved_to_warning": "This account is marked as moved to {moved_to_link}, and may thus not accept new follows.",
"navigation_bar.app_settings": "App settings",
"navigation_bar.featured_users": "Featured users",
"navigation_bar.info": "Extended information",
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
"navigation_bar.misc": "Misc",
"notification.markForDeletion": "Mark for deletion",
"notification.reaction": "{name} reacted to your post",
"notifications.column_settings.reaction": "Reactions:",
"notification_purge.btn_all": "Select\nall",
"notification_purge.btn_apply": "Clear\nselected",
"notification_purge.btn_invert": "Invert\nselection",
"notification_purge.btn_none": "Select\nnone",
"notification_purge.start": "Enter notification cleaning mode",
"notifications.marked_clear": "Clear selected notifications",
"notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?",
"onboarding.done": "Done",
"onboarding.next": "Next",
"onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
"onboarding.page_four.home": "The home timeline shows posts from people you follow.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
"onboarding.page_one.federation": "{domain} is an 'instance' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
"onboarding.page_one.welcome": "Welcome to {domain}!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
"onboarding.page_six.almost_done": "Almost done...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}, and is compatible with any Mastodon instance or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "community guidelines",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "mobile apps",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
"onboarding.skip": "Skip",
"settings.always_show_spoilers_field": "Always enable the Content Warning field",
"settings.auto_collapse": "Automatic collapsing",
"settings.auto_collapse_all": "Everything",
"settings.auto_collapse_lengthy": "Lengthy toots",
"settings.auto_collapse_media": "Toots with media",
"settings.auto_collapse_notifications": "Notifications",
"settings.auto_collapse_reblogs": "Boosts",
"settings.auto_collapse_replies": "Replies",
"settings.close": "Close",
"settings.collapsed_statuses": "Collapsed toots",
"settings.compose_box_opts": "Compose box",
"settings.confirm_before_clearing_draft": "Show confirmation dialog before overwriting the message being composed",
"settings.confirm_boost_missing_media_description": "Show confirmation dialog before boosting toots lacking media descriptions",
"settings.confirm_missing_media_description": "Show confirmation dialog before sending toots lacking media descriptions",
"settings.content_warnings": "Content Warnings",
"settings.content_warnings.regexp": "Regular expression",
"settings.content_warnings_filter": "Content warnings to not automatically unfold:",
"settings.content_warnings_media_outside": "Display media attachments outside content warnings",
"settings.content_warnings_media_outside_hint": "Reproduce upstream Mastodon behavior by having the Content Warning toggle not affect media attachments",
"settings.content_warnings_shared_state": "Show/hide content of all copies at once",
"settings.content_warnings_shared_state_hint": "Reproduce upstream Mastodon behavior by having the Content Warning button affect all copies of a post at once. This will prevent automatic collapsing of any copy of a toot with unfolded CW",
"settings.content_warnings_unfold_opts": "Auto-unfolding options",
"settings.deprecated_setting": "This setting is now controlled from Mastodon's {settings_page_link}",
"settings.enable_collapsed": "Enable collapsed toots",
"settings.enable_collapsed_hint": "Collapsed posts have parts of their contents hidden to take up less screen space. This is distinct from the Content Warning feature",
"settings.enter_amount_prompt": "Enter an amount",
"settings.enable_content_warnings_auto_unfold": "Automatically unfold content-warnings",
"settings.general": "General",
"settings.hicolor_privacy_icons": "High color privacy icons",
"settings.hicolor_privacy_icons.hint": "Display privacy icons in bright and easily distinguishable colors",
"settings.image_backgrounds": "Image backgrounds",
"settings.image_backgrounds_media": "Preview collapsed toot media",
"settings.image_backgrounds_media_hint": "If the post has any media attachment, use the first one as a background",
"settings.image_backgrounds_users": "Give collapsed toots an image background",
"settings.inline_preview_cards": "Inline preview cards for external links",
"settings.layout": "Layout:",
"settings.layout_opts": "Layout options",
"settings.media": "Media",
"settings.media_fullwidth": "Full-width media previews",
"settings.num_visible_reactions": "Number of visible reactions",
"settings.media_letterbox": "Letterbox media",
"settings.media_letterbox_hint": "Scale down and letterbox media to fill the image containers instead of stretching and cropping them",
"settings.media_reveal_behind_cw": "Reveal sensitive media behind a CW by default",
"settings.notifications.favicon_badge": "Unread notifications favicon badge",
"settings.notifications.favicon_badge.hint": "Add a badge for unread notifications to the favicon",
"settings.notifications.tab_badge": "Unread notifications badge",
"settings.notifications.tab_badge.hint": "Display a badge for unread notifications in the column icons when the notifications column isn't open",
"settings.notifications_opts": "Notifications options",
"settings.pop_in_left": "Left",
"settings.pop_in_player": "Enable pop-in player",
"settings.pop_in_position": "Pop-in player position:",
"settings.pop_in_right": "Right",
"settings.preferences": "User preferences",
"settings.prepend_cw_re": "Prepend “re: ” to content warnings when replying",
"settings.preselect_on_reply": "Pre-select usernames on reply",
"settings.preselect_on_reply_hint": "When replying to a conversation with multiple participants, pre-select usernames past the first",
"settings.rewrite_mentions": "Rewrite mentions in displayed statuses",
"settings.rewrite_mentions_acct": "Rewrite with username and domain (when the account is remote)",
"settings.rewrite_mentions_no": "Do not rewrite mentions",
"settings.rewrite_mentions_username": "Rewrite with username",
"settings.shared_settings_link": "user preferences",
"settings.show_action_bar": "Show action buttons in collapsed toots",
"settings.show_content_type_choice": "Show content-type choice when authoring toots",
"settings.show_reply_counter": "Display an estimate of the reply count",
"settings.side_arm": "Secondary toot button:",
"settings.side_arm.none": "None",
"settings.side_arm_reply_mode": "When replying to a toot, the secondary toot button should:",
"settings.side_arm_reply_mode.copy": "Copy privacy setting of the toot being replied to",
"settings.side_arm_reply_mode.keep": "Keep its set privacy",
"settings.side_arm_reply_mode.restrict": "Restrict privacy setting to that of the toot being replied to",
"settings.status_icons": "Toot icons",
"settings.status_icons_language": "Language indicator",
"settings.status_icons_local_only": "Local-only indicator",
"settings.status_icons_media": "Media and poll indicators",
"settings.status_icons_reply": "Reply indicator",
"settings.status_icons_visibility": "Toot privacy indicator",
"settings.swipe_to_change_columns": "Allow swiping to change columns (Mobile only)",
"settings.tag_misleading_links": "Tag misleading links",
"settings.tag_misleading_links.hint": "Add a visual indication with the link target host to every link not mentioning it explicitly",
"settings.wide_view": "Wide view (Desktop mode only)",
"settings.wide_view_hint": "Stretches columns to better fill the available space.",
"status.collapse": "Collapse",
"status.react": "React",
"status.has_audio": "Features attached audio files",
"status.has_pictures": "Features attached pictures",
"status.has_preview_card": "Features an attached preview card",
"status.has_video": "Features attached videos",
"status.in_reply_to": "This toot is a reply",
"status.is_poll": "This toot is a poll",
"status.local_only": "Only visible from your instance",
"status.sensitive_toggle": "Click to view",
"status.uncollapse": "Uncollapse",
"web_app_crash.change_your_settings": "Change your {settings}",
"web_app_crash.content": "You could try any of the following:",
"web_app_crash.debug_info": "Debug information",
"web_app_crash.disable_addons": "Disable browser add-ons or built-in translation tools",
"web_app_crash.issue_tracker": "issue tracker",
"web_app_crash.reload": "Reload",
"web_app_crash.reload_page": "{reload} the current page",
"web_app_crash.report_issue": "Report a bug in the {issuetracker}",
"web_app_crash.settings": "settings",
"web_app_crash.title": "We're sorry, but something went wrong with the Mastodon app."
}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/eo.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

View file

@ -0,0 +1,52 @@
{
"account.add_account_note": "Aldoni noton por @{name}",
"account_note.cancel": "Nuligi",
"account_note.edit": "Redakti",
"account_note.save": "Konservi",
"column.reblogged_by": "Diskonigita de",
"column.subheading": "Diversaj agordoj",
"column_header.profile": "Profilo",
"column_subheading.lists": "Listoj",
"compose.attach": "Aldoni…",
"compose.attach.doodle": "Desegni ion",
"compose.attach.upload": "Alŝuti dosieron",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",
"confirmations.unfilter.author": "Aŭtoro",
"confirmations.unfilter.confirm": "Montri",
"confirmations.unfilter.edit_filter": "Redakti filtrilon",
"navigation_bar.keyboard_shortcuts": "Fulmoklavoj",
"notification_purge.btn_all": "Selekti ĉiujn",
"notification_purge.btn_apply": "Forigi selektajn",
"notification_purge.btn_invert": "Inverti selekton",
"notification_purge.btn_none": "Elekti neniun",
"notifications.marked_clear": "Forigi selektajn sciigojn",
"onboarding.next": "Sekva",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.almost_done": "Preskaŭ finita…",
"onboarding.page_six.apps_available": "Estas {apps} disponeblaj por iOS, Android kaj aliaj sistemoj.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.various_app": "poŝtelefonaj aplikaĵoj",
"settings.auto_collapse_all": "Ĉiuj",
"settings.auto_collapse_lengthy": "Longaj afiŝoj",
"settings.auto_collapse_media": "Afiŝoj kun aŭdovidaĵoj",
"settings.auto_collapse_notifications": "Sciigoj",
"settings.auto_collapse_reblogs": "Diskonigoj",
"settings.auto_collapse_replies": "Respondoj",
"settings.close": "Fermi",
"settings.content_warnings": "Content warnings",
"settings.content_warnings.regexp": "Regula esprimo",
"settings.preferences": "Preferences",
"settings.shared_settings_link": "preferoj de uzanto",
"settings.side_arm": "Duaranga butono por afiŝi:",
"settings.side_arm.none": "Neniu",
"settings.status_icons": "Ikonoj sur la afiŝoj",
"settings.status_icons_language": "Indikilo de lingvo",
"settings.status_icons_media": "Indikilo de aŭdovidaĵojn kaj balotenketo",
"settings.status_icons_reply": "Indikilo de respondoj",
"settings.status_icons_visibility": "Indikilo de privateco de afiŝo",
"web_app_crash.change_your_settings": "Ŝanĝi viajn {settings}",
"web_app_crash.reload": "Reŝarĝi",
"web_app_crash.reload_page": "{reload} la nunan paĝon",
"web_app_crash.settings": "agordojn"
}

View file

@ -1,7 +0,0 @@
import inherited from 'mastodon/locales/es-AR.json';
const messages = {
// No translations available.
};
export default Object.assign({}, inherited, messages);

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