Compare commits

...

60 Commits

Author SHA1 Message Date
renovate[bot]
e1af321bda
fix(deps): update dependency @reduxjs/toolkit to v2.8.0 2025-05-07 03:48:51 +00:00
Claire
fbe9728f36
Bump version to v4.3.8 (#34626)
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
2025-05-06 14:17:07 +00:00
Claire
3bbf3e9709
Fix code style issue (#34624) 2025-05-06 13:35:54 +00:00
Claire
79931bf3ae
Merge commit from fork
* Check scheme in account and post links

* Harden media attachments

* Client-side mitigation

* Client-side mitigation for media attachments
2025-05-06 15:02:13 +02:00
Claire
22e2e7f02b
Fix crash when likes or shares collections are not inlined, for real (#34619)
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
2025-05-06 09:51:42 +00:00
Claire
41d00bc28b
Fix libvips being unconditionally required by tasks (#34620) 2025-05-06 09:45:32 +00:00
Claire
3e5d78cc5b
Fix crash when likes or shares collections are not inlined (#34618) 2025-05-06 07:39:26 +00:00
Renaud Chaput
df6b808750
fix: do not use the deprecated /api/v1/instance end point (#34613) 2025-05-06 06:08:44 +00:00
Claire
aedc5f6921
Add warning for REDIS_NAMESPACE deprecation at startup (#34581)
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
2025-05-05 13:01:16 +00:00
Claire
89cafb01b4
Remove double-query for signed query strings (#34610) 2025-05-05 12:33:31 +00:00
renovate[bot]
2133f2b47e
fix(deps): update dependency babel-plugin-formatjs to v10.5.38 (#34609)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Bundler Audit / security (push) Has been cancelled
CSS Linting / lint (push) Has been cancelled
Haml Linting / lint (push) Has been cancelled
JavaScript Linting / lint (push) Has been cancelled
JavaScript Testing / test (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 08:26:12 +00:00
renovate[bot]
833ea0725d
chore(deps): update dependency rubocop to v1.75.5 (#34608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 08:26:07 +00:00
github-actions[bot]
eacf6f2342
New Crowdin Translations (automated) (#34596)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-05-05 08:23:27 +00:00
renovate[bot]
84bca6fd54
chore(deps): update dependency public_suffix to v6.0.2 (#34590)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 08:23:21 +00:00
Claire
cbaba54e9d
Add support for importing embedded self-quotes (#34584) 2025-05-05 08:01:16 +00:00
renovate[bot]
d41a741e00
fix(deps): update dependency ws to v8.18.2 (#34603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 07:55:52 +00:00
renovate[bot]
03a0f7caf9
chore(deps): update dependency selenium-webdriver to v4.32.0 (#34604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 07:55:34 +00:00
Jonny Saunders
8b34daf254
Fix: Use strings not symbols to access totalItems in interaction collections (#34594)
Some checks failed
Check i18n / check-i18n (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (ruby) (push) Has been cancelled
Check formatting / lint (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
Ruby Testing / build (production) (push) Has been cancelled
Ruby Testing / build (test) (push) Has been cancelled
Ruby Testing / test (.ruby-version) (push) Has been cancelled
Ruby Testing / test (3.2) (push) Has been cancelled
Ruby Testing / test (3.3) (push) Has been cancelled
Ruby Testing / Libvips tests (.ruby-version) (push) Has been cancelled
Ruby Testing / Libvips tests (3.2) (push) Has been cancelled
Ruby Testing / Libvips tests (3.3) (push) Has been cancelled
Ruby Testing / End to End testing (.ruby-version) (push) Has been cancelled
Ruby Testing / End to End testing (3.2) (push) Has been cancelled
Ruby Testing / End to End testing (3.3) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Bundler Audit / security (push) Has been cancelled
2025-05-03 10:37:06 +00:00
Eugen Rochko
b4394ec129
Change design of audio player in web UI (#34520)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
CSS Linting / lint (push) Has been cancelled
JavaScript Linting / lint (push) Has been cancelled
JavaScript Testing / test (push) Has been cancelled
2025-05-02 16:15:00 +00:00
renovate[bot]
24c25ec4f5
fix(deps): update babel monorepo to v7.27.1 (#34592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-02 14:45:49 +00:00
github-actions[bot]
94fa5b7168
New Crowdin Translations (automated) (#34587)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Bundler Audit / security (push) Has been cancelled
Haml Linting / lint (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
Co-authored-by: GitHub Actions <noreply@github.com>
2025-05-02 06:59:30 +00:00
Matt Jankowski
4354f84c5c
Update rspec-rails to version 8.0.0 (#34588) 2025-05-02 06:33:20 +00:00
Patryk Rzucidło
e3f0b955b8
Fix directory scroll position reset (#34560)
Some checks failed
Check i18n / check-i18n (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (ruby) (push) Has been cancelled
Check formatting / lint (push) Has been cancelled
JavaScript Linting / lint (push) Has been cancelled
JavaScript Testing / test (push) Has been cancelled
Ruby Testing / build (production) (push) Has been cancelled
Ruby Testing / build (test) (push) Has been cancelled
Ruby Testing / test (.ruby-version) (push) Has been cancelled
Ruby Testing / test (3.2) (push) Has been cancelled
Ruby Testing / test (3.3) (push) Has been cancelled
Ruby Testing / Libvips tests (.ruby-version) (push) Has been cancelled
Ruby Testing / Libvips tests (3.2) (push) Has been cancelled
Ruby Testing / Libvips tests (3.3) (push) Has been cancelled
Ruby Testing / End to End testing (.ruby-version) (push) Has been cancelled
Ruby Testing / End to End testing (3.2) (push) Has been cancelled
Ruby Testing / End to End testing (3.3) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
2025-04-30 12:27:37 +00:00
renovate[bot]
05f6f7d28a
fix(deps): update dependency core-js to v3.42.0 (#34577)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
CSS Linting / lint (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-30 07:50:13 +00:00
github-actions[bot]
64ab9be93f
New Crowdin Translations (automated) (#34580)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-04-30 07:31:08 +00:00
renovate[bot]
a2310a06fa
fix(deps): update dependency axios to v1.9.0 (#34547)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
CSS Linting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Crowdin / Upload translations / upload-translations (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 14:40:02 +00:00
Eugen Rochko
79013c730d
Add endorsed accounts to profiles in web UI (#34568) 2025-04-29 12:14:22 +00:00
Claire
b81c28e7dc
Fix edit dropdown crashing the web interface on mobile (#34564) 2025-04-29 09:48:54 +00:00
Claire
ce13fca0c5
Add built-in context for interaction policies (#34574) 2025-04-29 08:51:03 +00:00
Claire
98e6dfcbcf
Fix context selector trying to mutate immutable state (#34573)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Bundler Audit / security (push) Has been cancelled
Haml Linting / lint (push) Has been cancelled
2025-04-29 08:12:05 +00:00
renovate[bot]
7cb93ef5a1
chore(deps): update dependency connection_pool to v2.5.3 (#34569)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 07:23:53 +00:00
github-actions[bot]
66d9e47178
New Crowdin Translations (automated) (#34572)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-04-29 07:05:02 +00:00
renovate[bot]
e7dd0b37c7
chore(deps): update dependency rubocop to v1.75.4 (#34570)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 06:47:42 +00:00
renovate[bot]
b0e63fbe1c
chore(deps): update dependency rqrcode to v3.1.0 (#34565)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 06:19:10 +00:00
Essem
e96044f389
Update to Twemoji 15.1.0 (#34321)
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Crowdin / Upload translations / upload-translations (push) Waiting to run
Check formatting / lint (push) Waiting to run
CSS Linting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
2025-04-28 14:22:14 +00:00
Eugen Rochko
715cbee93d
Fix dashboard warning about Elasticsearch index mismatch not showing up (#34567)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-04-28 13:59:49 +00:00
Eugen Rochko
17d8e2b6e3
Refactor context reducer to TypeScript (#34506) 2025-04-28 13:38:40 +00:00
Eugen Rochko
bd9223f0b9
Fix tootctl search deploy --only-mapping not updating index settings (#34566) 2025-04-28 13:02:41 +00:00
Eugen Rochko
40157e063d
Add ability to feature and unfeature hashtags from web UI (#34490) 2025-04-28 11:44:01 +00:00
Eugen Rochko
926c67c648
Refactor <ActionsModal> to TypeScript (#34559) 2025-04-28 11:43:42 +00:00
Claire
17e4345eb2
Add quoted_status attribute to PostStatusService for local testing (#34553) 2025-04-28 10:07:22 +00:00
Claire
9ed6a14d45
Add support for ingesting quote policies (#34479) 2025-04-28 08:48:27 +00:00
renovate[bot]
1a1f3f037d
fix(deps): update dependency pg to v8.15.6 (#34555)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
CSS Linting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Bundler Audit / security (push) Has been cancelled
Haml Linting / lint (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 07:34:18 +00:00
renovate[bot]
3032d9d0dd
chore(deps): update dependency shoulda-matchers to v6.5.0 (#34556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 07:33:51 +00:00
github-actions[bot]
a20686f593
New Crowdin Translations (automated) (#34558)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-04-28 07:33:20 +00:00
Claire
ae3b7dd28d
Reject incoming QuoteRequest activities (#34480)
Some checks failed
Check i18n / check-i18n (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (ruby) (push) Has been cancelled
Check formatting / lint (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
Ruby Testing / build (production) (push) Has been cancelled
Ruby Testing / build (test) (push) Has been cancelled
Ruby Testing / test (.ruby-version) (push) Has been cancelled
Ruby Testing / test (3.2) (push) Has been cancelled
Ruby Testing / test (3.3) (push) Has been cancelled
Ruby Testing / Libvips tests (.ruby-version) (push) Has been cancelled
Ruby Testing / Libvips tests (3.2) (push) Has been cancelled
Ruby Testing / Libvips tests (3.3) (push) Has been cancelled
Ruby Testing / End to End testing (.ruby-version) (push) Has been cancelled
Ruby Testing / End to End testing (3.2) (push) Has been cancelled
Ruby Testing / End to End testing (3.3) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Bundler Audit / security (push) Has been cancelled
2025-04-25 23:28:09 +00:00
Claire
8f59b63176
Change quote IDs to use snowflake IDs (#34551) 2025-04-25 23:24:26 +00:00
Eugen Rochko
a97647158c
Add REST API for featuring and unfeaturing a hashtag (#34489)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Crowdin / Upload translations / upload-translations (push) Has been cancelled
JavaScript Linting / lint (push) Has been cancelled
JavaScript Testing / test (push) Has been cancelled
Co-authored-by: Matt Jankowski <matt@jankowski.online>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-04-25 15:12:05 +00:00
Eugen Rochko
49b6a49c76
Change "Pin on profile" to "Feature on profile" for posts in web UI (#34492) 2025-04-25 15:11:59 +00:00
Claire
d4944a2467
Fix incorrect redirect in response to unauthenticated API requests in limited federation mode (#34549)
Some checks failed
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Crowdin / Upload translations / upload-translations (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Bundler Audit / security (push) Has been cancelled
CSS Linting / lint (push) Has been cancelled
Haml Linting / lint (push) Has been cancelled
2025-04-25 11:24:57 +00:00
Eugen Rochko
91db45b197
Change account search to be more forgiving of spaces (#34455) 2025-04-25 10:35:21 +00:00
Claire
7a70d95435
Add warning for Elasticsearch index analyzers mismatch (#34515) 2025-04-25 10:35:11 +00:00
renovate[bot]
1326c8cb1d
chore(deps): update dependency rqrcode to v3 (#34541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-25 09:54:56 +00:00
Claire
199acce481
Fix sign-up e-mail confirmation page reloading on error or redirect (#34548) 2025-04-25 09:00:54 +00:00
github-actions[bot]
b1b949f16c
New Crowdin Translations (automated) (#34546)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-04-25 06:44:41 +00:00
Matt Jankowski
6463415e06
Update rubocop-rspec to version 3.6.0 (#34497) 2025-04-24 14:56:13 +00:00
Claire
22ec828951
Change DEFAULT_LOCALE to not override unauthenticated users' browser language (#34535) 2025-04-24 11:38:27 +00:00
Terence Eden
13b13c8726
Reduce path size for oEmbed and logo (#34538)
Co-authored-by: Terence Eden <git@shkspr.mobi>
2025-04-24 11:38:02 +00:00
renovate[bot]
5679bb5394
fix(deps): update dependency pg to v8.15.5 (#34531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-24 08:24:16 +00:00
github-actions[bot]
1fc66c1970
New Crowdin Translations (automated) (#34534)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-04-24 08:06:10 +00:00
464 changed files with 4964 additions and 3520 deletions

View File

@ -63,7 +63,7 @@ docker-compose.override.yml
# Ignore emoji map file # Ignore emoji map file
/app/javascript/mastodon/features/emoji/emoji_map.json /app/javascript/mastodon/features/emoji/emoji_map.json
/app/javascript/mastodon/features/emoji/emoji_sheet.json /app/javascript/mastodon/features/emoji/emoji_data.json
# Ignore locale files # Ignore locale files
/app/javascript/mastodon/locales/*.json /app/javascript/mastodon/locales/*.json

View File

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.75.2. # using RuboCop version 1.75.3.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new

View File

@ -2,9 +2,34 @@
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.3.8] - 2025-05-06
### Security
- Update dependencies
- Check scheme on account, profile, and media URLs ([GHSA-x2rc-v5wx-g3m5](https://github.com/mastodon/mastodon/security/advisories/GHSA-x2rc-v5wx-g3m5))
### Added
- Add warning for REDIS_NAMESPACE deprecation at startup (#34581 by @ClearlyClaire)
- Add built-in context for interaction policies (#34574 by @ClearlyClaire)
### Changed
- Change activity distribution error handling to skip retrying for deleted accounts (#33617 by @ClearlyClaire)
### Removed
- Remove double-query for signed query strings (#34610 by @ClearlyClaire)
### Fixed
- Fix incorrect redirect in response to unauthenticated API requests in limited federation mode (#34549 by @ClearlyClaire)
- Fix sign-up e-mail confirmation page reloading on error or redirect (#34548 by @ClearlyClaire)
## [4.3.7] - 2025-04-02 ## [4.3.7] - 2025-04-02
### Add ### Added
- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire) - Add delay to profile updates to debounce them (#34137 by @ClearlyClaire)
- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire) - Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire)

View File

@ -79,7 +79,7 @@ gem 'rails-i18n', '~> 8.0'
gem 'redcarpet', '~> 3.6' gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'redis-namespace', '~> 1.10' gem 'redis-namespace', '~> 1.10'
gem 'rqrcode', '~> 2.2' gem 'rqrcode', '~> 3.0'
gem 'ruby-progressbar', '~> 1.13' gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 7.0' gem 'sanitize', '~> 7.0'
gem 'scenic', '~> 1.7' gem 'scenic', '~> 1.7'
@ -212,7 +212,7 @@ group :development, :test do
gem 'test-prof', require: false gem 'test-prof', require: false
# RSpec runner for rails # RSpec runner for rails
gem 'rspec-rails', '~> 7.0' gem 'rspec-rails', '~> 8.0'
end end
group :production do group :production do

View File

@ -160,7 +160,7 @@ GEM
cocoon (1.2.15) cocoon (1.2.15)
color_diff (0.1) color_diff (0.1)
concurrent-ruby (1.3.5) concurrent-ruby (1.3.5)
connection_pool (2.5.2) connection_pool (2.5.3)
cose (1.3.1) cose (1.3.1)
cbor (~> 0.5.9) cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0) openssl-signature_algorithm (~> 1.0)
@ -435,7 +435,7 @@ GEM
mutex_m (0.3.0) mutex_m (0.3.0)
net-http (0.6.0) net-http (0.6.0)
uri uri
net-imap (0.5.6) net-imap (0.5.8)
date date
net-protocol net-protocol
net-ldap (0.19.0) net-ldap (0.19.0)
@ -620,7 +620,7 @@ GEM
psych (5.2.3) psych (5.2.3)
date date
stringio stringio
public_suffix (6.0.1) public_suffix (6.0.2)
puma (6.6.0) puma (6.6.0)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.5.0) pundit (2.5.0)
@ -711,28 +711,28 @@ GEM
rotp (6.3.0) rotp (6.3.0)
rouge (4.5.1) rouge (4.5.1)
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (2.2.0) rqrcode (3.1.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 2.0)
rqrcode_core (1.2.0) rqrcode_core (2.0.0)
rspec (3.13.0) rspec (3.13.0)
rspec-core (~> 3.13.0) rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0) rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0) rspec-mocks (~> 3.13.0)
rspec-core (3.13.3) rspec-core (3.13.3)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.3) rspec-expectations (3.13.4)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-github (3.0.0) rspec-github (3.0.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
rspec-mocks (3.13.2) rspec-mocks (3.13.3)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (7.1.1) rspec-rails (8.0.0)
actionpack (>= 7.0) actionpack (>= 7.2)
activesupport (>= 7.0) activesupport (>= 7.2)
railties (>= 7.0) railties (>= 7.2)
rspec-core (~> 3.13) rspec-core (~> 3.13)
rspec-expectations (~> 3.13) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
@ -742,8 +742,8 @@ GEM
rspec-expectations (~> 3.0) rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9) sidekiq (>= 5, < 9)
rspec-support (3.13.2) rspec-support (3.13.3)
rubocop (1.75.2) rubocop (1.75.5)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@ -773,7 +773,7 @@ GEM
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rspec (3.5.0) rubocop-rspec (3.6.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1) rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0) rubocop-rspec_rails (2.31.0)
@ -800,14 +800,14 @@ GEM
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 4.0.0) railties (>= 4.0.0)
securerandom (0.4.1) securerandom (0.4.1)
selenium-webdriver (4.31.0) selenium-webdriver (4.32.0)
base64 (~> 0.2) base64 (~> 0.2)
logger (~> 1.4) logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0) websocket (~> 1.0)
semantic_range (3.1.0) semantic_range (3.1.0)
shoulda-matchers (6.4.0) shoulda-matchers (6.5.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (6.5.12) sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3) connection_pool (>= 2.2.5, < 3)
@ -842,7 +842,7 @@ GEM
base64 base64
stoplight (4.1.1) stoplight (4.1.1)
redlock (~> 1.0) redlock (~> 1.0)
stringio (3.1.6) stringio (3.1.7)
strong_migrations (2.3.0) strong_migrations (2.3.0)
activerecord (>= 7) activerecord (>= 7)
swd (2.0.3) swd (2.0.3)
@ -1043,9 +1043,9 @@ DEPENDENCIES
redcarpet (~> 3.6) redcarpet (~> 3.6)
redis (~> 4.5) redis (~> 4.5)
redis-namespace (~> 1.10) redis-namespace (~> 1.10)
rqrcode (~> 2.2) rqrcode (~> 3.0)
rspec-github (~> 3.0) rspec-github (~> 3.0)
rspec-rails (~> 7.0) rspec-rails (~> 8.0)
rspec-sidekiq (~> 5.0) rspec-sidekiq (~> 5.0)
rubocop rubocop
rubocop-capybara rubocop-capybara

View File

@ -19,9 +19,16 @@ class AccountsIndex < Chewy::Index
type: 'stemmer', type: 'stemmer',
language: 'possessive_english', language: 'possessive_english',
}, },
word_joiner: {
type: 'shingle',
output_unigrams: true,
token_separator: '',
},
}, },
analyzer: { analyzer: {
# "The FOOING's bar" becomes "foo bar"
natural: { natural: {
tokenizer: 'standard', tokenizer: 'standard',
filter: %w( filter: %w(
@ -35,11 +42,20 @@ class AccountsIndex < Chewy::Index
), ),
}, },
# "FOO bar" becomes "foo bar"
verbatim: { verbatim: {
tokenizer: 'standard', tokenizer: 'standard',
filter: %w(lowercase asciifolding cjk_width), filter: %w(lowercase asciifolding cjk_width),
}, },
# "Foo bar" becomes "foo bar foobar"
word_join_analyzer: {
type: 'custom',
tokenizer: 'standard',
filter: %w(lowercase asciifolding cjk_width word_joiner),
},
# "Foo bar" becomes "f fo foo b ba bar"
edge_ngram: { edge_ngram: {
tokenizer: 'edge_ngram', tokenizer: 'edge_ngram',
filter: %w(lowercase asciifolding cjk_width), filter: %w(lowercase asciifolding cjk_width),

View File

@ -72,6 +72,13 @@ class Api::BaseController < ApplicationController
end end
end end
# Redefine `require_functional!` to properly output JSON instead of HTML redirects
def require_functional!
return if current_user.functional?
require_user!
end
def render_empty def render_empty
render json: {}, status: 200 render json: {}, status: 200
end end

View File

@ -18,7 +18,7 @@ class Api::V1::FeaturedTagsController < Api::BaseController
end end
def destroy def destroy
RemoveFeaturedTagWorker.perform_async(current_account.id, @featured_tag.id) RemoveFeaturedTagService.new.call(current_account, @featured_tag)
render_empty render_empty
end end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::TagsController < Api::BaseController class Api::V1::TagsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, except: :show before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, only: [:follow, :unfollow]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:feature, :unfeature]
before_action :require_user!, except: :show before_action :require_user!, except: :show
before_action :set_or_create_tag before_action :set_or_create_tag
@ -23,6 +24,16 @@ class Api::V1::TagsController < Api::BaseController
render json: @tag, serializer: REST::TagSerializer render json: @tag, serializer: REST::TagSerializer
end end
def feature
CreateFeaturedTagService.new.call(current_account, @tag)
render json: @tag, serializer: REST::TagSerializer
end
def unfeature
RemoveFeaturedTagService.new.call(current_account, @tag)
render json: @tag, serializer: REST::TagSerializer
end
private private
def set_or_create_tag def set_or_create_tag

View File

@ -72,10 +72,24 @@ class ApplicationController < ActionController::Base
def require_functional! def require_functional!
return if current_user.functional? return if current_user.functional?
if current_user.confirmed? respond_to do |format|
redirect_to edit_user_registration_path format.any do
else if current_user.confirmed?
redirect_to auth_setup_path redirect_to edit_user_registration_path
else
redirect_to auth_setup_path
end
end
format.json do
if !current_user.confirmed?
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
elsif !current_user.approved?
render json: { error: 'Your login is currently pending approval' }, status: 403
elsif !current_user.functional?
render json: { error: 'Your login is currently disabled' }, status: 403
end
end
end end
end end

View File

@ -16,7 +16,7 @@ module Localized
def requested_locale def requested_locale
requested_locale_name = available_locale_or_nil(params[:lang]) requested_locale_name = available_locale_or_nil(params[:lang])
requested_locale_name ||= available_locale_or_nil(current_user.locale) if respond_to?(:user_signed_in?) && user_signed_in? requested_locale_name ||= available_locale_or_nil(current_user.locale) if respond_to?(:user_signed_in?) && user_signed_in?
requested_locale_name ||= http_accept_language if ENV['DEFAULT_LOCALE'].blank? requested_locale_name ||= http_accept_language unless ENV['FORCE_DEFAULT_LOCALE'] == 'true'
requested_locale_name requested_locale_name
end end

View File

@ -12,7 +12,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
end end
def create def create
@featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name], force: false) @featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name], raise_error: false)
if @featured_tag.valid? if @featured_tag.valid?
redirect_to settings_featured_tags_path redirect_to settings_featured_tags_path

View File

@ -25,6 +25,14 @@ module ContextHelper
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } }, attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
interaction_policies: {
'gts' => 'https://gotosocial.org/ns#',
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },
'canQuote' => { '@id' => 'gts:canQuote', '@type' => '@id' },
'automaticApproval' => { '@id' => 'gts:automaticApproval', '@type' => '@id' },
'manualApproval' => { '@id' => 'gts:manualApproval', '@type' => '@id' },
},
}.freeze }.freeze
def full_context def full_context

View File

@ -72,6 +72,18 @@ module JsonLdHelper
!haystack.casecmp(needle).zero? !haystack.casecmp(needle).zero?
end end
def safe_prefetched_embed(account, object, context)
return unless object.is_a?(Hash)
# NOTE: Replacing the object's context by that of the parent activity is
# not sound, but it's consistent with the rest of the codebase
object = object.merge({ '@context' => context })
return if value_or_id(first_of_value(object['attributedTo'])) != account.uri || non_matching_uri_hosts?(account.uri, object['id'])
object
end
def canonicalize(json) def canonicalize(json)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context)) graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
graph.dump(:normalize) graph.dump(:normalize)

View File

@ -4,9 +4,12 @@ import axios from 'axios';
import ready from '../mastodon/ready'; import ready from '../mastodon/ready';
async function checkConfirmation() { async function checkConfirmation() {
const response = await axios.get('/api/v1/emails/check_confirmation'); const response = await axios.get('/api/v1/emails/check_confirmation', {
headers: { Accept: 'application/json' },
withCredentials: true,
});
if (response.data) { if (response.status === 200 && response.data === true) {
window.location.href = '/start'; window.location.href = '/start';
} }
} }

View File

@ -1,2 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="79" height="79" viewBox="0 0 79 75"><symbol id="logo-symbol-icon"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor"/></symbol><use xlink:href="#logo-symbol-icon"/></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="79" height="79" viewBox="0 0 79 75"><symbol id="logo-symbol-icon"><path d="M63 45.3v-20c0-4.1-1-7.3-3.2-9.7-2.1-2.4-5-3.7-8.5-3.7-4.1 0-7.2 1.6-9.3 4.7l-2 3.3-2-3.3c-2-3.1-5.1-4.7-9.2-4.7-3.5 0-6.4 1.3-8.6 3.7-2.1 2.4-3.1 5.6-3.1 9.7v20h8V25.9c0-4.1 1.7-6.2 5.2-6.2 3.8 0 5.8 2.5 5.8 7.4V37.7H44V27.1c0-4.9 1.9-7.4 5.8-7.4 3.5 0 5.2 2.1 5.2 6.2V45.3h8ZM74.7 16.6c.6 6 .1 15.7.1 17.3 0 .5-.1 4.8-.1 5.3-.7 11.5-8 16-15.6 17.5-.1 0-.2 0-.3 0-4.9 1-10 1.2-14.9 1.4-1.2 0-2.4 0-3.6 0-4.8 0-9.7-.6-14.4-1.7-.1 0-.1 0-.1 0s-.1 0-.1 0 0 .1 0 .1 0 0 0 0c.1 1.6.4 3.1 1 4.5.6 1.7 2.9 5.7 11.4 5.7 5 0 9.9-.6 14.8-1.7 0 0 0 0 0 0 .1 0 .1 0 .1 0 0 .1 0 .1 0 .1.1 0 .1 0 .1.1v5.6s0 .1-.1.1c0 0 0 0 0 .1-1.6 1.1-3.7 1.7-5.6 2.3-.8.3-1.6.5-2.4.7-7.5 1.7-15.4 1.3-22.7-1.2-6.8-2.4-13.8-8.2-15.5-15.2-.9-3.8-1.6-7.6-1.9-11.5-.6-5.8-.6-11.7-.8-17.5C3.9 24.5 4 20 4.9 16 6.7 7.9 14.1 2.2 22.3 1c1.4-.2 4.1-1 16.5-1h.1C51.4 0 56.7.8 58.1 1c8.4 1.2 15.5 7.5 16.6 15.6Z" fill="currentColor"/></symbol><use xlink:href="#logo-symbol-icon"/></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,18 +1,18 @@
import { createAction } from '@reduxjs/toolkit'; import { createAction } from '@reduxjs/toolkit';
import { apiRemoveAccountFromFollowers } from 'mastodon/api/accounts'; import {
import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; apiRemoveAccountFromFollowers,
apiGetEndorsedAccounts,
} from 'mastodon/api/accounts';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import { importFetchedAccounts } from './importer';
export const revealAccount = createAction<{ export const revealAccount = createAction<{
id: string; id: string;
}>('accounts/revealAccount'); }>('accounts/revealAccount');
export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>(
'accounts/importAccounts',
);
function actionWithSkipLoadingTrue<Args extends object>(args: Args) { function actionWithSkipLoadingTrue<Args extends object>(args: Args) {
return { return {
payload: { payload: {
@ -104,3 +104,12 @@ export const removeAccountFromFollowers = createDataLoadingThunk(
apiRemoveAccountFromFollowers(accountId), apiRemoveAccountFromFollowers(accountId),
(relationship) => ({ relationship }), (relationship) => ({ relationship }),
); );
export const fetchEndorsedAccounts = createDataLoadingThunk(
'accounts/endorsements',
({ accountId }: { accountId: string }) => apiGetEndorsedAccounts(accountId),
(data, { dispatch }) => {
dispatch(importFetchedAccounts(data));
return data;
},
);

View File

@ -1,34 +0,0 @@
import api from '../api';
export const FEATURED_TAGS_FETCH_REQUEST = 'FEATURED_TAGS_FETCH_REQUEST';
export const FEATURED_TAGS_FETCH_SUCCESS = 'FEATURED_TAGS_FETCH_SUCCESS';
export const FEATURED_TAGS_FETCH_FAIL = 'FEATURED_TAGS_FETCH_FAIL';
export const fetchFeaturedTags = (id) => (dispatch, getState) => {
if (getState().getIn(['user_lists', 'featured_tags', id, 'items'])) {
return;
}
dispatch(fetchFeaturedTagsRequest(id));
api().get(`/api/v1/accounts/${id}/featured_tags`)
.then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data)))
.catch(err => dispatch(fetchFeaturedTagsFail(id, err)));
};
export const fetchFeaturedTagsRequest = (id) => ({
type: FEATURED_TAGS_FETCH_REQUEST,
id,
});
export const fetchFeaturedTagsSuccess = (id, tags) => ({
type: FEATURED_TAGS_FETCH_SUCCESS,
id,
tags,
});
export const fetchFeaturedTagsFail = (id, error) => ({
type: FEATURED_TAGS_FETCH_FAIL,
id,
error,
});

View File

@ -0,0 +1,7 @@
import { apiGetFeaturedTags } from 'mastodon/api/accounts';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
export const fetchFeaturedTags = createDataLoadingThunk(
'accounts/featured_tags',
({ accountId }: { accountId: string }) => apiGetFeaturedTags(accountId),
);

View File

@ -0,0 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>(
'accounts/importAccounts',
);

View File

@ -1,7 +1,6 @@
import { createPollFromServerJSON } from 'mastodon/models/poll'; import { createPollFromServerJSON } from 'mastodon/models/poll';
import { importAccounts } from '../accounts_typed'; import { importAccounts } from './accounts';
import { normalizeStatus } from './normalizer'; import { normalizeStatus } from './normalizer';
import { importPolls } from './polls'; import { importPolls } from './polls';

View File

@ -77,6 +77,17 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive; normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
if (normalStatus.url && !(normalStatus.url.startsWith('http://') || normalStatus.url.startsWith('https://'))) {
normalStatus.url = null;
}
normalStatus.url ||= normalStatus.uri;
normalStatus.media_attachments.forEach(item => {
if (item.remote_url && !(item.remote_url.startsWith('http://') || item.remote_url.startsWith('https://')))
item.remote_url = null;
});
} }
if (normalOldStatus) { if (normalOldStatus) {

View File

@ -4,8 +4,11 @@ import api from '../api';
import { ensureComposeIsVisible, setComposeToStatus } from './compose'; import { ensureComposeIsVisible, setComposeToStatus } from './compose';
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer'; import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
import { fetchContext } from './statuses_typed';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
export * from './statuses_typed';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL'; export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL';
@ -14,10 +17,6 @@ export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST';
export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS'; export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS';
export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL'; export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL';
export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST';
export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL';
export const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST'; export const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST';
export const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS'; export const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS';
export const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL'; export const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL';
@ -54,7 +53,7 @@ export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null; const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
if (alsoFetchContext) { if (alsoFetchContext) {
dispatch(fetchContext(id)); dispatch(fetchContext({ statusId: id }));
} }
if (skipLoading) { if (skipLoading) {
@ -178,50 +177,6 @@ export function deleteStatusFail(id, error) {
export const updateStatus = status => dispatch => export const updateStatus = status => dispatch =>
dispatch(importFetchedStatus(status)); dispatch(importFetchedStatus(status));
export function fetchContext(id) {
return (dispatch) => {
dispatch(fetchContextRequest(id));
api().get(`/api/v1/statuses/${id}/context`).then(response => {
dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants)));
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
}).catch(error => {
if (error.response && error.response.status === 404) {
dispatch(deleteFromTimelines(id));
}
dispatch(fetchContextFail(id, error));
});
};
}
export function fetchContextRequest(id) {
return {
type: CONTEXT_FETCH_REQUEST,
id,
};
}
export function fetchContextSuccess(id, ancestors, descendants) {
return {
type: CONTEXT_FETCH_SUCCESS,
id,
ancestors,
descendants,
statuses: ancestors.concat(descendants),
};
}
export function fetchContextFail(id, error) {
return {
type: CONTEXT_FETCH_FAIL,
id,
error,
skipAlert: true,
};
}
export function muteStatus(id) { export function muteStatus(id) {
return (dispatch) => { return (dispatch) => {
dispatch(muteStatusRequest(id)); dispatch(muteStatusRequest(id));

View File

@ -0,0 +1,18 @@
import { apiGetContext } from 'mastodon/api/statuses';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import { importFetchedStatuses } from './importer';
export const fetchContext = createDataLoadingThunk(
'status/context',
({ statusId }: { statusId: string }) => apiGetContext(statusId),
(context, { dispatch }) => {
const statuses = context.ancestors.concat(context.descendants);
dispatch(importFetchedStatuses(statuses));
return {
context,
};
},
);

View File

@ -1,4 +1,10 @@
import { apiGetTag, apiFollowTag, apiUnfollowTag } from 'mastodon/api/tags'; import {
apiGetTag,
apiFollowTag,
apiUnfollowTag,
apiFeatureTag,
apiUnfeatureTag,
} from 'mastodon/api/tags';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
export const fetchHashtag = createDataLoadingThunk( export const fetchHashtag = createDataLoadingThunk(
@ -15,3 +21,13 @@ export const unfollowHashtag = createDataLoadingThunk(
'tags/unfollow', 'tags/unfollow',
({ tagId }: { tagId: string }) => apiUnfollowTag(tagId), ({ tagId }: { tagId: string }) => apiUnfollowTag(tagId),
); );
export const featureHashtag = createDataLoadingThunk(
'tags/feature',
({ tagId }: { tagId: string }) => apiFeatureTag(tagId),
);
export const unfeatureHashtag = createDataLoadingThunk(
'tags/unfeature',
({ tagId }: { tagId: string }) => apiUnfeatureTag(tagId),
);

View File

@ -1,5 +1,7 @@
import { apiRequestPost } from 'mastodon/api'; import { apiRequestPost, apiRequestGet } from 'mastodon/api';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
export const apiSubmitAccountNote = (id: string, value: string) => export const apiSubmitAccountNote = (id: string, value: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, { apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
@ -23,3 +25,9 @@ export const apiRemoveAccountFromFollowers = (id: string) =>
apiRequestPost<ApiRelationshipJSON>( apiRequestPost<ApiRelationshipJSON>(
`v1/accounts/${id}/remove_from_followers`, `v1/accounts/${id}/remove_from_followers`,
); );
export const apiGetFeaturedTags = (id: string) =>
apiRequestGet<ApiHashtagJSON>(`v1/accounts/${id}/featured_tags`);
export const apiGetEndorsedAccounts = (id: string) =>
apiRequestGet<ApiAccountJSON>(`v1/accounts/${id}/endorsements`);

View File

@ -0,0 +1,5 @@
import { apiRequestGet } from 'mastodon/api';
import type { ApiContextJSON } from 'mastodon/api_types/statuses';
export const apiGetContext = (statusId: string) =>
apiRequestGet<ApiContextJSON>(`v1/statuses/${statusId}/context`);

View File

@ -10,6 +10,12 @@ export const apiFollowTag = (tagId: string) =>
export const apiUnfollowTag = (tagId: string) => export const apiUnfollowTag = (tagId: string) =>
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfollow`); apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfollow`);
export const apiFeatureTag = (tagId: string) =>
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/feature`);
export const apiUnfeatureTag = (tagId: string) =>
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfeature`);
export const apiGetFollowedTags = async (url?: string) => { export const apiGetFollowedTags = async (url?: string) => {
const response = await api().request<ApiHashtagJSON[]>({ const response = await api().request<ApiHashtagJSON[]>({
method: 'GET', method: 'GET',

View File

@ -119,3 +119,8 @@ export interface ApiStatusJSON {
card?: ApiPreviewCardJSON; card?: ApiPreviewCardJSON;
poll?: ApiPollJSON; poll?: ApiPollJSON;
} }
export interface ApiContextJSON {
ancestors: ApiStatusJSON[];
descendants: ApiStatusJSON[];
}

View File

@ -10,4 +10,5 @@ export interface ApiHashtagJSON {
url: string; url: string;
history: [ApiHistoryJSON, ...ApiHistoryJSON[]]; history: [ApiHistoryJSON, ...ApiHistoryJSON[]];
following?: boolean; following?: boolean;
featuring?: boolean;
} }

View File

@ -96,13 +96,19 @@ export const decode83 = (str: string) => {
return value; return value;
}; };
export const intToRGB = (int: number) => ({ export interface RGB {
r: number;
g: number;
b: number;
}
export const intToRGB = (int: number): RGB => ({
r: Math.max(0, int >> 16), r: Math.max(0, int >> 16),
g: Math.max(0, (int >> 8) & 255), g: Math.max(0, (int >> 8) & 255),
b: Math.max(0, int & 255), b: Math.max(0, int & 255),
}); });
export const getAverageFromBlurhash = (blurhash: string) => { export const getAverageFromBlurhash = (blurhash: string | null) => {
if (!blurhash) { if (!blurhash) {
return null; return null;
} }

View File

@ -105,7 +105,7 @@ class ReportReasonSelector extends PureComponent {
}; };
componentDidMount() { componentDidMount() {
api(false).get('/api/v1/instance').then(res => { api(false).get('/api/v2/instance').then(res => {
this.setState({ this.setState({
rules: res.data.rules, rules: res.data.rules,
}); });

View File

@ -26,11 +26,12 @@ import {
import { openModal, closeModal } from 'mastodon/actions/modal'; import { openModal, closeModal } from 'mastodon/actions/modal';
import { CircularProgress } from 'mastodon/components/circular_progress'; import { CircularProgress } from 'mastodon/components/circular_progress';
import { isUserTouching } from 'mastodon/is_mobile'; import { isUserTouching } from 'mastodon/is_mobile';
import type { import {
MenuItem, isMenuItem,
ActionMenuItem, isActionItem,
ExternalLinkMenuItem, isExternalLinkItem,
} from 'mastodon/models/dropdown_menu'; } from 'mastodon/models/dropdown_menu';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store';
import type { IconProp } from './icon'; import type { IconProp } from './icon';
@ -38,30 +39,6 @@ import { IconButton } from './icon_button';
let id = 0; let id = 0;
const isMenuItem = (item: unknown): item is MenuItem => {
if (item === null) {
return true;
}
return typeof item === 'object' && 'text' in item;
};
const isActionItem = (item: unknown): item is ActionMenuItem => {
if (!item || !isMenuItem(item)) {
return false;
}
return 'action' in item;
};
const isExternalLinkItem = (item: unknown): item is ExternalLinkMenuItem => {
if (!item || !isMenuItem(item)) {
return false;
}
return 'href' in item;
};
type RenderItemFn<Item = MenuItem> = ( type RenderItemFn<Item = MenuItem> = (
item: Item, item: Item,
index: number, index: number,
@ -320,6 +297,7 @@ interface DropdownProps<Item = MenuItem> {
scrollable?: boolean; scrollable?: boolean;
scrollKey?: string; scrollKey?: string;
status?: ImmutableMap<string, unknown>; status?: ImmutableMap<string, unknown>;
forceDropdown?: boolean;
renderItem?: RenderItemFn<Item>; renderItem?: RenderItemFn<Item>;
renderHeader?: RenderHeaderFn<Item>; renderHeader?: RenderHeaderFn<Item>;
onOpen?: () => void; onOpen?: () => void;
@ -339,6 +317,7 @@ export const Dropdown = <Item = MenuItem,>({
disabled, disabled,
scrollable, scrollable,
status, status,
forceDropdown = false,
renderItem, renderItem,
renderHeader, renderHeader,
onOpen, onOpen,
@ -354,6 +333,9 @@ export const Dropdown = <Item = MenuItem,>({
const open = currentId === openDropdownId; const open = currentId === openDropdownId;
const activeElement = useRef<HTMLElement | null>(null); const activeElement = useRef<HTMLElement | null>(null);
const targetRef = useRef<HTMLButtonElement | null>(null); const targetRef = useRef<HTMLButtonElement | null>(null);
const prefetchAccountId = status
? status.getIn(['account', 'id'])
: undefined;
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
if (activeElement.current) { if (activeElement.current) {
@ -402,16 +384,15 @@ export const Dropdown = <Item = MenuItem,>({
} else { } else {
onOpen?.(); onOpen?.();
if (status) { if (prefetchAccountId) {
dispatch(fetchRelationships([status.getIn(['account', 'id'])])); dispatch(fetchRelationships([prefetchAccountId]));
} }
if (isUserTouching()) { if (isUserTouching() && !forceDropdown) {
dispatch( dispatch(
openModal({ openModal({
modalType: 'ACTIONS', modalType: 'ACTIONS',
modalProps: { modalProps: {
status,
actions: items, actions: items,
onClick: handleItemClick, onClick: handleItemClick,
}, },
@ -431,12 +412,13 @@ export const Dropdown = <Item = MenuItem,>({
[ [
dispatch, dispatch,
currentId, currentId,
prefetchAccountId,
scrollKey, scrollKey,
onOpen, onOpen,
handleItemClick, handleItemClick,
open, open,
status,
items, items,
forceDropdown,
handleClose, handleClose,
], ],
); );

View File

@ -116,6 +116,7 @@ export const EditedTimestamp: React.FC<{
renderHeader={renderHeader} renderHeader={renderHeader}
onOpen={handleOpen} onOpen={handleOpen}
onItemClick={handleItemClick} onItemClick={handleItemClick}
forceDropdown
> >
<button className='dropdown-menu__text-button'> <button className='dropdown-menu__text-button'>
<FormattedMessage <FormattedMessage

View File

@ -1,24 +1,32 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { LoadingIndicator } from './loading_indicator';
interface Props { interface Props {
onClick: (event: React.MouseEvent) => void; onClick: (event: React.MouseEvent) => void;
disabled?: boolean; disabled?: boolean;
visible?: boolean; visible?: boolean;
loading?: boolean;
} }
export const LoadMore: React.FC<Props> = ({ export const LoadMore: React.FC<Props> = ({
onClick, onClick,
disabled, disabled,
visible = true, visible = true,
loading = false,
}) => { }) => {
return ( return (
<button <button
type='button' type='button'
className='load-more' className='load-more'
disabled={disabled || !visible} disabled={disabled || loading || !visible}
style={{ visibility: visible ? 'visible' : 'hidden' }} style={{ visibility: visible ? 'visible' : 'hidden' }}
onClick={onClick} onClick={onClick}
> >
<FormattedMessage id='status.load_more' defaultMessage='Load more' /> {loading ? (
<LoadingIndicator />
) : (
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
)}
</button> </button>
); );
}; };

View File

@ -1,37 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import CancelPresentationIcon from '@/material-icons/400-24px/cancel_presentation.svg?react';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import { Icon } from 'mastodon/components/icon';
class PictureInPicturePlaceholder extends PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
aspectRatio: PropTypes.string,
};
handleClick = () => {
const { dispatch } = this.props;
dispatch(removePictureInPicture());
};
render () {
const { aspectRatio } = this.props;
return (
<div className='picture-in-picture-placeholder' style={{ aspectRatio }} role='button' tabIndex={0} onClick={this.handleClick}>
<Icon id='window-restore' icon={CancelPresentationIcon} />
<FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
</div>
);
}
}
export default connect()(PictureInPicturePlaceholder);

View File

@ -0,0 +1,46 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import PipExitIcon from '@/material-icons/400-24px/pip_exit.svg?react';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import { Icon } from 'mastodon/components/icon';
import { useAppDispatch } from 'mastodon/store';
export const PictureInPicturePlaceholder: React.FC<{ aspectRatio: string }> = ({
aspectRatio,
}) => {
const dispatch = useAppDispatch();
const handleClick = useCallback(() => {
dispatch(removePictureInPicture());
}, [dispatch]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
handleClick();
}
},
[handleClick],
);
return (
<div /* eslint-disable-line jsx-a11y/click-events-have-key-events */
className='picture-in-picture-placeholder'
style={{ aspectRatio }}
role='button'
tabIndex={0}
onClick={handleClick}
onKeyDownCapture={handleKeyDown}
>
<Icon id='' icon={PipExitIcon} />
<FormattedMessage
id='picture_in_picture.restore'
defaultMessage='Put it back'
/>
</div>
);
};

View File

@ -17,7 +17,7 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import { ContentWarning } from 'mastodon/components/content_warning'; import { ContentWarning } from 'mastodon/components/content_warning';
import { FilterWarning } from 'mastodon/components/filter_warning'; import { FilterWarning } from 'mastodon/components/filter_warning';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; import { PictureInPicturePlaceholder } from 'mastodon/components/picture_in_picture_placeholder';
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'mastodon/utils/react_router'; import { withOptionalRouter, WithOptionalRouterPropTypes } from 'mastodon/utils/react_router';
import Card from '../features/status/components/card'; import Card from '../features/status/components/card';
@ -403,14 +403,7 @@ class Status extends ImmutablePureComponent {
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id'); const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
const matchedFilters = status.get('matched_filters'); const matchedFilters = status.get('matched_filters');
if (featured) { if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
prepend = (
<div className='status__prepend'>
<div className='status__prepend__icon'><Icon id='thumb-tack' icon={PushPinIcon} /></div>
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
</div>
);
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) }; const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
prepend = ( prepend = (
@ -491,9 +484,6 @@ class Status extends ImmutablePureComponent {
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])} foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])} accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)} duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
width={this.props.cachedMediaWidth}
height={110}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.get('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')}

View File

@ -8,7 +8,7 @@ import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import MediaGallery from 'mastodon/components/media_gallery'; import MediaGallery from 'mastodon/components/media_gallery';
import ModalRoot from 'mastodon/components/modal_root'; import ModalRoot from 'mastodon/components/modal_root';
import { Poll } from 'mastodon/components/poll'; import { Poll } from 'mastodon/components/poll';
import Audio from 'mastodon/features/audio'; import { Audio } from 'mastodon/features/audio';
import Card from 'mastodon/features/status/components/card'; import Card from 'mastodon/features/status/components/card';
import MediaModal from 'mastodon/features/ui/components/media_modal'; import MediaModal from 'mastodon/features/ui/components/media_modal';
import { Video } from 'mastodon/features/video'; import { Video } from 'mastodon/features/video';

View File

@ -7,19 +7,21 @@ import { useParams } from 'react-router';
import type { Map as ImmutableMap } from 'immutable'; import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
import { fetchFeaturedTags } from 'mastodon/actions/featured_tags'; import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
import { expandAccountFeaturedTimeline } from 'mastodon/actions/timelines'; import { expandAccountFeaturedTimeline } from 'mastodon/actions/timelines';
import { Account } from 'mastodon/components/account';
import { ColumnBackButton } from 'mastodon/components/column_back_button'; import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RemoteHint } from 'mastodon/components/remote_hint'; import { RemoteHint } from 'mastodon/components/remote_hint';
import StatusContainer from 'mastodon/containers/status_container'; import StatusContainer from 'mastodon/containers/status_container';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import Column from 'mastodon/features/ui/components/column';
import { useAccountId } from 'mastodon/hooks/useAccountId'; import { useAccountId } from 'mastodon/hooks/useAccountId';
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility'; import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { AccountHeader } from '../account_timeline/components/account_header';
import Column from '../ui/components/column';
import { EmptyMessage } from './components/empty_message'; import { EmptyMessage } from './components/empty_message';
import { FeaturedTag } from './components/featured_tag'; import { FeaturedTag } from './components/featured_tag';
import type { TagMap } from './components/featured_tag'; import type { TagMap } from './components/featured_tag';
@ -29,7 +31,9 @@ interface Params {
id?: string; id?: string;
} }
const AccountFeatured = () => { const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
multiColumn,
}) => {
const accountId = useAccountId(); const accountId = useAccountId();
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId); const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
const forceEmptyState = suspended || blockedBy || hidden; const forceEmptyState = suspended || blockedBy || hidden;
@ -40,7 +44,8 @@ const AccountFeatured = () => {
useEffect(() => { useEffect(() => {
if (accountId) { if (accountId) {
void dispatch(expandAccountFeaturedTimeline(accountId)); void dispatch(expandAccountFeaturedTimeline(accountId));
dispatch(fetchFeaturedTags(accountId)); void dispatch(fetchFeaturedTags({ accountId }));
void dispatch(fetchEndorsedAccounts({ accountId }));
} }
}, [accountId, dispatch]); }, [accountId, dispatch]);
@ -67,6 +72,17 @@ const AccountFeatured = () => {
ImmutableList(), ImmutableList(),
) as ImmutableList<string>, ) as ImmutableList<string>,
); );
const featuredAccountIds = useAppSelector(
(state) =>
state.user_lists.getIn(
['featured_accounts', accountId, 'items'],
ImmutableList(),
) as ImmutableList<string>,
);
if (accountId === null) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
if (isLoading) { if (isLoading) {
return ( return (
@ -78,7 +94,11 @@ const AccountFeatured = () => {
); );
} }
if (featuredStatusIds.isEmpty() && featuredTags.isEmpty()) { if (
featuredStatusIds.isEmpty() &&
featuredTags.isEmpty() &&
featuredAccountIds.isEmpty()
) {
return ( return (
<AccountFeaturedWrapper accountId={accountId}> <AccountFeaturedWrapper accountId={accountId}>
<EmptyMessage <EmptyMessage
@ -131,6 +151,19 @@ const AccountFeatured = () => {
))} ))}
</> </>
)} )}
{!featuredAccountIds.isEmpty() && (
<>
<h4 className='column-subheading'>
<FormattedMessage
id='account.featured.accounts'
defaultMessage='Profiles'
/>
</h4>
{featuredAccountIds.map((featuredAccountId) => (
<Account key={featuredAccountId} id={featuredAccountId} />
))}
</>
)}
<RemoteHint accountId={accountId} /> <RemoteHint accountId={accountId} />
</div> </div>
</Column> </Column>

View File

@ -147,7 +147,7 @@ export const AccountGallery: React.FC<{
[dispatch], [dispatch],
); );
if (accountId && !isAccount) { if (accountId === null) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />; return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
} }

View File

@ -107,7 +107,6 @@ const messages = defineMessages({
id: 'account.disable_notifications', id: 'account.disable_notifications',
defaultMessage: 'Stop notifying me when @{name} posts', defaultMessage: 'Stop notifying me when @{name} posts',
}, },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: { preferences: {
id: 'navigation_bar.preferences', id: 'navigation_bar.preferences',
defaultMessage: 'Preferences', defaultMessage: 'Preferences',
@ -451,7 +450,6 @@ export const AccountHeader: React.FC<{
text: intl.formatMessage(messages.preferences), text: intl.formatMessage(messages.preferences),
href: '/settings/preferences', href: '/settings/preferences',
}); });
arr.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
arr.push(null); arr.push(null);
arr.push({ arr.push({
text: intl.formatMessage(messages.follow_requests), text: intl.formatMessage(messages.follow_requests),

View File

@ -13,7 +13,6 @@ import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors/accounts'; import { getAccountHidden } from 'mastodon/selectors/accounts';
import { lookupAccount, fetchAccount } from '../../actions/accounts'; import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags';
import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines'; import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines';
import { ColumnBackButton } from '../../components/column_back_button'; import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
@ -27,7 +26,7 @@ import { LimitedAccountHint } from './components/limited_account_hint';
const emptyList = ImmutableList(); const emptyList = ImmutableList();
const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => { const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); const accountId = id || state.accounts_map[normalizeForLookup(acct)];
if (accountId === null) { if (accountId === null) {
return { return {
@ -86,7 +85,6 @@ class AccountTimeline extends ImmutablePureComponent {
dispatch(expandAccountFeaturedTimeline(accountId, { tagged })); dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
} }
dispatch(fetchFeaturedTags(accountId));
dispatch(expandAccountTimeline(accountId, { withReplies, tagged })); dispatch(expandAccountTimeline(accountId, { withReplies, tagged }));
if (accountId === me) { if (accountId === me) {

View File

@ -27,7 +27,7 @@ import { Button } from 'mastodon/components/button';
import { GIFV } from 'mastodon/components/gifv'; import { GIFV } from 'mastodon/components/gifv';
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { Skeleton } from 'mastodon/components/skeleton'; import { Skeleton } from 'mastodon/components/skeleton';
import Audio from 'mastodon/features/audio'; import { Audio } from 'mastodon/features/audio';
import { CharacterCounter } from 'mastodon/features/compose/components/character_counter'; import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import { Video, getPointerPosition } from 'mastodon/features/video'; import { Video, getPointerPosition } from 'mastodon/features/video';
@ -212,11 +212,11 @@ const Preview: React.FC<{
return ( return (
<Audio <Audio
src={media.get('url') as string} src={media.get('url') as string}
duration={media.getIn(['meta', 'original', 'duration'], 0) as number}
poster={ poster={
(media.get('preview_url') as string | undefined) ?? (media.get('preview_url') as string | undefined) ??
account?.avatar_static account?.avatar_static
} }
duration={media.getIn(['meta', 'original', 'duration'], 0) as number}
backgroundColor={ backgroundColor={
media.getIn(['meta', 'colors', 'background']) as string media.getIn(['meta', 'colors', 'background']) as string
} }

View File

@ -1,588 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { is } from 'immutable';
import { throttle, debounce } from 'lodash';
import DownloadIcon from '@/material-icons/400-24px/download.svg?react';
import PauseIcon from '@/material-icons/400-24px/pause.svg?react';
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react';
import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
import { Icon } from 'mastodon/components/icon';
import { SpoilerButton } from 'mastodon/components/spoiler_button';
import { formatTime, getPointerPosition, fileNameFromURL } from 'mastodon/features/video';
import { Blurhash } from '../../components/blurhash';
import { displayMedia, useBlurhash } from '../../initial_state';
import Visualizer from './visualizer';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
mute: { id: 'video.mute', defaultMessage: 'Mute' },
unmute: { id: 'video.unmute', defaultMessage: 'Unmute' },
download: { id: 'video.download', defaultMessage: 'Download file' },
hide: { id: 'audio.hide', defaultMessage: 'Hide audio' },
});
const TICK_SIZE = 10;
const PADDING = 180;
class Audio extends PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string,
lang: PropTypes.string,
poster: PropTypes.string,
duration: PropTypes.number,
width: PropTypes.number,
height: PropTypes.number,
sensitive: PropTypes.bool,
editable: PropTypes.bool,
fullscreen: PropTypes.bool,
intl: PropTypes.object.isRequired,
blurhash: PropTypes.string,
cacheWidth: PropTypes.func,
visible: PropTypes.bool,
onToggleVisibility: PropTypes.func,
backgroundColor: PropTypes.string,
foregroundColor: PropTypes.string,
accentColor: PropTypes.string,
currentTime: PropTypes.number,
autoPlay: PropTypes.bool,
volume: PropTypes.number,
muted: PropTypes.bool,
deployPictureInPicture: PropTypes.func,
matchedFilters: PropTypes.arrayOf(PropTypes.string),
};
state = {
width: this.props.width,
currentTime: 0,
buffer: 0,
duration: null,
paused: true,
muted: false,
volume: 1,
dragging: false,
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
};
constructor (props) {
super(props);
this.visualizer = new Visualizer(TICK_SIZE);
}
setPlayerRef = c => {
this.player = c;
if (this.player) {
this._setDimensions();
}
};
_pack() {
return {
src: this.props.src,
volume: this.state.volume,
muted: this.state.muted,
currentTime: this.audio.currentTime,
poster: this.props.poster,
backgroundColor: this.props.backgroundColor,
foregroundColor: this.props.foregroundColor,
accentColor: this.props.accentColor,
sensitive: this.props.sensitive,
visible: this.props.visible,
};
}
_setDimensions () {
const width = this.player.offsetWidth;
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
if (this.props.cacheWidth) {
this.props.cacheWidth(width);
}
this.setState({ width, height });
}
setSeekRef = c => {
this.seek = c;
};
setVolumeRef = c => {
this.volume = c;
};
setAudioRef = c => {
this.audio = c;
if (this.audio) {
this.audio.volume = 1;
this.audio.muted = false;
}
};
setCanvasRef = c => {
this.canvas = c;
this.visualizer.setCanvas(c);
};
componentDidMount () {
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize, { passive: true });
}
componentDidUpdate (prevProps, prevState) {
if (prevProps.src !== this.props.src || this.state.width !== prevState.width || this.state.height !== prevState.height || prevProps.accentColor !== this.props.accentColor) {
this._clear();
this._draw();
}
}
UNSAFE_componentWillReceiveProps (nextProps) {
if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
this.setState({ revealed: nextProps.visible });
}
}
componentWillUnmount () {
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
if (!this.state.paused && this.audio && this.props.deployPictureInPicture) {
this.props.deployPictureInPicture('audio', this._pack());
}
}
togglePlay = () => {
if (!this.audioContext) {
this._initAudioContext();
}
if (this.state.paused) {
this.setState({ paused: false }, () => this.audio.play());
} else {
this.setState({ paused: true }, () => this.audio.pause());
}
};
handleResize = debounce(() => {
if (this.player) {
this._setDimensions();
}
}, 250, {
trailing: true,
});
handlePlay = () => {
this.setState({ paused: false });
if (this.audioContext && this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
this._renderCanvas();
};
handlePause = () => {
this.setState({ paused: true });
if (this.audioContext) {
this.audioContext.suspend();
}
};
handleProgress = () => {
const lastTimeRange = this.audio.buffered.length - 1;
if (lastTimeRange > -1) {
this.setState({ buffer: Math.ceil(this.audio.buffered.end(lastTimeRange) / this.audio.duration * 100) });
}
};
toggleMute = () => {
const muted = !(this.state.muted || this.state.volume === 0);
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
if (this.gainNode) {
this.gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
}
});
};
toggleReveal = () => {
if (this.props.onToggleVisibility) {
this.props.onToggleVisibility();
} else {
this.setState({ revealed: !this.state.revealed });
}
};
handleVolumeMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseVolSlide, true);
document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
document.addEventListener('touchmove', this.handleMouseVolSlide, true);
document.addEventListener('touchend', this.handleVolumeMouseUp, true);
this.handleMouseVolSlide(e);
e.preventDefault();
e.stopPropagation();
};
handleVolumeMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
};
handleMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseMove, true);
document.addEventListener('mouseup', this.handleMouseUp, true);
document.addEventListener('touchmove', this.handleMouseMove, true);
document.addEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: true });
this.audio.pause();
this.handleMouseMove(e);
e.preventDefault();
e.stopPropagation();
};
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('mouseup', this.handleMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseMove, true);
document.removeEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: false });
this.audio.play();
};
handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e);
const currentTime = this.audio.duration * x;
if (!isNaN(currentTime)) {
this.setState({ currentTime }, () => {
this.audio.currentTime = currentTime;
});
}
}, 15);
handleTimeUpdate = () => {
this.setState({
currentTime: this.audio.currentTime,
duration: this.audio.duration,
});
};
handleMouseVolSlide = throttle(e => {
const { x } = getPointerPosition(this.volume, e);
if(!isNaN(x)) {
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
if (this.gainNode) {
this.gainNode.gain.value = this.state.muted ? 0 : x;
}
});
}
}, 15);
handleScroll = throttle(() => {
if (!this.canvas || !this.audio) {
return;
}
const { top, height } = this.canvas.getBoundingClientRect();
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
if (!this.state.paused && !inView) {
this.audio.pause();
if (this.props.deployPictureInPicture) {
this.props.deployPictureInPicture('audio', this._pack());
}
this.setState({ paused: true });
}
}, 150, { trailing: true });
handleMouseEnter = () => {
this.setState({ hovered: true });
};
handleMouseLeave = () => {
this.setState({ hovered: false });
};
handleLoadedData = () => {
const { autoPlay, currentTime } = this.props;
if (currentTime) {
this.audio.currentTime = currentTime;
}
if (autoPlay) {
this.togglePlay();
}
};
_initAudioContext () {
const AudioContext = window.AudioContext || window.webkitAudioContext;
const context = new AudioContext();
const source = context.createMediaElementSource(this.audio);
const gainNode = context.createGain();
gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
this.visualizer.setAudioContext(context, source);
source.connect(gainNode);
gainNode.connect(context.destination);
this.audioContext = context;
this.gainNode = gainNode;
}
handleDownload = () => {
fetch(this.props.src).then(res => res.blob()).then(blob => {
const element = document.createElement('a');
const objectURL = URL.createObjectURL(blob);
element.setAttribute('href', objectURL);
element.setAttribute('download', fileNameFromURL(this.props.src));
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
URL.revokeObjectURL(objectURL);
}).catch(err => {
console.error(err);
});
};
_renderCanvas () {
requestAnimationFrame(() => {
if (!this.audio) return;
this.handleTimeUpdate();
this._clear();
this._draw();
if (!this.state.paused) {
this._renderCanvas();
}
});
}
_clear() {
this.visualizer.clear(this.state.width, this.state.height);
}
_draw() {
this.visualizer.draw(this._getCX(), this._getCY(), this._getAccentColor(), this._getRadius(), this._getScaleCoefficient());
}
_getRadius () {
return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient());
}
_getScaleCoefficient () {
return (this.state.height || this.props.height) / 982;
}
_getCX() {
return Math.floor(this.state.width / 2);
}
_getCY() {
return Math.floor((this.state.height || this.props.height) / 2);
}
_getAccentColor () {
return this.props.accentColor || '#ffffff';
}
_getBackgroundColor () {
return this.props.backgroundColor || '#000000';
}
_getForegroundColor () {
return this.props.foregroundColor || '#ffffff';
}
seekBy (time) {
const currentTime = this.audio.currentTime + time;
if (!isNaN(currentTime)) {
this.setState({ currentTime }, () => {
this.audio.currentTime = currentTime;
});
}
}
handleAudioKeyDown = e => {
// On the audio element or the seek bar, we can safely use the space bar
// for playback control because there are no buttons to press
if (e.key === ' ') {
e.preventDefault();
e.stopPropagation();
this.togglePlay();
}
};
handleKeyDown = e => {
switch(e.key) {
case 'k':
e.preventDefault();
e.stopPropagation();
this.togglePlay();
break;
case 'm':
e.preventDefault();
e.stopPropagation();
this.toggleMute();
break;
case 'j':
e.preventDefault();
e.stopPropagation();
this.seekBy(-10);
break;
case 'l':
e.preventDefault();
e.stopPropagation();
this.seekBy(10);
break;
}
};
render () {
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash, matchedFilters } = this.props;
const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
const progress = Math.min((currentTime / duration) * 100, 100);
const muted = this.state.muted || volume === 0;
return (
<div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
<Blurhash
hash={blurhash}
className={classNames('media-gallery__preview', {
'media-gallery__preview--hidden': revealed,
})}
dummy={!useBlurhash}
/>
{(revealed || editable) && <audio
src={src}
ref={this.setAudioRef}
preload={autoPlay ? 'auto' : 'none'}
onPlay={this.handlePlay}
onPause={this.handlePause}
onProgress={this.handleProgress}
onLoadedData={this.handleLoadedData}
crossOrigin='anonymous'
/>}
<canvas
role='button'
tabIndex={0}
className='audio-player__canvas'
width={this.state.width}
height={this.state.height}
style={{ width: '100%', position: 'absolute', top: 0, left: 0 }}
ref={this.setCanvasRef}
onClick={this.togglePlay}
onKeyDown={this.handleAudioKeyDown}
title={alt}
aria-label={alt}
lang={lang}
/>
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} />
{(revealed || editable) && <img
src={this.props.poster}
alt=''
style={{
position: 'absolute',
left: '50%',
top: '50%',
height: `calc(${(100 - 2 * 100 * PADDING / 982)}% - ${TICK_SIZE * 2}px)`,
aspectRatio: '1',
transform: 'translate(-50%, -50%)',
borderRadius: '50%',
pointerEvents: 'none',
}}
/>}
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
<div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} />
<div className='video-player__seek__progress' style={{ width: `${progress}%`, backgroundColor: this._getAccentColor() }} />
<span
className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex={0}
style={{ left: `${progress}%`, backgroundColor: this._getAccentColor() }}
onKeyDown={this.handleAudioKeyDown}
/>
</div>
<div className='video-player__controls active'>
<div className='video-player__buttons-bar'>
<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}><Icon id={paused ? 'play' : 'pause'} icon={paused ? PlayArrowIcon : PauseIcon} /></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'} icon={muted ? VolumeOffIcon : VolumeUpIcon} /></button>
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
<div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }} />
<span
className='video-player__volume__handle'
tabIndex={0}
style={{ left: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }}
/>
</div>
<span className='video-player__time'>
<span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
<span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(Math.floor(this.state.duration || this.props.duration))}</span>
</span>
</div>
<div className='video-player__buttons right'>
{!editable && (
<>
<button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' icon={VisibilityOffIcon} /></button>
<a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download>
<Icon id='download' icon={DownloadIcon} />
</a>
</>
)}
</div>
</div>
</div>
</div>
);
}
}
export default injectIntl(Audio);

View File

@ -0,0 +1,840 @@
import { useEffect, useRef, useCallback, useState, useId } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useSpring, animated, config } from '@react-spring/web';
import DownloadIcon from '@/material-icons/400-24px/download.svg?react';
import Forward5Icon from '@/material-icons/400-24px/forward_5-fill.svg?react';
import PauseIcon from '@/material-icons/400-24px/pause-fill.svg?react';
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
import Replay5Icon from '@/material-icons/400-24px/replay_5-fill.svg?react';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react';
import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import { SpoilerButton } from 'mastodon/components/spoiler_button';
import { formatTime, getPointerPosition } from 'mastodon/features/video';
import { useAudioVisualizer } from 'mastodon/hooks/useAudioVisualizer';
import {
displayMedia,
useBlurhash,
reduceMotion,
} from 'mastodon/initial_state';
import { playerSettings } from 'mastodon/settings';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
mute: { id: 'video.mute', defaultMessage: 'Mute' },
unmute: { id: 'video.unmute', defaultMessage: 'Unmute' },
download: { id: 'video.download', defaultMessage: 'Download file' },
hide: { id: 'audio.hide', defaultMessage: 'Hide audio' },
skipForward: { id: 'video.skip_forward', defaultMessage: 'Skip forward' },
skipBackward: { id: 'video.skip_backward', defaultMessage: 'Skip backward' },
});
const persistVolume = (volume: number, muted: boolean) => {
playerSettings.set('volume', volume);
playerSettings.set('muted', muted);
};
const restoreVolume = (audio: HTMLAudioElement) => {
const volume = (playerSettings.get('volume') as number | undefined) ?? 0.5;
const muted = (playerSettings.get('muted') as boolean | undefined) ?? false;
audio.volume = volume;
audio.muted = muted;
};
const HOVER_FADE_DELAY = 4000;
export const Audio: React.FC<{
src: string;
alt?: string;
lang?: string;
poster?: string;
sensitive?: boolean;
editable?: boolean;
blurhash?: string;
visible?: boolean;
duration?: number;
onToggleVisibility?: () => void;
backgroundColor?: string;
foregroundColor?: string;
accentColor?: string;
startTime?: number;
startPlaying?: boolean;
startVolume?: number;
startMuted?: boolean;
deployPictureInPicture?: (
type: string,
mediaProps: {
src: string;
muted: boolean;
volume: number;
currentTime: number;
poster?: string;
backgroundColor: string;
foregroundColor: string;
accentColor: string;
},
) => void;
matchedFilters?: string[];
}> = ({
src,
alt,
lang,
poster,
duration,
sensitive,
editable,
blurhash,
visible,
onToggleVisibility,
backgroundColor = '#000000',
foregroundColor = '#ffffff',
accentColor = '#ffffff',
startTime,
startPlaying,
startVolume,
startMuted,
deployPictureInPicture,
matchedFilters,
}) => {
const intl = useIntl();
const [currentTime, setCurrentTime] = useState(0);
const [loadedDuration, setDuration] = useState(duration ?? 0);
const [paused, setPaused] = useState(true);
const [muted, setMuted] = useState(false);
const [volume, setVolume] = useState(0.5);
const [hovered, setHovered] = useState(false);
const [dragging, setDragging] = useState(false);
const [revealed, setRevealed] = useState(false);
const playerRef = useRef<HTMLDivElement>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const seekRef = useRef<HTMLDivElement>(null);
const volumeRef = useRef<HTMLDivElement>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
const [resumeAudio, suspendAudio, frequencyBands] = useAudioVisualizer(
audioRef,
3,
);
const accessibilityId = useId();
const [style, spring] = useSpring(() => ({
progress: '0%',
buffer: '0%',
volume: '0%',
}));
const handleAudioRef = useCallback(
(c: HTMLVideoElement | null) => {
if (audioRef.current && !audioRef.current.paused && c === null) {
deployPictureInPicture?.('audio', {
src,
poster,
backgroundColor,
foregroundColor,
accentColor,
currentTime: audioRef.current.currentTime,
muted: audioRef.current.muted,
volume: audioRef.current.volume,
});
}
audioRef.current = c;
if (audioRef.current) {
restoreVolume(audioRef.current);
setVolume(audioRef.current.volume);
setMuted(audioRef.current.muted);
void spring.start({
volume: `${audioRef.current.volume * 100}%`,
immediate: reduceMotion,
});
}
},
[
spring,
setVolume,
setMuted,
src,
poster,
backgroundColor,
accentColor,
foregroundColor,
deployPictureInPicture,
],
);
useEffect(() => {
if (!audioRef.current) {
return;
}
audioRef.current.volume = volume;
audioRef.current.muted = muted;
}, [volume, muted]);
useEffect(() => {
if (typeof visible !== 'undefined') {
setRevealed(visible);
} else {
setRevealed(
displayMedia === 'show_all' ||
(displayMedia !== 'hide_all' && !sensitive),
);
}
}, [visible, sensitive]);
useEffect(() => {
if (!revealed && audioRef.current) {
audioRef.current.pause();
suspendAudio();
}
}, [suspendAudio, revealed]);
useEffect(() => {
let nextFrame: ReturnType<typeof requestAnimationFrame>;
const updateProgress = () => {
nextFrame = requestAnimationFrame(() => {
if (audioRef.current && audioRef.current.duration > 0) {
void spring.start({
progress: `${(audioRef.current.currentTime / audioRef.current.duration) * 100}%`,
immediate: reduceMotion,
config: config.stiff,
});
}
updateProgress();
});
};
updateProgress();
return () => {
cancelAnimationFrame(nextFrame);
};
}, [spring]);
const togglePlay = useCallback(() => {
if (!audioRef.current) {
return;
}
if (audioRef.current.paused) {
resumeAudio();
void audioRef.current.play();
} else {
audioRef.current.pause();
suspendAudio();
}
}, [resumeAudio, suspendAudio]);
const handlePlay = useCallback(() => {
setPaused(false);
}, []);
const handlePause = useCallback(() => {
setPaused(true);
}, []);
const handleProgress = useCallback(() => {
if (!audioRef.current) {
return;
}
const lastTimeRange = audioRef.current.buffered.length - 1;
if (lastTimeRange > -1) {
void spring.start({
buffer: `${Math.ceil(audioRef.current.buffered.end(lastTimeRange) / audioRef.current.duration) * 100}%`,
immediate: reduceMotion,
});
}
}, [spring]);
const handleVolumeChange = useCallback(() => {
if (!audioRef.current) {
return;
}
setVolume(audioRef.current.volume);
setMuted(audioRef.current.muted);
void spring.start({
volume: `${audioRef.current.muted ? 0 : audioRef.current.volume * 100}%`,
immediate: reduceMotion,
});
persistVolume(audioRef.current.volume, audioRef.current.muted);
}, [spring, setVolume, setMuted]);
const handleTimeUpdate = useCallback(() => {
if (!audioRef.current) {
return;
}
setCurrentTime(audioRef.current.currentTime);
}, [setCurrentTime]);
const toggleMute = useCallback(() => {
if (!audioRef.current) {
return;
}
const effectivelyMuted =
audioRef.current.muted || audioRef.current.volume === 0;
if (effectivelyMuted) {
audioRef.current.muted = false;
if (audioRef.current.volume === 0) {
audioRef.current.volume = 0.05;
}
} else {
audioRef.current.muted = true;
}
}, []);
const toggleReveal = useCallback(() => {
if (onToggleVisibility) {
onToggleVisibility();
} else {
setRevealed((value) => !value);
}
}, [onToggleVisibility, setRevealed]);
const handleVolumeMouseDown = useCallback(
(e: React.MouseEvent) => {
const handleVolumeMouseUp = () => {
document.removeEventListener('mousemove', handleVolumeMouseMove, true);
document.removeEventListener('mouseup', handleVolumeMouseUp, true);
};
const handleVolumeMouseMove = (e: MouseEvent) => {
if (!volumeRef.current || !audioRef.current) {
return;
}
const { x } = getPointerPosition(volumeRef.current, e);
if (!isNaN(x)) {
audioRef.current.volume = x;
audioRef.current.muted = x > 0 ? false : true;
void spring.start({ volume: `${x * 100}%`, immediate: true });
}
};
document.addEventListener('mousemove', handleVolumeMouseMove, true);
document.addEventListener('mouseup', handleVolumeMouseUp, true);
handleVolumeMouseMove(e.nativeEvent);
e.preventDefault();
e.stopPropagation();
},
[spring],
);
const handleSeekMouseDown = useCallback(
(e: React.MouseEvent) => {
const handleSeekMouseUp = () => {
document.removeEventListener('mousemove', handleSeekMouseMove, true);
document.removeEventListener('mouseup', handleSeekMouseUp, true);
setDragging(false);
resumeAudio();
void audioRef.current?.play();
};
const handleSeekMouseMove = (e: MouseEvent) => {
if (!seekRef.current || !audioRef.current) {
return;
}
const { x } = getPointerPosition(seekRef.current, e);
const newTime = audioRef.current.duration * x;
if (!isNaN(newTime)) {
audioRef.current.currentTime = newTime;
void spring.start({ progress: `${x * 100}%`, immediate: true });
}
};
document.addEventListener('mousemove', handleSeekMouseMove, true);
document.addEventListener('mouseup', handleSeekMouseUp, true);
setDragging(true);
audioRef.current?.pause();
handleSeekMouseMove(e.nativeEvent);
e.preventDefault();
e.stopPropagation();
},
[setDragging, spring, resumeAudio],
);
const handleMouseEnter = useCallback(() => {
setHovered(true);
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
hoverTimeoutRef.current = setTimeout(() => {
setHovered(false);
}, HOVER_FADE_DELAY);
}, [setHovered]);
const handleMouseMove = useCallback(() => {
setHovered(true);
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
hoverTimeoutRef.current = setTimeout(() => {
setHovered(false);
}, HOVER_FADE_DELAY);
}, [setHovered]);
const handleMouseLeave = useCallback(() => {
setHovered(false);
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
}, [setHovered]);
const handleTouchEnd = useCallback(() => {
setHovered(true);
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
hoverTimeoutRef.current = setTimeout(() => {
setHovered(false);
}, HOVER_FADE_DELAY);
}, [setHovered]);
const handleLoadedData = useCallback(() => {
if (!audioRef.current) {
return;
}
setDuration(audioRef.current.duration);
if (typeof startTime !== 'undefined') {
audioRef.current.currentTime = startTime;
}
if (typeof startVolume !== 'undefined') {
audioRef.current.volume = startVolume;
}
if (typeof startMuted !== 'undefined') {
audioRef.current.muted = startMuted;
}
if (startPlaying) {
void audioRef.current.play();
}
}, [setDuration, startTime, startVolume, startMuted, startPlaying]);
const seekBy = (time: number) => {
if (!audioRef.current) {
return;
}
const newTime = audioRef.current.currentTime + time;
if (!isNaN(newTime)) {
audioRef.current.currentTime = newTime;
}
};
const handleAudioKeyDown = useCallback(
(e: React.KeyboardEvent) => {
// On the audio element or the seek bar, we can safely use the space bar
// for playback control because there are no buttons to press
if (e.key === ' ') {
e.preventDefault();
e.stopPropagation();
togglePlay();
}
},
[togglePlay],
);
const handleSkipBackward = useCallback(() => {
seekBy(-5);
}, []);
const handleSkipForward = useCallback(() => {
seekBy(5);
}, []);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
const updateVolumeBy = (step: number) => {
if (!audioRef.current) {
return;
}
const newVolume = audioRef.current.volume + step;
if (!isNaN(newVolume)) {
audioRef.current.volume = newVolume;
audioRef.current.muted = newVolume > 0 ? false : true;
}
};
switch (e.key) {
case 'k':
case ' ':
e.preventDefault();
e.stopPropagation();
togglePlay();
break;
case 'm':
e.preventDefault();
e.stopPropagation();
toggleMute();
break;
case 'j':
case 'ArrowLeft':
e.preventDefault();
e.stopPropagation();
seekBy(-5);
break;
case 'l':
case 'ArrowRight':
e.preventDefault();
e.stopPropagation();
seekBy(5);
break;
case 'ArrowUp':
e.preventDefault();
e.stopPropagation();
updateVolumeBy(0.15);
break;
case 'ArrowDown':
e.preventDefault();
e.stopPropagation();
updateVolumeBy(-0.15);
break;
}
},
[togglePlay, toggleMute],
);
const springForBand0 = useSpring({
to: { r: 50 + (frequencyBands[0] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand1 = useSpring({
to: { r: 50 + (frequencyBands[1] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand2 = useSpring({
to: { r: 50 + (frequencyBands[2] ?? 0) * 10 },
config: config.wobbly,
});
const progress = Math.min((currentTime / loadedDuration) * 100, 100);
const effectivelyMuted = muted || volume === 0;
return (
<div
className={classNames('audio-player', { inactive: !revealed })}
ref={playerRef}
style={
{
'--player-background-color': backgroundColor,
'--player-foreground-color': foregroundColor,
'--player-accent-color': accentColor,
} as React.CSSProperties
}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onTouchEnd={handleTouchEnd}
role='button'
tabIndex={0}
onKeyDownCapture={handleKeyDown}
aria-label={alt}
lang={lang}
>
{blurhash && (
<Blurhash
hash={blurhash}
className={classNames('media-gallery__preview', {
'media-gallery__preview--hidden': revealed,
})}
dummy={!useBlurhash}
/>
)}
<audio /* eslint-disable-line jsx-a11y/media-has-caption */
src={src}
ref={handleAudioRef}
preload={startPlaying ? 'auto' : 'none'}
onPlay={handlePlay}
onPause={handlePause}
onProgress={handleProgress}
onLoadedData={handleLoadedData}
onTimeUpdate={handleTimeUpdate}
onVolumeChange={handleVolumeChange}
crossOrigin='anonymous'
/>
<div
className='video-player__seek'
aria-valuemin={0}
aria-valuenow={progress}
aria-valuemax={100}
onMouseDown={handleSeekMouseDown}
onKeyDownCapture={handleAudioKeyDown}
ref={seekRef}
role='slider'
tabIndex={0}
>
<animated.div
className='video-player__seek__buffer'
style={{ width: style.buffer }}
/>
<animated.div
className='video-player__seek__progress'
style={{ width: style.progress }}
/>
<animated.span
className={classNames('video-player__seek__handle', {
active: dragging,
})}
style={{ left: style.progress }}
/>
</div>
<div className='audio-player__controls'>
<div className='audio-player__controls__play'>
<button
type='button'
title={intl.formatMessage(messages.skipBackward)}
aria-label={intl.formatMessage(messages.skipBackward)}
className='player-button'
onClick={handleSkipBackward}
>
<Icon id='' icon={Replay5Icon} />
</button>
</div>
<div className='audio-player__controls__play'>
<svg
className='audio-player__visualizer'
viewBox='0 0 124 124'
xmlns='http://www.w3.org/2000/svg'
>
<animated.circle
opacity={0.5}
cx={57}
cy={62.5}
r={springForBand0.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={65}
cy={57.5}
r={springForBand1.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={63}
cy={66.5}
r={springForBand2.r}
fill='var(--player-accent-color)'
/>
<g clipPath={`url(#${accessibilityId}-clip)`}>
<rect
x={14}
y={14}
width={96}
height={96}
fill={`url(#${accessibilityId}-pattern)`}
/>
<rect
x={14}
y={14}
width={96}
height={96}
fill='var(--player-background-color'
opacity={0.45}
/>
</g>
<defs>
<pattern
id={`${accessibilityId}-pattern`}
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use href={`#${accessibilityId}-image`} />
</pattern>
<clipPath id={`${accessibilityId}-clip`}>
<rect
x={14}
y={14}
width={96}
height={96}
rx={48}
fill='white'
/>
</clipPath>
<image
id={`${accessibilityId}-image`}
href={poster}
width={1}
height={1}
preserveAspectRatio='none'
/>
</defs>
</svg>
<button
type='button'
title={intl.formatMessage(paused ? messages.play : messages.pause)}
aria-label={intl.formatMessage(
paused ? messages.play : messages.pause,
)}
className='player-button'
onClick={togglePlay}
>
<Icon
id={paused ? 'play' : 'pause'}
icon={paused ? PlayArrowIcon : PauseIcon}
/>
</button>
</div>
<div className='audio-player__controls__play'>
<button
type='button'
title={intl.formatMessage(messages.skipForward)}
aria-label={intl.formatMessage(messages.skipForward)}
className='player-button'
onClick={handleSkipForward}
>
<Icon id='' icon={Forward5Icon} />
</button>
</div>
</div>
<SpoilerButton
hidden={revealed || editable}
sensitive={sensitive ?? false}
onClick={toggleReveal}
matchedFilters={matchedFilters}
/>
<div
className={classNames('video-player__controls', { active: hovered })}
>
<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button
type='button'
title={intl.formatMessage(
muted ? messages.unmute : messages.mute,
)}
aria-label={intl.formatMessage(
muted ? messages.unmute : messages.mute,
)}
className='player-button'
onClick={toggleMute}
>
<Icon
id={muted ? 'volume-off' : 'volume-up'}
icon={muted ? VolumeOffIcon : VolumeUpIcon}
/>
</button>
<div
className='video-player__volume active'
ref={volumeRef}
onMouseDown={handleVolumeMouseDown}
role='slider'
aria-valuemin={0}
aria-valuenow={effectivelyMuted ? 0 : volume * 100}
aria-valuemax={100}
tabIndex={0}
>
<animated.div
className='video-player__volume__current'
style={{ width: style.volume }}
/>
<animated.span
className={classNames('video-player__volume__handle')}
style={{ left: style.volume }}
/>
</div>
<span className='video-player__time'>
<span className='video-player__time-current'>
{formatTime(Math.floor(currentTime))}
</span>
<span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>
{formatTime(Math.floor(loadedDuration))}
</span>
</span>
</div>
<div className='video-player__buttons right'>
{!editable && (
<>
<button
type='button'
className='player-button'
onClick={toggleReveal}
>
<FormattedMessage
id='media_gallery.hide'
defaultMessage='Hide'
/>
</button>
<a
title={intl.formatMessage(messages.download)}
aria-label={intl.formatMessage(messages.download)}
className='video-player__download__icon player-button'
href={src}
download
>
<Icon id='download' icon={DownloadIcon} />
</a>
</>
)}
</div>
</div>
</div>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default Audio;

View File

@ -1,136 +0,0 @@
/*
Copyright (c) 2020 by Alex Permyakov (https://codepen.io/alexdevp/pen/RNELPV)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const hex2rgba = (hex, alpha = 1) => {
const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
export default class Visualizer {
constructor (tickSize) {
this.tickSize = tickSize;
}
setCanvas(canvas) {
this.canvas = canvas;
if (canvas) {
this.context = canvas.getContext('2d');
}
}
setAudioContext(context, source) {
const analyser = context.createAnalyser();
analyser.smoothingTimeConstant = 0.6;
analyser.fftSize = 2048;
source.connect(analyser);
this.analyser = analyser;
}
getTickPoints (count) {
const coords = [];
for(let i = 0; i < count; i++) {
const rad = Math.PI * 2 * i / count;
coords.push({ x: Math.cos(rad), y: -Math.sin(rad) });
}
return coords;
}
drawTick (cx, cy, mainColor, x1, y1, x2, y2) {
const dx1 = Math.ceil(cx + x1);
const dy1 = Math.ceil(cy + y1);
const dx2 = Math.ceil(cx + x2);
const dy2 = Math.ceil(cy + y2);
const gradient = this.context.createLinearGradient(dx1, dy1, dx2, dy2);
const lastColor = hex2rgba(mainColor, 0);
gradient.addColorStop(0, mainColor);
gradient.addColorStop(0.6, mainColor);
gradient.addColorStop(1, lastColor);
this.context.beginPath();
this.context.strokeStyle = gradient;
this.context.lineWidth = 2;
this.context.moveTo(dx1, dy1);
this.context.lineTo(dx2, dy2);
this.context.stroke();
}
getTicks (count, size, radius, scaleCoefficient) {
const ticks = this.getTickPoints(count);
const lesser = 200;
const m = [];
const bufferLength = this.analyser ? this.analyser.frequencyBinCount : 0;
const frequencyData = new Uint8Array(bufferLength);
const allScales = [];
if (this.analyser) {
this.analyser.getByteFrequencyData(frequencyData);
}
ticks.forEach((tick, i) => {
const coef = 1 - i / (ticks.length * 2.5);
let delta = ((frequencyData[i] || 0) - lesser * coef) * scaleCoefficient;
if (delta < 0) {
delta = 0;
}
const k = radius / (radius - (size + delta));
const x1 = tick.x * (radius - size);
const y1 = tick.y * (radius - size);
const x2 = x1 * k;
const y2 = y1 * k;
m.push({ x1, y1, x2, y2 });
if (i < 20) {
let scale = delta / (200 * scaleCoefficient);
scale = scale < 1 ? 1 : scale;
allScales.push(scale);
}
});
const scale = allScales.reduce((pv, cv) => pv + cv, 0) / allScales.length;
return m.map(({ x1, y1, x2, y2 }) => ({
x1: x1,
y1: y1,
x2: x2 * scale,
y2: y2 * scale,
}));
}
clear (width, height) {
this.context.clearRect(0, 0, width, height);
}
draw (cx, cy, color, radius, coefficient) {
this.context.save();
const ticks = this.getTicks(parseInt(360 * coefficient), this.tickSize, radius, coefficient);
ticks.forEach(tick => {
this.drawTick(cx, cy, color, tick.x1, tick.y1, tick.x2, tick.y2);
});
this.context.restore();
}
}

View File

@ -9,7 +9,6 @@ import { useAppDispatch } from 'mastodon/store';
const messages = defineMessages({ const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: { preferences: {
id: 'navigation_bar.preferences', id: 'navigation_bar.preferences',
defaultMessage: 'Preferences', defaultMessage: 'Preferences',
@ -53,7 +52,6 @@ export const ActionBar: React.FC = () => {
text: intl.formatMessage(messages.preferences), text: intl.formatMessage(messages.preferences),
href: '/settings/preferences', href: '/settings/preferences',
}, },
{ text: intl.formatMessage(messages.pins), to: '/pinned' },
null, null,
{ {
text: intl.formatMessage(messages.follow_requests), text: intl.formatMessage(messages.follow_requests),

View File

@ -40,7 +40,7 @@ let EmojiPicker, Emoji; // load asynchronously
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
const backgroundImageFn = () => `${assetHost}/emoji/sheet_15.png`; const backgroundImageFn = () => `${assetHost}/emoji/sheet_15_1.png`;
const notFoundFn = () => ( const notFoundFn = () => (
<div className='emoji-mart-no-results'> <div className='emoji-mart-no-results'>

View File

@ -30,9 +30,6 @@ const messages = defineMessages({
class PrivacyDropdown extends PureComponent { class PrivacyDropdown extends PureComponent {
static propTypes = { static propTypes = {
isUserTouching: PropTypes.func,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
noDirect: PropTypes.bool, noDirect: PropTypes.bool,

View File

@ -15,16 +15,6 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeComposeVisibility(value)); dispatch(changeComposeVisibility(value));
}, },
isUserTouching,
onModalOpen: props => dispatch(openModal({
modalType: 'ACTIONS',
modalProps: props,
})),
onModalClose: () => dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
})),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown); export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);

View File

@ -130,6 +130,7 @@ export const Directory: React.FC<{
}, [dispatch, order, local]); }, [dispatch, order, local]);
const pinned = !!columnId; const pinned = !!columnId;
const initialLoad = isLoading && accountIds.size === 0;
const scrollableArea = ( const scrollableArea = (
<div className='scrollable'> <div className='scrollable'>
@ -170,7 +171,7 @@ export const Directory: React.FC<{
</div> </div>
<div className='directory__list'> <div className='directory__list'>
{isLoading ? ( {initialLoad ? (
<LoadingIndicator /> <LoadingIndicator />
) : ( ) : (
accountIds.map((accountId) => ( accountIds.map((accountId) => (
@ -179,7 +180,11 @@ export const Directory: React.FC<{
)} )}
</div> </div>
<LoadMore onClick={handleLoadMore} visible={!isLoading} /> <LoadMore
onClick={handleLoadMore}
visible={!initialLoad}
loading={isLoading}
/>
</div> </div>
); );

View File

@ -7,94 +7,21 @@
// This version comment should be bumped each time the emoji data is changed // This version comment should be bumped each time the emoji data is changed
// to ensure that the prevaled file is regenerated by Babel // to ensure that the prevaled file is regenerated by Babel
// version: 3 // version: 4
// This json file contains the names of the categories. const { NimbleEmojiIndex } = require('emoji-mart');
const emojiMart5LocalesData = require('@emoji-mart/data/i18n/en.json');
const emojiMart5Data = require('@emoji-mart/data/sets/15/all.json');
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data'); const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
const _ = require('lodash');
let data = require('./emoji_data.json');
const emojiMap = require('./emoji_map.json'); const emojiMap = require('./emoji_map.json');
// This json file is downloaded from https://github.com/iamcal/emoji-data/
// and is used to correct the sheet coordinates since we're using that repo's sheet
const emojiSheetData = require('./emoji_sheet.json');
const { unicodeToFilename } = require('./unicode_to_filename'); const { unicodeToFilename } = require('./unicode_to_filename');
const { unicodeToUnifiedName } = require('./unicode_to_unified_name'); const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
// Grabbed from `emoji_utils` to avoid circular dependency emojiMartUncompress(data);
function unifiedToNative(unified) {
let unicodes = unified.split('-'),
codePoints = unicodes.map((u) => `0x${u}`);
return String.fromCodePoint(...codePoints);
}
let data = {
compressed: true,
categories: emojiMart5Data.categories.map(cat => {
return {
...cat,
name: emojiMart5LocalesData.categories[cat.id]
};
}),
aliases: emojiMart5Data.aliases,
emojis: _(emojiMart5Data.emojis).values().map(emoji => {
let skin_variations = {};
const unified = emoji.skins[0].unified.toUpperCase();
const emojiFromRawData = emojiSheetData.find(e => e.unified === unified);
if (!emojiFromRawData) {
return undefined;
}
if (emoji.skins.length > 1) {
const [, ...nonDefaultSkins] = emoji.skins;
nonDefaultSkins.forEach(skin => {
const [matchingRawCodePoints,matchingRawEmoji] = Object.entries(emojiFromRawData.skin_variations).find((pair) => {
const [, value] = pair;
return value.unified.toLowerCase() === skin.unified;
});
if (matchingRawEmoji && matchingRawCodePoints) {
// At the time of writing, the json from `@emoji-mart/data` doesn't have data
// for emoji like `woman-heart-woman` with two different skin tones.
const skinToneCode = matchingRawCodePoints.split('-')[0];
skin_variations[skinToneCode] = {
unified: matchingRawEmoji.unified.toUpperCase(),
non_qualified: null,
sheet_x: matchingRawEmoji.sheet_x,
sheet_y: matchingRawEmoji.sheet_y,
has_img_twitter: true,
native: unifiedToNative(matchingRawEmoji.unified.toUpperCase())
};
}
});
}
return {
a: emoji.name,
b: unified,
c: undefined,
f: true,
j: [emoji.id, ...emoji.keywords],
k: [emojiFromRawData.sheet_x, emojiFromRawData.sheet_y],
m: emoji.emoticons?.[0],
l: emoji.emoticons,
o: emoji.version,
id: emoji.id,
skin_variations,
native: unifiedToNative(unified.toUpperCase())
};
}).compact().keyBy(e => e.id).mapValues(e => _.omit(e, 'id')).value()
};
if (data.compressed) {
emojiMartUncompress(data);
}
const emojiMartData = data; const emojiMartData = data;
const emojiIndex = new NimbleEmojiIndex(emojiMartData);
const excluded = ['®', '©', '™']; const excluded = ['®', '©', '™'];
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿']; const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
@ -103,10 +30,15 @@ const shortcodeMap = {};
const shortCodesToEmojiData = {}; const shortCodesToEmojiData = {};
const emojisWithoutShortCodes = []; const emojisWithoutShortCodes = [];
Object.keys(emojiMart5Data.emojis).forEach(key => { Object.keys(emojiIndex.emojis).forEach(key => {
let emoji = emojiMart5Data.emojis[key]; let emoji = emojiIndex.emojis[key];
shortcodeMap[emoji.skins[0].native] = emoji.id; // Emojis with skin tone modifiers are stored like this
if (Object.hasOwn(emoji, '1')) {
emoji = emoji['1'];
}
shortcodeMap[emoji.native] = emoji.id;
}); });
const stripModifiers = unicode => { const stripModifiers = unicode => {
@ -150,9 +82,13 @@ Object.keys(emojiMap).forEach(key => {
} }
}); });
Object.keys(emojiMartData.emojis).forEach(key => { Object.keys(emojiIndex.emojis).forEach(key => {
let emoji = emojiMartData.emojis[key]; let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this
if (Object.hasOwn(emoji, '1')) {
emoji = emoji['1'];
}
const { native } = emoji; const { native } = emoji;
let { short_names, search, unified } = emojiMartData.emojis[key]; let { short_names, search, unified } = emojiMartData.emojis[key];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -29,7 +29,7 @@ import { LimitedAccountHint } from '../account_timeline/components/limited_accou
import Column from '../ui/components/column'; import Column from '../ui/components/column';
const mapStateToProps = (state, { params: { acct, id } }) => { const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); const accountId = id || state.accounts_map[normalizeForLookup(acct)];
if (!accountId) { if (!accountId) {
return { return {

View File

@ -29,7 +29,7 @@ import { LimitedAccountHint } from '../account_timeline/components/limited_accou
import Column from '../ui/components/column'; import Column from '../ui/components/column';
const mapStateToProps = (state, { params: { acct, id } }) => { const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); const accountId = id || state.accounts_map[normalizeForLookup(acct)];
if (!accountId) { if (!accountId) {
return { return {

View File

@ -9,6 +9,8 @@ import {
fetchHashtag, fetchHashtag,
followHashtag, followHashtag,
unfollowHashtag, unfollowHashtag,
featureHashtag,
unfeatureHashtag,
} from 'mastodon/actions/tags_typed'; } from 'mastodon/actions/tags_typed';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
import { Button } from 'mastodon/components/button'; import { Button } from 'mastodon/components/button';
@ -28,6 +30,11 @@ const messages = defineMessages({
id: 'hashtag.admin_moderation', id: 'hashtag.admin_moderation',
defaultMessage: 'Open moderation interface for #{name}', defaultMessage: 'Open moderation interface for #{name}',
}, },
feature: { id: 'hashtag.feature', defaultMessage: 'Feature on profile' },
unfeature: {
id: 'hashtag.unfeature',
defaultMessage: "Don't feature on profile",
},
}); });
const usesRenderer = (displayNumber: React.ReactNode, pluralReady: number) => ( const usesRenderer = (displayNumber: React.ReactNode, pluralReady: number) => (
@ -88,22 +95,51 @@ export const HashtagHeader: React.FC<{
}, [dispatch, tagId, setTag]); }, [dispatch, tagId, setTag]);
const menu = useMemo(() => { const menu = useMemo(() => {
const tmp = []; const arr = [];
if ( if (tag && signedIn) {
tag && const handleFeature = () => {
signedIn && if (tag.featuring) {
(permissions & PERMISSION_MANAGE_TAXONOMIES) === void dispatch(unfeatureHashtag({ tagId })).then((result) => {
PERMISSION_MANAGE_TAXONOMIES if (isFulfilled(result)) {
) { setTag(result.payload);
tmp.push({ }
text: intl.formatMessage(messages.adminModeration, { name: tag.id }),
href: `/admin/tags/${tag.id}`, return '';
});
} else {
void dispatch(featureHashtag({ tagId })).then((result) => {
if (isFulfilled(result)) {
setTag(result.payload);
}
return '';
});
}
};
arr.push({
text: intl.formatMessage(
tag.featuring ? messages.unfeature : messages.feature,
),
action: handleFeature,
}); });
arr.push(null);
if (
(permissions & PERMISSION_MANAGE_TAXONOMIES) ===
PERMISSION_MANAGE_TAXONOMIES
) {
arr.push({
text: intl.formatMessage(messages.adminModeration, { name: tagId }),
href: `/admin/tags/${tag.id}`,
});
}
} }
return tmp; return arr;
}, [signedIn, permissions, intl, tag]); }, [setTag, dispatch, tagId, signedIn, permissions, intl, tag]);
const handleFollow = useCallback(() => { const handleFollow = useCallback(() => {
if (!signedIn || !tag) { if (!signedIn || !tag) {

View File

@ -13,7 +13,6 @@ import { Avatar } from 'mastodon/components/avatar';
import { ContentWarning } from 'mastodon/components/content_warning'; import { ContentWarning } from 'mastodon/components/content_warning';
import { DisplayName } from 'mastodon/components/display_name'; import { DisplayName } from 'mastodon/components/display_name';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import type { Status } from 'mastodon/models/status';
import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { EmbeddedStatusContent } from './embedded_status_content'; import { EmbeddedStatusContent } from './embedded_status_content';
@ -27,9 +26,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
const clickCoordinatesRef = useRef<[number, number] | null>(); const clickCoordinatesRef = useRef<[number, number] | null>();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const status = useAppSelector( const status = useAppSelector((state) => state.statuses.get(statusId));
(state) => state.statuses.get(statusId) as Status | undefined,
);
const account = useAppSelector((state) => const account = useAppSelector((state) =>
state.accounts.get(status?.get('account') as string), state.accounts.get(status?.get('account') as string),

View File

@ -6,7 +6,6 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
import type { NotificationGroupMention } from 'mastodon/models/notification_group'; import type { NotificationGroupMention } from 'mastodon/models/notification_group';
import type { Status } from 'mastodon/models/status';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
@ -39,9 +38,9 @@ export const NotificationMention: React.FC<{
unread: boolean; unread: boolean;
}> = ({ notification, unread }) => { }> = ({ notification, unread }) => {
const [isDirect, isReply] = useAppSelector((state) => { const [isDirect, isReply] = useAppSelector((state) => {
const status = state.statuses.get(notification.statusId) as const status = notification.statusId
| Status ? state.statuses.get(notification.statusId)
| undefined; : undefined;
if (!status) return [false, false] as const; if (!status) return [false, false] as const;

View File

@ -1,195 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import { replyCompose } from 'mastodon/actions/compose';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal';
import { IconButton } from 'mastodon/components/icon_button';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { me } from 'mastodon/initial_state';
import { makeGetStatus } from 'mastodon/selectors';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
const messages = defineMessages({
reply: { id: 'status.reply', defaultMessage: 'Reply' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
removeFavourite: { id: 'status.remove_favourite', defaultMessage: 'Remove from favorites' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = (state, { statusId }) => ({
status: getStatus(state, { id: statusId }),
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
});
return mapStateToProps;
};
class Footer extends ImmutablePureComponent {
static propTypes = {
identity: identityContextPropShape,
statusId: PropTypes.string.isRequired,
status: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
askReplyConfirmation: PropTypes.bool,
withOpenButton: PropTypes.bool,
onClose: PropTypes.func,
...WithRouterPropTypes,
};
_performReply = () => {
const { dispatch, status, onClose } = this.props;
if (onClose) {
onClose(true);
}
dispatch(replyCompose(status));
};
handleReplyClick = () => {
const { dispatch, askReplyConfirmation, status, onClose } = this.props;
const { signedIn } = this.props.identity;
if (signedIn) {
if (askReplyConfirmation) {
onClose(true);
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
} else {
this._performReply();
}
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reply',
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}));
}
};
handleFavouriteClick = () => {
const { dispatch, status } = this.props;
const { signedIn } = this.props.identity;
if (signedIn) {
dispatch(toggleFavourite(status.get('id')));
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'favourite',
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}));
}
};
handleReblogClick = e => {
const { dispatch, status } = this.props;
const { signedIn } = this.props.identity;
if (signedIn) {
dispatch(toggleReblog(status.get('id'), e && e.shiftKey));
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}));
}
};
handleOpenClick = e => {
if (e.button !== 0 || !history) {
return;
}
const { status, onClose } = this.props;
if (onClose) {
onClose();
}
this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
};
render () {
const { status, intl, withOpenButton } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let replyIcon, replyIconComponent, replyTitle;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyIconComponent = ReplyIcon;
replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyIconComponent = ReplyAllIcon;
replyTitle = intl.formatMessage(messages.replyAll);
}
let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
}
const favouriteTitle = intl.formatMessage(status.get('favourited') ? messages.removeFavourite : messages.favourite);
return (
<div className='picture-in-picture__footer'>
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={favouriteTitle} icon='star' iconComponent={StarIcon} onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' iconComponent={OpenInNewIcon} onClick={this.handleOpenClick} href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} />}
</div>
);
}
}
export default connect(makeMapStateToProps)(withIdentity(withRouter(injectIntl(Footer))));

View File

@ -0,0 +1,255 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
import { replyCompose } from 'mastodon/actions/compose';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal';
import { IconButton } from 'mastodon/components/icon_button';
import { useIdentity } from 'mastodon/identity_context';
import { me } from 'mastodon/initial_state';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
reply: { id: 'status.reply', defaultMessage: 'Reply' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: {
id: 'status.reblog_private',
defaultMessage: 'Boost with original visibility',
},
cancel_reblog_private: {
id: 'status.cancel_reblog_private',
defaultMessage: 'Unboost',
},
cannot_reblog: {
id: 'status.cannot_reblog',
defaultMessage: 'This post cannot be boosted',
},
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
removeFavourite: {
id: 'status.remove_favourite',
defaultMessage: 'Remove from favorites',
},
open: { id: 'status.open', defaultMessage: 'Expand this status' },
});
export const Footer: React.FC<{
statusId: string;
withOpenButton?: boolean;
onClose: (arg0?: boolean) => void;
}> = ({ statusId, withOpenButton, onClose }) => {
const { signedIn } = useIdentity();
const intl = useIntl();
const history = useHistory();
const dispatch = useAppDispatch();
const status = useAppSelector((state) => state.statuses.get(statusId));
const accountId = status?.get('account') as string | undefined;
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const askReplyConfirmation = useAppSelector(
(state) => (state.compose.get('text') as string).trim().length !== 0,
);
const handleReplyClick = useCallback(() => {
if (!status) {
return;
}
if (signedIn) {
onClose(true);
if (askReplyConfirmation) {
dispatch(
openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }),
);
} else {
dispatch(replyCompose(status));
}
} else {
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reply',
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}),
);
}
}, [dispatch, status, signedIn, askReplyConfirmation, onClose]);
const handleFavouriteClick = useCallback(() => {
if (!status) {
return;
}
if (signedIn) {
dispatch(toggleFavourite(status.get('id')));
} else {
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'favourite',
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}),
);
}
}, [dispatch, status, signedIn]);
const handleReblogClick = useCallback(
(e: React.MouseEvent) => {
if (!status) {
return;
}
if (signedIn) {
dispatch(toggleReblog(status.get('id'), e.shiftKey));
} else {
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}),
);
}
},
[dispatch, status, signedIn],
);
const handleOpenClick = useCallback(
(e: React.MouseEvent) => {
if (e.button !== 0 || !status) {
return;
}
onClose();
history.push(`/@${account?.acct}/${status.get('id') as string}`);
},
[history, status, account, onClose],
);
if (!status) {
return null;
}
const publicStatus = ['public', 'unlisted'].includes(
status.get('visibility') as string,
);
const reblogPrivate =
status.getIn(['account', 'id']) === me &&
status.get('visibility') === 'private';
let replyIcon, replyIconComponent, replyTitle;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyIconComponent = ReplyIcon;
replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyIconComponent = ReplyAllIcon;
replyTitle = intl.formatMessage(messages.replyAll);
}
let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus
? RepeatActiveIcon
: RepeatPrivateActiveIcon;
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
}
const favouriteTitle = intl.formatMessage(
status.get('favourited') ? messages.removeFavourite : messages.favourite,
);
return (
<div className='picture-in-picture__footer'>
<IconButton
className='status__action-bar-button'
title={replyTitle}
icon={
status.get('in_reply_to_account_id') ===
status.getIn(['account', 'id'])
? 'reply'
: replyIcon
}
iconComponent={
status.get('in_reply_to_account_id') ===
status.getIn(['account', 'id'])
? ReplyIcon
: replyIconComponent
}
onClick={handleReplyClick}
counter={status.get('replies_count') as number}
/>
<IconButton
className={classNames('status__action-bar-button', { reblogPrivate })}
disabled={!publicStatus && !reblogPrivate}
active={status.get('reblogged') as boolean}
title={reblogTitle}
icon='retweet'
iconComponent={reblogIconComponent}
onClick={handleReblogClick}
counter={status.get('reblogs_count') as number}
/>
<IconButton
className='status__action-bar-button star-icon'
animate
active={status.get('favourited') as boolean}
title={favouriteTitle}
icon='star'
iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon}
onClick={handleFavouriteClick}
counter={status.get('favourites_count') as number}
/>
{withOpenButton && (
<IconButton
className='status__action-bar-button'
title={intl.formatMessage(messages.open)}
icon='external-link'
iconComponent={OpenInNewIcon}
onClick={handleOpenClick}
href={`/@${account?.acct}/${status.get('id') as string}`}
/>
)}
</div>
);
};

View File

@ -1,11 +1,11 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture'; import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import Audio from 'mastodon/features/audio'; import { Audio } from 'mastodon/features/audio';
import { Video } from 'mastodon/features/video'; import { Video } from 'mastodon/features/video';
import { useAppDispatch, useAppSelector } from 'mastodon/store/typed_functions'; import { useAppDispatch, useAppSelector } from 'mastodon/store/typed_functions';
import Footer from './components/footer'; import { Footer } from './components/footer';
import { Header } from './components/header'; import { Header } from './components/header';
export const PictureInPicture: React.FC = () => { export const PictureInPicture: React.FC = () => {
@ -58,14 +58,14 @@ export const PictureInPicture: React.FC = () => {
player = ( player = (
<Audio <Audio
src={src} src={src}
currentTime={currentTime} startTime={currentTime}
volume={volume} startVolume={volume}
muted={muted} startMuted={muted}
startPlaying
poster={poster} poster={poster}
backgroundColor={backgroundColor} backgroundColor={backgroundColor}
foregroundColor={foregroundColor} foregroundColor={foregroundColor}
accentColor={accentColor} accentColor={accentColor}
autoPlay
/> />
); );
} }
@ -76,7 +76,7 @@ export const PictureInPicture: React.FC = () => {
{player} {player}
<Footer statusId={statusId} /> <Footer statusId={statusId} onClose={handleClose} />
</div> </div>
); );
}; };

View File

@ -13,7 +13,9 @@ import { Link } from 'react-router-dom';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import { AnimatedNumber } from 'mastodon/components/animated_number'; import { AnimatedNumber } from 'mastodon/components/animated_number';
import { Avatar } from 'mastodon/components/avatar';
import { ContentWarning } from 'mastodon/components/content_warning'; import { ContentWarning } from 'mastodon/components/content_warning';
import { DisplayName } from 'mastodon/components/display_name';
import { EditedTimestamp } from 'mastodon/components/edited_timestamp'; import { EditedTimestamp } from 'mastodon/components/edited_timestamp';
import { FilterWarning } from 'mastodon/components/filter_warning'; import { FilterWarning } from 'mastodon/components/filter_warning';
import { FormattedDateWrapper } from 'mastodon/components/formatted_date'; import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
@ -21,17 +23,14 @@ import type { StatusLike } from 'mastodon/components/hashtag_bar';
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar'; import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { IconLogo } from 'mastodon/components/logo'; import { IconLogo } from 'mastodon/components/logo';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; import MediaGallery from 'mastodon/components/media_gallery';
import { PictureInPicturePlaceholder } from 'mastodon/components/picture_in_picture_placeholder';
import StatusContent from 'mastodon/components/status_content';
import { VisibilityIcon } from 'mastodon/components/visibility_icon'; import { VisibilityIcon } from 'mastodon/components/visibility_icon';
import { Audio } from 'mastodon/features/audio';
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
import { Video } from 'mastodon/features/video'; import { Video } from 'mastodon/features/video';
import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name';
import MediaGallery from '../../../components/media_gallery';
import StatusContent from '../../../components/status_content';
import Audio from '../../audio';
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
import Card from './card'; import Card from './card';
interface VideoModalOptions { interface VideoModalOptions {
@ -189,18 +188,17 @@ export const DetailedStatus: React.FC<{
src={attachment.get('url')} src={attachment.get('url')}
alt={description} alt={description}
lang={language} lang={language}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
poster={ poster={
attachment.get('preview_url') || attachment.get('preview_url') ||
status.getIn(['account', 'avatar_static']) status.getIn(['account', 'avatar_static'])
} }
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])} backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])} foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])} accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
visible={showMedia} visible={showMedia}
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
height={150}
onToggleVisibility={onToggleMediaVisibility} onToggleVisibility={onToggleMediaVisibility}
matchedFilters={status.get('matched_media_filters')} matchedFilters={status.get('matched_media_filters')}
/> />

View File

@ -65,6 +65,7 @@ import { textForScreenReader, defaultMediaVisibility } from '../../components/st
import StatusContainer from '../../containers/status_container'; import StatusContainer from '../../containers/status_container';
import { deleteModal } from '../../initial_state'; import { deleteModal } from '../../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import { getAncestorsIds, getDescendantsIds } from 'mastodon/selectors/contexts';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
@ -83,69 +84,15 @@ const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture(); const getPictureInPicture = makeGetPictureInPicture();
const getAncestorsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = ImmutableList();
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id && !mutable.includes(id)) {
mutable.unshift(id);
id = inReplyTos.get(id);
}
});
return ancestorsIds;
});
const getDescendantsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'replies']),
state => state.get('statuses'),
], (statusId, contextReplies, statuses) => {
let descendantsIds = [];
const ids = [statusId];
while (ids.length > 0) {
let id = ids.pop();
const replies = contextReplies.get(id);
if (statusId !== id) {
descendantsIds.push(id);
}
if (replies) {
replies.reverse().forEach(reply => {
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
});
}
}
let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
if (insertAt !== -1) {
descendantsIds.forEach((id, idx) => {
if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) {
descendantsIds.splice(idx, 1);
descendantsIds.splice(insertAt, 0, id);
insertAt += 1;
}
});
}
return ImmutableList(descendantsIds);
});
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' }); const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' });
let ancestorsIds = ImmutableList(); let ancestorsIds = [];
let descendantsIds = ImmutableList(); let descendantsIds = [];
if (status) { if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); ancestorsIds = getAncestorsIds(state, status.get('in_reply_to_id'));
descendantsIds = getDescendantsIds(state, { id: status.get('id') }); descendantsIds = getDescendantsIds(state, status.get('id'));
} }
return { return {
@ -188,8 +135,8 @@ class Status extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
ancestorsIds: ImmutablePropTypes.list.isRequired, ancestorsIds: PropTypes.arrayOf(PropTypes.string).isRequired,
descendantsIds: ImmutablePropTypes.list.isRequired, descendantsIds: PropTypes.arrayOf(PropTypes.string).isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool, askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
@ -383,7 +330,7 @@ class Status extends ImmutablePureComponent {
handleToggleAll = () => { handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS()); const statusIds = [status.get('id')].concat(ancestorsIds, descendantsIds);
if (status.get('hidden')) { if (status.get('hidden')) {
this.props.dispatch(revealStatus(statusIds)); this.props.dispatch(revealStatus(statusIds));
@ -482,13 +429,13 @@ class Status extends ImmutablePureComponent {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) { if (id === status.get('id')) {
this._selectChild(ancestorsIds.size - 1, true); this._selectChild(ancestorsIds.length - 1, true);
} else { } else {
let index = ancestorsIds.indexOf(id); let index = ancestorsIds.indexOf(id);
if (index === -1) { if (index === -1) {
index = descendantsIds.indexOf(id); index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index, true); this._selectChild(ancestorsIds.length + index, true);
} else { } else {
this._selectChild(index - 1, true); this._selectChild(index - 1, true);
} }
@ -499,13 +446,13 @@ class Status extends ImmutablePureComponent {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) { if (id === status.get('id')) {
this._selectChild(ancestorsIds.size + 1, false); this._selectChild(ancestorsIds.length + 1, false);
} else { } else {
let index = ancestorsIds.indexOf(id); let index = ancestorsIds.indexOf(id);
if (index === -1) { if (index === -1) {
index = descendantsIds.indexOf(id); index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index + 2, false); this._selectChild(ancestorsIds.length + index + 2, false);
} else { } else {
this._selectChild(index + 1, false); this._selectChild(index + 1, false);
} }
@ -536,8 +483,8 @@ class Status extends ImmutablePureComponent {
onMoveUp={this.handleMoveUp} onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown} onMoveDown={this.handleMoveDown}
contextType='thread' contextType='thread'
previousId={i > 0 ? list.get(i - 1) : undefined} previousId={i > 0 ? list[i - 1] : undefined}
nextId={list.get(i + 1) || (ancestors && statusId)} nextId={list[i + 1] || (ancestors && statusId)}
rootId={statusId} rootId={statusId}
/> />
)); ));
@ -574,7 +521,7 @@ class Status extends ImmutablePureComponent {
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
const { status, ancestorsIds } = this.props; const { status, ancestorsIds } = this.props;
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) { if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || prevProps.status?.get('id') !== status.get('id'))) {
this._scrollStatusIntoView(); this._scrollStatusIntoView();
} }
} }
@ -621,11 +568,11 @@ class Status extends ImmutablePureComponent {
); );
} }
if (ancestorsIds && ancestorsIds.size > 0) { if (ancestorsIds && ancestorsIds.length > 0) {
ancestors = <>{this.renderChildren(ancestorsIds, true)}</>; ancestors = <>{this.renderChildren(ancestorsIds, true)}</>;
} }
if (descendantsIds && descendantsIds.size > 0) { if (descendantsIds && descendantsIds.length > 0) {
descendants = <>{this.renderChildren(descendantsIds)}</>; descendants = <>{this.renderChildren(descendantsIds)}</>;
} }

View File

@ -1,48 +0,0 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { IconButton } from '../../../components/icon_button';
export default class ActionsModal extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
actions: PropTypes.array,
onClick: PropTypes.func,
};
renderAction = (action, i) => {
if (action === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { icon = null, iconComponent = null, text, meta = null, active = false, href = '#' } = action;
return (
<li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
{icon && <IconButton title={text} icon={icon} iconComponent={iconComponent} role='presentation' tabIndex={-1} inverted />}
<div>
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
<div>{meta}</div>
</div>
</a>
</li>
);
};
render () {
return (
<div className='modal-root__modal actions-modal'>
<ul className={classNames({ 'with-status': !!status })}>
{this.props.actions.map(this.renderAction)}
</ul>
</div>
);
}
}

View File

@ -0,0 +1,65 @@
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
import {
isActionItem,
isExternalLinkItem,
} from 'mastodon/models/dropdown_menu';
export const ActionsModal: React.FC<{
actions: MenuItem[];
onClick: React.MouseEventHandler;
}> = ({ actions, onClick }) => (
<div className='modal-root__modal actions-modal'>
<ul>
{actions.map((option, i: number) => {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, dangerous } = option;
let element: React.ReactElement;
if (isActionItem(option)) {
element = (
<button onClick={onClick} data-index={i}>
{text}
</button>
);
} else if (isExternalLinkItem(option)) {
element = (
<a
href={option.href}
target={option.target ?? '_target'}
data-method={option.method}
rel='noopener'
onClick={onClick}
data-index={i}
>
{text}
</a>
);
} else {
element = (
<Link to={option.to} onClick={onClick} data-index={i}>
{text}
</Link>
);
}
return (
<li
className={classNames({
'dropdown-menu__item--dangerous': dangerous,
})}
key={`${text}-${i}`}
>
{element}
</li>
);
})}
</ul>
</div>
);

View File

@ -1,74 +0,0 @@
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { getAverageFromBlurhash } from 'mastodon/blurhash';
import Audio from 'mastodon/features/audio';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
const mapStateToProps = (state, { statusId }) => ({
status: state.getIn(['statuses', statusId]),
accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
});
class AudioModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
statusId: PropTypes.string.isRequired,
status: ImmutablePropTypes.map.isRequired,
accountStaticAvatar: PropTypes.string.isRequired,
options: PropTypes.shape({
autoPlay: PropTypes.bool,
}),
onClose: PropTypes.func.isRequired,
onChangeBackgroundColor: PropTypes.func.isRequired,
};
componentDidMount () {
const { media, onChangeBackgroundColor } = this.props;
const backgroundColor = getAverageFromBlurhash(media.get('blurhash'));
onChangeBackgroundColor(backgroundColor || { r: 255, g: 255, b: 255 });
}
componentWillUnmount () {
this.props.onChangeBackgroundColor(null);
}
render () {
const { media, status, accountStaticAvatar, onClose } = this.props;
const options = this.props.options || {};
const language = status.getIn(['translation', 'language']) || status.get('language');
const description = media.getIn(['translation', 'description']) || media.get('description');
return (
<div className='modal-root__modal audio-modal'>
<div className='audio-modal__container'>
<Audio
src={media.get('url')}
alt={description}
lang={language}
duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150}
poster={media.get('preview_url') || accountStaticAvatar}
backgroundColor={media.getIn(['meta', 'colors', 'background'])}
foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
accentColor={media.getIn(['meta', 'colors', 'accent'])}
autoPlay={options.autoPlay}
/>
</div>
<div className='media-modal__overlay'>
{status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
</div>
</div>
);
}
}
export default connect(mapStateToProps, null, null, { forwardRef: true })(AudioModal);

View File

@ -0,0 +1,78 @@
import { useEffect } from 'react';
import { getAverageFromBlurhash } from 'mastodon/blurhash';
import type { RGB } from 'mastodon/blurhash';
import { Audio } from 'mastodon/features/audio';
import { Footer } from 'mastodon/features/picture_in_picture/components/footer';
import type { MediaAttachment } from 'mastodon/models/media_attachment';
import { useAppSelector } from 'mastodon/store';
const AudioModal: React.FC<{
media: MediaAttachment;
statusId: string;
options: {
autoPlay: boolean;
};
onClose: () => void;
onChangeBackgroundColor: (color: RGB | null) => void;
}> = ({ media, statusId, options, onClose, onChangeBackgroundColor }) => {
const status = useAppSelector((state) => state.statuses.get(statusId));
const accountId = status?.get('account') as string | undefined;
const accountStaticAvatar = useAppSelector((state) =>
accountId ? state.accounts.get(accountId)?.avatar_static : undefined,
);
useEffect(() => {
const backgroundColor = getAverageFromBlurhash(
media.get('blurhash') as string | null,
);
onChangeBackgroundColor(backgroundColor ?? { r: 255, g: 255, b: 255 });
return () => {
onChangeBackgroundColor(null);
};
}, [media, onChangeBackgroundColor]);
const language = (status?.getIn(['translation', 'language']) ??
status?.get('language')) as string;
const description = (media.getIn(['translation', 'description']) ??
media.get('description')) as string;
return (
<div className='modal-root__modal audio-modal'>
<div className='audio-modal__container'>
<Audio
src={media.get('url') as string}
alt={description}
lang={language}
poster={
(media.get('preview_url') as string | null) ?? accountStaticAvatar
}
duration={media.getIn(['meta', 'original', 'duration'], 0) as number}
backgroundColor={
media.getIn(['meta', 'colors', 'background']) as string
}
foregroundColor={
media.getIn(['meta', 'colors', 'foreground']) as string
}
accentColor={media.getIn(['meta', 'colors', 'accent']) as string}
startPlaying={options.autoPlay}
/>
</div>
<div className='media-modal__overlay'>
{status && (
<Footer
statusId={status.get('id') as string}
withOpenButton
onClose={onClose}
/>
)}
</div>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default AudioModal;

View File

@ -18,7 +18,7 @@ import { getAverageFromBlurhash } from 'mastodon/blurhash';
import { GIFV } from 'mastodon/components/gifv'; import { GIFV } from 'mastodon/components/gifv';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import Footer from 'mastodon/features/picture_in_picture/components/footer'; import { Footer } from 'mastodon/features/picture_in_picture/components/footer';
import { Video } from 'mastodon/features/video'; import { Video } from 'mastodon/features/video';
import { disableSwiping } from 'mastodon/initial_state'; import { disableSwiping } from 'mastodon/initial_state';

View File

@ -24,7 +24,7 @@ import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
import BundleContainer from '../containers/bundle_container'; import BundleContainer from '../containers/bundle_container';
import ActionsModal from './actions_modal'; import { ActionsModal } from './actions_modal';
import AudioModal from './audio_modal'; import AudioModal from './audio_modal';
import { BoostModal } from './boost_modal'; import { BoostModal } from './boost_modal';
import { import {

View File

@ -5,7 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getAverageFromBlurhash } from 'mastodon/blurhash'; import { getAverageFromBlurhash } from 'mastodon/blurhash';
import Footer from 'mastodon/features/picture_in_picture/components/footer'; import { Footer } from 'mastodon/features/picture_in_picture/components/footer';
import { Video } from 'mastodon/features/video'; import { Video } from 'mastodon/features/video';
const mapStateToProps = (state, { statusId }) => ({ const mapStateToProps = (state, { statusId }) => ({

View File

@ -806,7 +806,7 @@ export const Video: React.FC<{
// The outer wrapper is necessary to avoid reflowing the layout when going into full screen // The outer wrapper is necessary to avoid reflowing the layout when going into full screen
return ( return (
<div> <div>
<div <div /* eslint-disable-line jsx-a11y/click-events-have-key-events */
role='menuitem' role='menuitem'
className={classNames('video-player', { className={classNames('video-player', {
inactive: !revealed, inactive: !revealed,
@ -820,7 +820,7 @@ export const Video: React.FC<{
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
onClick={handleClickRoot} onClick={handleClickRoot}
onKeyDown={handleKeyDown} onKeyDownCapture={handleKeyDown}
tabIndex={0} tabIndex={0}
> >
{blurhash && ( {blurhash && (
@ -845,7 +845,7 @@ export const Video: React.FC<{
title={alt} title={alt}
lang={lang} lang={lang}
onClick={handleClick} onClick={handleClick}
onKeyDown={handleVideoKeyDown} onKeyDownCapture={handleVideoKeyDown}
onPlay={handlePlay} onPlay={handlePlay}
onPause={handlePause} onPause={handlePause}
onLoadedData={handleLoadedData} onLoadedData={handleLoadedData}

View File

@ -11,27 +11,25 @@ interface Params {
id?: string; id?: string;
} }
export function useAccountId() { export const useAccountId = () => {
const { acct, id } = useParams<Params>(); const { acct, id } = useParams<Params>();
const dispatch = useAppDispatch();
const accountId = useAppSelector( const accountId = useAppSelector(
(state) => (state) =>
id ?? id ?? (acct ? state.accounts_map[normalizeForLookup(acct)] : undefined),
(state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
); );
const account = useAppSelector((state) => const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined, accountId ? state.accounts.get(accountId) : undefined,
); );
const isAccount = !!account; const accountInStore = !!account;
const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
if (!accountId) { if (typeof accountId === 'undefined' && acct) {
dispatch(lookupAccount(acct)); dispatch(lookupAccount(acct));
} else if (!isAccount) { } else if (accountId && !accountInStore) {
dispatch(fetchAccount(accountId)); dispatch(fetchAccount(accountId));
} }
}, [dispatch, accountId, acct, isAccount]); }, [dispatch, accountId, acct, accountInStore]);
return accountId; return accountId;
} };

View File

@ -1,12 +1,14 @@
import { getAccountHidden } from 'mastodon/selectors/accounts'; import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
export function useAccountVisibility(accountId?: string) { export function useAccountVisibility(accountId?: string | null) {
const blockedBy = useAppSelector( const blockedBy = useAppSelector((state) =>
(state) => !!state.relationships.getIn([accountId, 'blocked_by'], false), accountId
? !!state.relationships.getIn([accountId, 'blocked_by'], false)
: false,
); );
const suspended = useAppSelector( const suspended = useAppSelector((state) =>
(state) => !!state.accounts.getIn([accountId, 'suspended'], false), accountId ? !!state.accounts.getIn([accountId, 'suspended'], false) : false,
); );
const hidden = useAppSelector((state) => const hidden = useAppSelector((state) =>
accountId ? Boolean(getAccountHidden(state, accountId)) : false, accountId ? Boolean(getAccountHidden(state, accountId)) : false,

View File

@ -0,0 +1,112 @@
import { useState, useEffect, useRef, useCallback } from 'react';
const normalizeFrequencies = (arr: Float32Array): number[] => {
return new Array(...arr).map((value: number) => {
if (value === -Infinity) {
return 0;
}
return Math.sqrt(1 - (Math.max(-100, Math.min(-10, value)) * -1) / 100);
});
};
export const useAudioVisualizer = (
ref: React.MutableRefObject<HTMLAudioElement | null>,
numBands: number,
) => {
const audioContextRef = useRef<AudioContext>();
const sourceRef = useRef<MediaElementAudioSourceNode>();
const analyzerRef = useRef<AnalyserNode>();
const [frequencyBands, setFrequencyBands] = useState<number[]>(
new Array(numBands).fill(0),
);
useEffect(() => {
if (!audioContextRef.current) {
audioContextRef.current = new AudioContext();
analyzerRef.current = audioContextRef.current.createAnalyser();
analyzerRef.current.smoothingTimeConstant = 0.6;
analyzerRef.current.fftSize = 2048;
}
return () => {
if (audioContextRef.current) {
void audioContextRef.current.close();
}
};
}, []);
useEffect(() => {
if (
audioContextRef.current &&
analyzerRef.current &&
!sourceRef.current &&
ref.current
) {
sourceRef.current = audioContextRef.current.createMediaElementSource(
ref.current,
);
sourceRef.current.connect(analyzerRef.current);
sourceRef.current.connect(audioContextRef.current.destination);
}
return () => {
if (sourceRef.current) {
sourceRef.current.disconnect();
}
};
}, [ref]);
useEffect(() => {
const source = sourceRef.current;
const analyzer = analyzerRef.current;
const context = audioContextRef.current;
if (!source || !analyzer || !context) {
return;
}
const bufferLength = analyzer.frequencyBinCount;
const frequencyData = new Float32Array(bufferLength);
const updateProgress = () => {
analyzer.getFloatFrequencyData(frequencyData);
const normalizedFrequencies = normalizeFrequencies(
frequencyData.slice(100, 600),
);
const bands: number[] = [];
const chunkSize = Math.ceil(normalizedFrequencies.length / numBands);
for (let i = 0; i < numBands; i++) {
const sum = normalizedFrequencies
.slice(i * chunkSize, (i + 1) * chunkSize)
.reduce((sum, cur) => sum + cur, 0);
bands.push(sum / chunkSize);
}
setFrequencyBands(bands);
};
const updateInterval = setInterval(updateProgress, 15);
return () => {
clearInterval(updateInterval);
};
}, [numBands]);
const resume = useCallback(() => {
if (audioContextRef.current) {
void audioContextRef.current.resume();
}
}, []);
const suspend = useCallback(() => {
if (audioContextRef.current) {
void audioContextRef.current.suspend();
}
}, []);
return [resume, suspend, frequencyBands] as const;
};

View File

@ -86,7 +86,6 @@
"column.lists": "Lyste", "column.lists": "Lyste",
"column.mutes": "Uitgedoofte gebruikers", "column.mutes": "Uitgedoofte gebruikers",
"column.notifications": "Kennisgewings", "column.notifications": "Kennisgewings",
"column.pins": "Vasgemaakte plasings",
"column.public": "Gefedereerde tydlyn", "column.public": "Gefedereerde tydlyn",
"column_back_button.label": "Terug", "column_back_button.label": "Terug",
"column_header.hide_settings": "Versteek instellings", "column_header.hide_settings": "Versteek instellings",
@ -196,7 +195,6 @@
"keyboard_shortcuts.my_profile": "to open your profile", "keyboard_shortcuts.my_profile": "to open your profile",
"keyboard_shortcuts.notifications": "Vertoon kennisgewingkolom", "keyboard_shortcuts.notifications": "Vertoon kennisgewingkolom",
"keyboard_shortcuts.open_media": "to open media", "keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.pinned": "Vertoon vasgemaakte plasings",
"keyboard_shortcuts.profile": "Vertoon skrywersprofiel", "keyboard_shortcuts.profile": "Vertoon skrywersprofiel",
"keyboard_shortcuts.reply": "Reageer op plasing", "keyboard_shortcuts.reply": "Reageer op plasing",
"keyboard_shortcuts.requests": "Sien volgversoeke", "keyboard_shortcuts.requests": "Sien volgversoeke",
@ -224,7 +222,6 @@
"navigation_bar.lists": "Lyste", "navigation_bar.lists": "Lyste",
"navigation_bar.logout": "Teken uit", "navigation_bar.logout": "Teken uit",
"navigation_bar.personal": "Persoonlik", "navigation_bar.personal": "Persoonlik",
"navigation_bar.pins": "Vasgemaakte plasings",
"navigation_bar.preferences": "Voorkeure", "navigation_bar.preferences": "Voorkeure",
"navigation_bar.public_timeline": "Gefedereerde tydlyn", "navigation_bar.public_timeline": "Gefedereerde tydlyn",
"navigation_bar.search": "Soek", "navigation_bar.search": "Soek",
@ -262,7 +259,6 @@
"status.copy": "Kopieer skakel na hierdie plasing", "status.copy": "Kopieer skakel na hierdie plasing",
"status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}", "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
"status.open": "Brei hierdie plasing uit", "status.open": "Brei hierdie plasing uit",
"status.pinned": "Vasgemaakte plasing",
"status.reblog": "Stuur aan", "status.reblog": "Stuur aan",
"status.reblog_private": "Stuur aan met oorspronklike sigbaarheid", "status.reblog_private": "Stuur aan met oorspronklike sigbaarheid",
"status.reblogged_by": "Aangestuur deur {name}", "status.reblogged_by": "Aangestuur deur {name}",

View File

@ -96,7 +96,6 @@
"column.lists": "Listas", "column.lists": "Listas",
"column.mutes": "Usuarios silenciaus", "column.mutes": "Usuarios silenciaus",
"column.notifications": "Notificacions", "column.notifications": "Notificacions",
"column.pins": "Publicacions fixadas",
"column.public": "Linia de tiempo federada", "column.public": "Linia de tiempo federada",
"column_back_button.label": "Dezaga", "column_back_button.label": "Dezaga",
"column_header.hide_settings": "Amagar configuración", "column_header.hide_settings": "Amagar configuración",
@ -264,7 +263,6 @@
"keyboard_shortcuts.my_profile": "Ubrir lo tuyo perfil", "keyboard_shortcuts.my_profile": "Ubrir lo tuyo perfil",
"keyboard_shortcuts.notifications": "Ubrir la columna de notificacions", "keyboard_shortcuts.notifications": "Ubrir la columna de notificacions",
"keyboard_shortcuts.open_media": "Ubrir fichers multimedia", "keyboard_shortcuts.open_media": "Ubrir fichers multimedia",
"keyboard_shortcuts.pinned": "Ubrir la lista de publicacions destacadas",
"keyboard_shortcuts.profile": "Ubrir lo perfil de l'autor", "keyboard_shortcuts.profile": "Ubrir lo perfil de l'autor",
"keyboard_shortcuts.reply": "Responder publicación", "keyboard_shortcuts.reply": "Responder publicación",
"keyboard_shortcuts.requests": "Ubrir la lista de peticions de seguidores", "keyboard_shortcuts.requests": "Ubrir la lista de peticions de seguidores",
@ -303,7 +301,6 @@
"navigation_bar.logout": "Zarrar sesión", "navigation_bar.logout": "Zarrar sesión",
"navigation_bar.mutes": "Usuarios silenciaus", "navigation_bar.mutes": "Usuarios silenciaus",
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Personal",
"navigation_bar.pins": "Publicacions fixadas",
"navigation_bar.preferences": "Preferencias", "navigation_bar.preferences": "Preferencias",
"navigation_bar.public_timeline": "Linia de tiempo federada", "navigation_bar.public_timeline": "Linia de tiempo federada",
"navigation_bar.search": "Buscar", "navigation_bar.search": "Buscar",
@ -452,8 +449,6 @@
"status.mute": "Silenciar @{name}", "status.mute": "Silenciar @{name}",
"status.mute_conversation": "Silenciar conversación", "status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir estau", "status.open": "Expandir estau",
"status.pin": "Fixar",
"status.pinned": "Publicación fixada",
"status.read_more": "Leyer mas", "status.read_more": "Leyer mas",
"status.reblog": "Retutar", "status.reblog": "Retutar",
"status.reblog_private": "Empentar con l'audiencia orichinal", "status.reblog_private": "Empentar con l'audiencia orichinal",
@ -474,7 +469,6 @@
"status.translate": "Traducir", "status.translate": "Traducir",
"status.translated_from_with": "Traduciu de {lang} usando {provider}", "status.translated_from_with": "Traduciu de {lang} usando {provider}",
"status.unmute_conversation": "Deixar de silenciar conversación", "status.unmute_conversation": "Deixar de silenciar conversación",
"status.unpin": "Deixar de fixar",
"subscribed_languages.lead": "Nomás los mensaches en os idiomas triaus amaneixerán en o suyo inicio y atras linias de tiempo dimpués d'o cambio. Tríe garra pa recibir mensaches en totz los idiomas.", "subscribed_languages.lead": "Nomás los mensaches en os idiomas triaus amaneixerán en o suyo inicio y atras linias de tiempo dimpués d'o cambio. Tríe garra pa recibir mensaches en totz los idiomas.",
"subscribed_languages.save": "Alzar cambios", "subscribed_languages.save": "Alzar cambios",
"subscribed_languages.target": "Cambiar idiomas suscritos pa {target}", "subscribed_languages.target": "Cambiar idiomas suscritos pa {target}",

View File

@ -131,7 +131,6 @@
"column.lists": "القوائم", "column.lists": "القوائم",
"column.mutes": "المُستَخدِمون المَكتومون", "column.mutes": "المُستَخدِمون المَكتومون",
"column.notifications": "الإشعارات", "column.notifications": "الإشعارات",
"column.pins": "المنشورات المُثَبَّتَة",
"column.public": "الخيط الفيدرالي", "column.public": "الخيط الفيدرالي",
"column_back_button.label": "العودة", "column_back_button.label": "العودة",
"column_header.hide_settings": "إخفاء الإعدادات", "column_header.hide_settings": "إخفاء الإعدادات",
@ -395,7 +394,6 @@
"keyboard_shortcuts.my_profile": "لفتح ملفك التعريفي", "keyboard_shortcuts.my_profile": "لفتح ملفك التعريفي",
"keyboard_shortcuts.notifications": "لفتح عمود الإشعارات", "keyboard_shortcuts.notifications": "لفتح عمود الإشعارات",
"keyboard_shortcuts.open_media": "لفتح الوسائط", "keyboard_shortcuts.open_media": "لفتح الوسائط",
"keyboard_shortcuts.pinned": "لفتح قائمة المنشورات المثبتة",
"keyboard_shortcuts.profile": "لفتح الملف التعريفي للناشر", "keyboard_shortcuts.profile": "لفتح الملف التعريفي للناشر",
"keyboard_shortcuts.reply": "للردّ", "keyboard_shortcuts.reply": "للردّ",
"keyboard_shortcuts.requests": "لفتح قائمة طلبات المتابعة", "keyboard_shortcuts.requests": "لفتح قائمة طلبات المتابعة",
@ -464,7 +462,6 @@
"navigation_bar.mutes": "الحسابات المكتومة", "navigation_bar.mutes": "الحسابات المكتومة",
"navigation_bar.opened_in_classic_interface": "تُفتَح المنشورات والحسابات وغيرها من الصفحات الخاصة بشكل مبدئي على واجهة الويب التقليدية.", "navigation_bar.opened_in_classic_interface": "تُفتَح المنشورات والحسابات وغيرها من الصفحات الخاصة بشكل مبدئي على واجهة الويب التقليدية.",
"navigation_bar.personal": "شخصي", "navigation_bar.personal": "شخصي",
"navigation_bar.pins": "المنشورات المُثَبَّتَة",
"navigation_bar.preferences": "التفضيلات", "navigation_bar.preferences": "التفضيلات",
"navigation_bar.public_timeline": "الخيط الفيدرالي", "navigation_bar.public_timeline": "الخيط الفيدرالي",
"navigation_bar.search": "البحث", "navigation_bar.search": "البحث",
@ -724,8 +721,6 @@
"status.mute": "أكتم @{name}", "status.mute": "أكتم @{name}",
"status.mute_conversation": "كتم المحادثة", "status.mute_conversation": "كتم المحادثة",
"status.open": "وسّع هذا المنشور", "status.open": "وسّع هذا المنشور",
"status.pin": "دبّسه على الصفحة التعريفية",
"status.pinned": "منشور مثبَّت",
"status.read_more": "اقرأ المزيد", "status.read_more": "اقرأ المزيد",
"status.reblog": "إعادة النشر", "status.reblog": "إعادة النشر",
"status.reblog_private": "إعادة النشر إلى الجمهور الأصلي", "status.reblog_private": "إعادة النشر إلى الجمهور الأصلي",
@ -749,7 +744,6 @@
"status.translated_from_with": "مترجم من {lang} باستخدام {provider}", "status.translated_from_with": "مترجم من {lang} باستخدام {provider}",
"status.uncached_media_warning": "المعاينة غير متوفرة", "status.uncached_media_warning": "المعاينة غير متوفرة",
"status.unmute_conversation": "فك الكتم عن المحادثة", "status.unmute_conversation": "فك الكتم عن المحادثة",
"status.unpin": "فك التدبيس من الصفحة التعريفية",
"subscribed_languages.lead": "فقط المنشورات في اللغات المحددة ستظهر في خيطك الرئيسي وتسرد في الجداول الزمنية بعد تأكيد التغيير. لا تقم بأي خيار لتلقي المنشورات في جميع اللغات.", "subscribed_languages.lead": "فقط المنشورات في اللغات المحددة ستظهر في خيطك الرئيسي وتسرد في الجداول الزمنية بعد تأكيد التغيير. لا تقم بأي خيار لتلقي المنشورات في جميع اللغات.",
"subscribed_languages.save": "حفظ التغييرات", "subscribed_languages.save": "حفظ التغييرات",
"subscribed_languages.target": "تغيير اللغات المشتركة لـ {target}", "subscribed_languages.target": "تغيير اللغات المشتركة لـ {target}",

View File

@ -112,7 +112,6 @@
"column.lists": "Llistes", "column.lists": "Llistes",
"column.mutes": "Perfiles colos avisos desactivaos", "column.mutes": "Perfiles colos avisos desactivaos",
"column.notifications": "Avisos", "column.notifications": "Avisos",
"column.pins": "Artículos fixaos",
"column.public": "Llinia de tiempu federada", "column.public": "Llinia de tiempu federada",
"column_back_button.label": "Atrás", "column_back_button.label": "Atrás",
"column_header.moveLeft_settings": "Mover la columna a la esquierda", "column_header.moveLeft_settings": "Mover la columna a la esquierda",
@ -309,7 +308,6 @@
"keyboard_shortcuts.my_profile": "Abrir el to perfil", "keyboard_shortcuts.my_profile": "Abrir el to perfil",
"keyboard_shortcuts.notifications": "Abrir la columna d'avisos", "keyboard_shortcuts.notifications": "Abrir la columna d'avisos",
"keyboard_shortcuts.open_media": "Abrir el conteníu mutimedia", "keyboard_shortcuts.open_media": "Abrir el conteníu mutimedia",
"keyboard_shortcuts.pinned": "Abrir la llista d'artículos fixaos",
"keyboard_shortcuts.profile": "Abrir el perfil del autor/a", "keyboard_shortcuts.profile": "Abrir el perfil del autor/a",
"keyboard_shortcuts.reply": "Responder a una publicación", "keyboard_shortcuts.reply": "Responder a una publicación",
"keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu", "keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu",
@ -359,7 +357,6 @@
"navigation_bar.mutes": "Perfiles colos avisos desactivaos", "navigation_bar.mutes": "Perfiles colos avisos desactivaos",
"navigation_bar.opened_in_classic_interface": "Los artículos, les cuentes y otres páxines específiques ábrense por defeutu na interfaz web clásica.", "navigation_bar.opened_in_classic_interface": "Los artículos, les cuentes y otres páxines específiques ábrense por defeutu na interfaz web clásica.",
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Personal",
"navigation_bar.pins": "Artículos fixaos",
"navigation_bar.preferences": "Preferencies", "navigation_bar.preferences": "Preferencies",
"navigation_bar.public_timeline": "Llinia de tiempu federada", "navigation_bar.public_timeline": "Llinia de tiempu federada",
"navigation_bar.security": "Seguranza", "navigation_bar.security": "Seguranza",
@ -529,8 +526,6 @@
"status.mute": "Desactivar los avisos de @{name}", "status.mute": "Desactivar los avisos de @{name}",
"status.mute_conversation": "Desactivar los avisos de la conversación", "status.mute_conversation": "Desactivar los avisos de la conversación",
"status.open": "Espander esta publicación", "status.open": "Espander esta publicación",
"status.pin": "Fixar nel perfil",
"status.pinned": "Publicación fixada",
"status.read_more": "Lleer más", "status.read_more": "Lleer más",
"status.reblog": "Compartir", "status.reblog": "Compartir",
"status.reblogged_by": "{name} compartió", "status.reblogged_by": "{name} compartió",
@ -548,7 +543,6 @@
"status.translated_from_with": "Tradúxose del {lang} con {provider}", "status.translated_from_with": "Tradúxose del {lang} con {provider}",
"status.uncached_media_warning": "La previsualización nun ta disponible", "status.uncached_media_warning": "La previsualización nun ta disponible",
"status.unmute_conversation": "Activar los avisos de la conversación", "status.unmute_conversation": "Activar los avisos de la conversación",
"status.unpin": "Lliberar del perfil",
"subscribed_languages.save": "Guardar los cambeos", "subscribed_languages.save": "Guardar los cambeos",
"tabs_bar.home": "Aniciu", "tabs_bar.home": "Aniciu",
"tabs_bar.notifications": "Avisos", "tabs_bar.notifications": "Avisos",

View File

@ -157,7 +157,6 @@
"column.lists": "Siyahılar", "column.lists": "Siyahılar",
"column.mutes": "Səssizləşdirilmiş istifadəçilər", "column.mutes": "Səssizləşdirilmiş istifadəçilər",
"column.notifications": "Bildirişlər", "column.notifications": "Bildirişlər",
"column.pins": "Bərkidilmiş paylaşımlar",
"column.public": "Federasiya zaman qrafiki", "column.public": "Federasiya zaman qrafiki",
"column_back_button.label": "Geriyə", "column_back_button.label": "Geriyə",
"column_header.hide_settings": "Parametrləri gizlət", "column_header.hide_settings": "Parametrləri gizlət",

View File

@ -151,7 +151,6 @@
"column.lists": "Спісы", "column.lists": "Спісы",
"column.mutes": "Ігнараваныя карыстальнікі", "column.mutes": "Ігнараваныя карыстальнікі",
"column.notifications": "Апавяшчэнні", "column.notifications": "Апавяшчэнні",
"column.pins": "Замацаваныя допісы",
"column.public": "Інтэграваная стужка", "column.public": "Інтэграваная стужка",
"column_back_button.label": "Назад", "column_back_button.label": "Назад",
"column_header.hide_settings": "Схаваць налады", "column_header.hide_settings": "Схаваць налады",
@ -440,7 +439,6 @@
"keyboard_shortcuts.my_profile": "Адкрыць ваш профіль", "keyboard_shortcuts.my_profile": "Адкрыць ваш профіль",
"keyboard_shortcuts.notifications": "Адкрыць слупок апавяшчэнняў", "keyboard_shortcuts.notifications": "Адкрыць слупок апавяшчэнняў",
"keyboard_shortcuts.open_media": "Адкрыць медыя", "keyboard_shortcuts.open_media": "Адкрыць медыя",
"keyboard_shortcuts.pinned": "Адкрыць спіс замацаваных допісаў",
"keyboard_shortcuts.profile": "Адкрыць профіль аўтара", "keyboard_shortcuts.profile": "Адкрыць профіль аўтара",
"keyboard_shortcuts.reply": "Адказаць на допіс", "keyboard_shortcuts.reply": "Адказаць на допіс",
"keyboard_shortcuts.requests": "Адкрыць спіс запытаў на падпіску", "keyboard_shortcuts.requests": "Адкрыць спіс запытаў на падпіску",
@ -505,7 +503,6 @@
"navigation_bar.mutes": "Ігнараваныя карыстальнікі", "navigation_bar.mutes": "Ігнараваныя карыстальнікі",
"navigation_bar.opened_in_classic_interface": "Допісы, уліковыя запісы і іншыя спецыфічныя старонкі па змоўчанні адчыняюцца ў класічным вэб-інтэрфейсе.", "navigation_bar.opened_in_classic_interface": "Допісы, уліковыя запісы і іншыя спецыфічныя старонкі па змоўчанні адчыняюцца ў класічным вэб-інтэрфейсе.",
"navigation_bar.personal": "Асабістае", "navigation_bar.personal": "Асабістае",
"navigation_bar.pins": "Замацаваныя допісы",
"navigation_bar.preferences": "Налады", "navigation_bar.preferences": "Налады",
"navigation_bar.public_timeline": "Глабальная стужка", "navigation_bar.public_timeline": "Глабальная стужка",
"navigation_bar.search": "Пошук", "navigation_bar.search": "Пошук",
@ -782,8 +779,6 @@
"status.mute": "Ігнараваць @{name}", "status.mute": "Ігнараваць @{name}",
"status.mute_conversation": "Ігнараваць размову", "status.mute_conversation": "Ігнараваць размову",
"status.open": "Разгарнуць гэты допіс", "status.open": "Разгарнуць гэты допіс",
"status.pin": "Замацаваць у профілі",
"status.pinned": "Замацаваны допіс",
"status.read_more": "Чытаць болей", "status.read_more": "Чытаць болей",
"status.reblog": "Пашырыць", "status.reblog": "Пашырыць",
"status.reblog_private": "Пашырыць з першапачатковай бачнасцю", "status.reblog_private": "Пашырыць з першапачатковай бачнасцю",
@ -807,7 +802,6 @@
"status.translated_from_with": "Перакладзена з {lang} з дапамогай {provider}", "status.translated_from_with": "Перакладзена з {lang} з дапамогай {provider}",
"status.uncached_media_warning": "Перадпрагляд недаступны", "status.uncached_media_warning": "Перадпрагляд недаступны",
"status.unmute_conversation": "Не ігнараваць размову", "status.unmute_conversation": "Не ігнараваць размову",
"status.unpin": "Адмацаваць ад профілю",
"subscribed_languages.lead": "Толькі допісы ў абраных мовах будуць паказвацца ў вашых стужках пасля змены. Не абірайце нічога, каб бачыць допісы на ўсіх мовах.", "subscribed_languages.lead": "Толькі допісы ў абраных мовах будуць паказвацца ў вашых стужках пасля змены. Не абірайце нічога, каб бачыць допісы на ўсіх мовах.",
"subscribed_languages.save": "Захаваць змены", "subscribed_languages.save": "Захаваць змены",
"subscribed_languages.target": "Змяніць мовы падпіскі для {target}", "subscribed_languages.target": "Змяніць мовы падпіскі для {target}",

View File

@ -162,7 +162,6 @@
"column.lists": "Списъци", "column.lists": "Списъци",
"column.mutes": "Заглушени потребители", "column.mutes": "Заглушени потребители",
"column.notifications": "Известия", "column.notifications": "Известия",
"column.pins": "Закачени публикации",
"column.public": "Федеративна хронология", "column.public": "Федеративна хронология",
"column_back_button.label": "Назад", "column_back_button.label": "Назад",
"column_header.hide_settings": "Скриване на настройките", "column_header.hide_settings": "Скриване на настройките",
@ -465,7 +464,6 @@
"keyboard_shortcuts.my_profile": "Отваряне на профила ви", "keyboard_shortcuts.my_profile": "Отваряне на профила ви",
"keyboard_shortcuts.notifications": "Отваряне на колоната с известия", "keyboard_shortcuts.notifications": "Отваряне на колоната с известия",
"keyboard_shortcuts.open_media": "Отваряне на мултимедията", "keyboard_shortcuts.open_media": "Отваряне на мултимедията",
"keyboard_shortcuts.pinned": "Отваряне на списъка със закачени публикации",
"keyboard_shortcuts.profile": "Отваряне на профила на автора", "keyboard_shortcuts.profile": "Отваряне на профила на автора",
"keyboard_shortcuts.reply": "Отговаряне на публикация", "keyboard_shortcuts.reply": "Отговаряне на публикация",
"keyboard_shortcuts.requests": "Отваряне на списъка със заявки за последване", "keyboard_shortcuts.requests": "Отваряне на списъка със заявки за последване",
@ -549,7 +547,6 @@
"navigation_bar.mutes": "Заглушени потребители", "navigation_bar.mutes": "Заглушени потребители",
"navigation_bar.opened_in_classic_interface": "Публикации, акаунти и други особени страници се отварят по подразбиране в класическия мрежови интерфейс.", "navigation_bar.opened_in_classic_interface": "Публикации, акаунти и други особени страници се отварят по подразбиране в класическия мрежови интерфейс.",
"navigation_bar.personal": "Лично", "navigation_bar.personal": "Лично",
"navigation_bar.pins": "Закачени публикации",
"navigation_bar.preferences": "Предпочитания", "navigation_bar.preferences": "Предпочитания",
"navigation_bar.public_timeline": "Федеративна хронология", "navigation_bar.public_timeline": "Федеративна хронология",
"navigation_bar.search": "Търсене", "navigation_bar.search": "Търсене",
@ -845,8 +842,6 @@
"status.mute": "Заглушаване на @{name}", "status.mute": "Заглушаване на @{name}",
"status.mute_conversation": "Заглушаване на разговора", "status.mute_conversation": "Заглушаване на разговора",
"status.open": "Разширяване на публикацията", "status.open": "Разширяване на публикацията",
"status.pin": "Закачане в профила",
"status.pinned": "Закачена публикация",
"status.read_more": "Още за четене", "status.read_more": "Още за четене",
"status.reblog": "Подсилване", "status.reblog": "Подсилване",
"status.reblog_private": "Подсилване с оригиналната видимост", "status.reblog_private": "Подсилване с оригиналната видимост",
@ -871,7 +866,6 @@
"status.translated_from_with": "Преведено от {lang}, използвайки {provider}", "status.translated_from_with": "Преведено от {lang}, използвайки {provider}",
"status.uncached_media_warning": "Онагледяването не е налично", "status.uncached_media_warning": "Онагледяването не е налично",
"status.unmute_conversation": "Без заглушаването на разговора", "status.unmute_conversation": "Без заглушаването на разговора",
"status.unpin": "Разкачане от профила",
"subscribed_languages.lead": "Публикации само на избрани езици ще се явяват в началото ви и в хронологичните списъци след промяната. Изберете \"нищо\", за да получавате публикации на всички езици.", "subscribed_languages.lead": "Публикации само на избрани езици ще се явяват в началото ви и в хронологичните списъци след промяната. Изберете \"нищо\", за да получавате публикации на всички езици.",
"subscribed_languages.save": "Запазване на промените", "subscribed_languages.save": "Запазване на промените",
"subscribed_languages.target": "Промяна на абонираните езици за {target}", "subscribed_languages.target": "Промяна на абонираните езици за {target}",

View File

@ -112,7 +112,6 @@
"column.lists": "তালিকাগুলো", "column.lists": "তালিকাগুলো",
"column.mutes": "যাদের কার্যক্রম দেখা বন্ধ আছে", "column.mutes": "যাদের কার্যক্রম দেখা বন্ধ আছে",
"column.notifications": "প্রজ্ঞাপনগুলো", "column.notifications": "প্রজ্ঞাপনগুলো",
"column.pins": "পিন করা টুট",
"column.public": "যুক্ত সময়রেখা", "column.public": "যুক্ত সময়রেখা",
"column_back_button.label": "পেছনে", "column_back_button.label": "পেছনে",
"column_header.hide_settings": "সেটিংগুলো সরান", "column_header.hide_settings": "সেটিংগুলো সরান",
@ -258,7 +257,6 @@
"keyboard_shortcuts.my_profile": "আপনার নিজের পাতা দেখতে", "keyboard_shortcuts.my_profile": "আপনার নিজের পাতা দেখতে",
"keyboard_shortcuts.notifications": "প্রজ্ঞাপনের কলাম খুলতে", "keyboard_shortcuts.notifications": "প্রজ্ঞাপনের কলাম খুলতে",
"keyboard_shortcuts.open_media": "মিডিয়া খলার জন্য", "keyboard_shortcuts.open_media": "মিডিয়া খলার জন্য",
"keyboard_shortcuts.pinned": "পিন দেওয়া টুটের তালিকা খুলতে",
"keyboard_shortcuts.profile": "লেখকের পাতা দেখতে", "keyboard_shortcuts.profile": "লেখকের পাতা দেখতে",
"keyboard_shortcuts.reply": "মতামত দিতে", "keyboard_shortcuts.reply": "মতামত দিতে",
"keyboard_shortcuts.requests": "অনুসরণ অনুরোধের তালিকা দেখতে", "keyboard_shortcuts.requests": "অনুসরণ অনুরোধের তালিকা দেখতে",
@ -294,7 +292,6 @@
"navigation_bar.logout": "বাইরে যান", "navigation_bar.logout": "বাইরে যান",
"navigation_bar.mutes": "যাদের কার্যক্রম দেখা বন্ধ আছে", "navigation_bar.mutes": "যাদের কার্যক্রম দেখা বন্ধ আছে",
"navigation_bar.personal": "নিজস্ব", "navigation_bar.personal": "নিজস্ব",
"navigation_bar.pins": "পিন দেওয়া টুট",
"navigation_bar.preferences": "পছন্দসমূহ", "navigation_bar.preferences": "পছন্দসমূহ",
"navigation_bar.public_timeline": "যুক্তবিশ্বের সময়রেখা", "navigation_bar.public_timeline": "যুক্তবিশ্বের সময়রেখা",
"navigation_bar.search": "অনুসন্ধান", "navigation_bar.search": "অনুসন্ধান",
@ -389,8 +386,6 @@
"status.mute": "@{name}র কার্যক্রম সরিয়ে ফেলতে", "status.mute": "@{name}র কার্যক্রম সরিয়ে ফেলতে",
"status.mute_conversation": "কথোপকথননের প্রজ্ঞাপন সরিয়ে ফেলতে", "status.mute_conversation": "কথোপকথননের প্রজ্ঞাপন সরিয়ে ফেলতে",
"status.open": "এটার সম্পূর্ণটা দেখতে", "status.open": "এটার সম্পূর্ণটা দেখতে",
"status.pin": "নিজের পাতায় এটা পিন করতে",
"status.pinned": "পিন করা টুট",
"status.read_more": "আরো পড়ুন", "status.read_more": "আরো পড়ুন",
"status.reblog": "সমর্থন দিতে", "status.reblog": "সমর্থন দিতে",
"status.reblog_private": "আপনার অনুসরণকারীদের কাছে এটার সমর্থন দেখাতে", "status.reblog_private": "আপনার অনুসরণকারীদের কাছে এটার সমর্থন দেখাতে",
@ -408,7 +403,6 @@
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}", "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
"status.translate": "অনুবাদ", "status.translate": "অনুবাদ",
"status.unmute_conversation": "আলোচনার প্রজ্ঞাপন চালু করতে", "status.unmute_conversation": "আলোচনার প্রজ্ঞাপন চালু করতে",
"status.unpin": "নিজের পাতা থেকে পিন করে রাখাটির পিন খুলতে",
"tabs_bar.home": "বাড়ি", "tabs_bar.home": "বাড়ি",
"tabs_bar.notifications": "প্রজ্ঞাপনগুলো", "tabs_bar.notifications": "প্রজ্ঞাপনগুলো",
"time_remaining.days": "{number, plural, one {# day} other {# days}} বাকি আছে", "time_remaining.days": "{number, plural, one {# day} other {# days}} বাকি আছে",

View File

@ -124,7 +124,6 @@
"column.lists": "Listennoù", "column.lists": "Listennoù",
"column.mutes": "Implijer·ion·ezed kuzhet", "column.mutes": "Implijer·ion·ezed kuzhet",
"column.notifications": "Kemennoù", "column.notifications": "Kemennoù",
"column.pins": "Embannadurioù spilhennet",
"column.public": "Red-amzer kevredet", "column.public": "Red-amzer kevredet",
"column_back_button.label": "Distreiñ", "column_back_button.label": "Distreiñ",
"column_header.hide_settings": "Kuzhat an arventennoù", "column_header.hide_settings": "Kuzhat an arventennoù",
@ -329,7 +328,6 @@
"keyboard_shortcuts.my_profile": "Digeriñ ho profil", "keyboard_shortcuts.my_profile": "Digeriñ ho profil",
"keyboard_shortcuts.notifications": "Digeriñ bann ar c'hemennoù", "keyboard_shortcuts.notifications": "Digeriñ bann ar c'hemennoù",
"keyboard_shortcuts.open_media": "Digeriñ ar media", "keyboard_shortcuts.open_media": "Digeriñ ar media",
"keyboard_shortcuts.pinned": "Digeriñ listenn an toudoù spilhennet",
"keyboard_shortcuts.profile": "Digeriñ profil an aozer.ez", "keyboard_shortcuts.profile": "Digeriñ profil an aozer.ez",
"keyboard_shortcuts.reply": "Respont d'an toud", "keyboard_shortcuts.reply": "Respont d'an toud",
"keyboard_shortcuts.requests": "Digeriñ roll goulennoù heuliañ", "keyboard_shortcuts.requests": "Digeriñ roll goulennoù heuliañ",
@ -378,7 +376,6 @@
"navigation_bar.logout": "Digennaskañ", "navigation_bar.logout": "Digennaskañ",
"navigation_bar.mutes": "Implijer·ion·ezed kuzhet", "navigation_bar.mutes": "Implijer·ion·ezed kuzhet",
"navigation_bar.personal": "Personel", "navigation_bar.personal": "Personel",
"navigation_bar.pins": "Toudoù spilhennet",
"navigation_bar.preferences": "Gwellvezioù", "navigation_bar.preferences": "Gwellvezioù",
"navigation_bar.public_timeline": "Red-amzer kevredet", "navigation_bar.public_timeline": "Red-amzer kevredet",
"navigation_bar.search": "Klask", "navigation_bar.search": "Klask",
@ -578,8 +575,6 @@
"status.mute": "Kuzhat @{name}", "status.mute": "Kuzhat @{name}",
"status.mute_conversation": "Kuzhat ar gaozeadenn", "status.mute_conversation": "Kuzhat ar gaozeadenn",
"status.open": "Digeriñ ar c'hannad-mañ", "status.open": "Digeriñ ar c'hannad-mañ",
"status.pin": "Spilhennañ d'ar profil",
"status.pinned": "Toud spilhennet",
"status.read_more": "Lenn muioc'h", "status.read_more": "Lenn muioc'h",
"status.reblog": "Skignañ", "status.reblog": "Skignañ",
"status.reblog_private": "Skignañ gant ar weledenn gentañ", "status.reblog_private": "Skignañ gant ar weledenn gentañ",
@ -601,7 +596,6 @@
"status.translated_from_with": "Troet diwar {lang} gant {provider}", "status.translated_from_with": "Troet diwar {lang} gant {provider}",
"status.uncached_media_warning": "Rakwel n'eo ket da gaout", "status.uncached_media_warning": "Rakwel n'eo ket da gaout",
"status.unmute_conversation": "Diguzhat ar gaozeadenn", "status.unmute_conversation": "Diguzhat ar gaozeadenn",
"status.unpin": "Dispilhennañ eus ar profil",
"subscribed_languages.save": "Enrollañ ar cheñchamantoù", "subscribed_languages.save": "Enrollañ ar cheñchamantoù",
"subscribed_languages.target": "Cheñch ar yezhoù koumanantet evit {target}", "subscribed_languages.target": "Cheñch ar yezhoù koumanantet evit {target}",
"tabs_bar.home": "Degemer", "tabs_bar.home": "Degemer",

View File

@ -2,7 +2,6 @@
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.cancel_follow_request": "Withdraw follow request", "account.cancel_follow_request": "Withdraw follow request",
"account_note.placeholder": "Click to add a note", "account_note.placeholder": "Click to add a note",
"column.pins": "Pinned post",
"community.column_settings.media_only": "Media only", "community.column_settings.media_only": "Media only",
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.", "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
@ -34,7 +33,6 @@
"keyboard_shortcuts.my_profile": "to open your profile", "keyboard_shortcuts.my_profile": "to open your profile",
"keyboard_shortcuts.notifications": "to open notifications column", "keyboard_shortcuts.notifications": "to open notifications column",
"keyboard_shortcuts.open_media": "to open media", "keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.pinned": "to open pinned posts list",
"keyboard_shortcuts.profile": "to open author's profile", "keyboard_shortcuts.profile": "to open author's profile",
"keyboard_shortcuts.reply": "to reply", "keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list", "keyboard_shortcuts.requests": "to open follow requests list",

View File

@ -29,6 +29,7 @@
"account.enable_notifications": "Notifica'm els tuts de @{name}", "account.enable_notifications": "Notifica'm els tuts de @{name}",
"account.endorse": "Recomana en el perfil", "account.endorse": "Recomana en el perfil",
"account.featured": "Destacat", "account.featured": "Destacat",
"account.featured.accounts": "Perfils",
"account.featured.hashtags": "Etiquetes", "account.featured.hashtags": "Etiquetes",
"account.featured.posts": "Publicacions", "account.featured.posts": "Publicacions",
"account.featured_tags.last_status_at": "Darrer tut el {date}", "account.featured_tags.last_status_at": "Darrer tut el {date}",
@ -168,7 +169,7 @@
"column.lists": "Llistes", "column.lists": "Llistes",
"column.mutes": "Usuaris silenciats", "column.mutes": "Usuaris silenciats",
"column.notifications": "Notificacions", "column.notifications": "Notificacions",
"column.pins": "Tuts fixats", "column.pins": "Publicacions destacades",
"column.public": "Línia de temps federada", "column.public": "Línia de temps federada",
"column_back_button.label": "Enrere", "column_back_button.label": "Enrere",
"column_header.hide_settings": "Amaga la configuració", "column_header.hide_settings": "Amaga la configuració",
@ -405,8 +406,10 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participant} other {{counter} participants}}", "hashtag.counter_by_accounts": "{count, plural, one {{counter} participant} other {{counter} participants}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} tut} other {{counter} tuts}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} tut} other {{counter} tuts}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} tut} other {{counter} tuts}} avui", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} tut} other {{counter} tuts}} avui",
"hashtag.feature": "Destaca al perfil",
"hashtag.follow": "Segueix l'etiqueta", "hashtag.follow": "Segueix l'etiqueta",
"hashtag.mute": "Silencia #{hashtag}", "hashtag.mute": "Silencia #{hashtag}",
"hashtag.unfeature": "No destaquis al perfil",
"hashtag.unfollow": "Deixa de seguir l'etiqueta", "hashtag.unfollow": "Deixa de seguir l'etiqueta",
"hashtags.and_other": "…i {count, plural, other {# més}}", "hashtags.and_other": "…i {count, plural, other {# més}}",
"hints.profiles.followers_may_be_missing": "Es poden haver perdut seguidors d'aquest perfil.", "hints.profiles.followers_may_be_missing": "Es poden haver perdut seguidors d'aquest perfil.",
@ -476,7 +479,7 @@
"keyboard_shortcuts.my_profile": "Obre el teu perfil", "keyboard_shortcuts.my_profile": "Obre el teu perfil",
"keyboard_shortcuts.notifications": "Obre la columna de notificacions", "keyboard_shortcuts.notifications": "Obre la columna de notificacions",
"keyboard_shortcuts.open_media": "Obre mèdia", "keyboard_shortcuts.open_media": "Obre mèdia",
"keyboard_shortcuts.pinned": "Obre la llista de tuts fixats", "keyboard_shortcuts.pinned": "Obre la llista de publicacions destacades",
"keyboard_shortcuts.profile": "Obre el perfil de l'autor", "keyboard_shortcuts.profile": "Obre el perfil de l'autor",
"keyboard_shortcuts.reply": "Respon al tut", "keyboard_shortcuts.reply": "Respon al tut",
"keyboard_shortcuts.requests": "Obre la llista de sol·licituds de seguiment", "keyboard_shortcuts.requests": "Obre la llista de sol·licituds de seguiment",
@ -560,7 +563,7 @@
"navigation_bar.mutes": "Usuaris silenciats", "navigation_bar.mutes": "Usuaris silenciats",
"navigation_bar.opened_in_classic_interface": "Els tuts, comptes i altres pàgines especifiques s'obren per defecte en la interfície web clàssica.", "navigation_bar.opened_in_classic_interface": "Els tuts, comptes i altres pàgines especifiques s'obren per defecte en la interfície web clàssica.",
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Personal",
"navigation_bar.pins": "Tuts fixats", "navigation_bar.pins": "Publicacions destacades",
"navigation_bar.preferences": "Preferències", "navigation_bar.preferences": "Preferències",
"navigation_bar.public_timeline": "Línia de temps federada", "navigation_bar.public_timeline": "Línia de temps federada",
"navigation_bar.search": "Cerca", "navigation_bar.search": "Cerca",
@ -856,8 +859,7 @@
"status.mute": "Silencia @{name}", "status.mute": "Silencia @{name}",
"status.mute_conversation": "Silencia la conversa", "status.mute_conversation": "Silencia la conversa",
"status.open": "Amplia el tut", "status.open": "Amplia el tut",
"status.pin": "Fixa en el perfil", "status.pin": "Destaca al perfil",
"status.pinned": "Tut fixat",
"status.read_more": "Més informació", "status.read_more": "Més informació",
"status.reblog": "Impulsa", "status.reblog": "Impulsa",
"status.reblog_private": "Impulsa amb la visibilitat original", "status.reblog_private": "Impulsa amb la visibilitat original",
@ -882,7 +884,7 @@
"status.translated_from_with": "Traduït del {lang} fent servir {provider}", "status.translated_from_with": "Traduït del {lang} fent servir {provider}",
"status.uncached_media_warning": "Previsualització no disponible", "status.uncached_media_warning": "Previsualització no disponible",
"status.unmute_conversation": "Deixa de silenciar la conversa", "status.unmute_conversation": "Deixa de silenciar la conversa",
"status.unpin": "Desfixa del perfil", "status.unpin": "No destaquis al perfil",
"subscribed_languages.lead": "Només els tuts en les llengües seleccionades apareixeran en les teves línies de temps \"Inici\" i \"Llistes\" després del canvi. No en seleccionis cap per a rebre tuts en totes les llengües.", "subscribed_languages.lead": "Només els tuts en les llengües seleccionades apareixeran en les teves línies de temps \"Inici\" i \"Llistes\" després del canvi. No en seleccionis cap per a rebre tuts en totes les llengües.",
"subscribed_languages.save": "Desa els canvis", "subscribed_languages.save": "Desa els canvis",
"subscribed_languages.target": "Canvia les llengües subscrites per a {target}", "subscribed_languages.target": "Canvia les llengües subscrites per a {target}",

View File

@ -112,7 +112,6 @@
"column.lists": "پێرست", "column.lists": "پێرست",
"column.mutes": "بێدەنگکردنی بەکارهێنەران", "column.mutes": "بێدەنگکردنی بەکارهێنەران",
"column.notifications": "ئاگادارییەکان", "column.notifications": "ئاگادارییەکان",
"column.pins": "تووتسی چەسپاو",
"column.public": "نووسراوەکانی هەمووشوێنێک", "column.public": "نووسراوەکانی هەمووشوێنێک",
"column_back_button.label": "دواوە", "column_back_button.label": "دواوە",
"column_header.hide_settings": "شاردنەوەی ڕێکخستنەکان", "column_header.hide_settings": "شاردنەوەی ڕێکخستنەکان",
@ -308,7 +307,6 @@
"keyboard_shortcuts.my_profile": "بۆ کردنەوەی پرۆفایڵ", "keyboard_shortcuts.my_profile": "بۆ کردنەوەی پرۆفایڵ",
"keyboard_shortcuts.notifications": "بۆ کردنەوەی ستوونی ئاگانامەکان", "keyboard_shortcuts.notifications": "بۆ کردنەوەی ستوونی ئاگانامەکان",
"keyboard_shortcuts.open_media": "بۆ کردنەوەی میدیا", "keyboard_shortcuts.open_media": "بۆ کردنەوەی میدیا",
"keyboard_shortcuts.pinned": "بۆ کردنەوەی لیستی توتەکانی چەسپێنراو",
"keyboard_shortcuts.profile": "بۆ کردنەوەی پرۆفایڵی نووسەر", "keyboard_shortcuts.profile": "بۆ کردنەوەی پرۆفایڵی نووسەر",
"keyboard_shortcuts.reply": "بۆ وەڵامدانەوە", "keyboard_shortcuts.reply": "بۆ وەڵامدانەوە",
"keyboard_shortcuts.requests": "بۆ کردنەوەی لیستی داواکاریەکانی بەدوادا", "keyboard_shortcuts.requests": "بۆ کردنەوەی لیستی داواکاریەکانی بەدوادا",
@ -349,7 +347,6 @@
"navigation_bar.logout": "دەرچوون", "navigation_bar.logout": "دەرچوون",
"navigation_bar.mutes": "کپکردنی بەکارهێنەران", "navigation_bar.mutes": "کپکردنی بەکارهێنەران",
"navigation_bar.personal": "کەسی", "navigation_bar.personal": "کەسی",
"navigation_bar.pins": "توتی چەسپاو",
"navigation_bar.preferences": "پەسەندەکان", "navigation_bar.preferences": "پەسەندەکان",
"navigation_bar.public_timeline": "نووسراوەکانی هەمووشوێنێک", "navigation_bar.public_timeline": "نووسراوەکانی هەمووشوێنێک",
"navigation_bar.search": "گەڕان", "navigation_bar.search": "گەڕان",
@ -508,8 +505,6 @@
"status.mute": "@{name} بێدەنگ بکە", "status.mute": "@{name} بێدەنگ بکە",
"status.mute_conversation": "بێدەنگی بکە", "status.mute_conversation": "بێدەنگی بکە",
"status.open": "ئەم توتە فراوان بکە", "status.open": "ئەم توتە فراوان بکە",
"status.pin": "لکاندن لەسەر پرۆفایل",
"status.pinned": "توتی چەسپکراو",
"status.read_more": "زیاتر بخوێنەوە", "status.read_more": "زیاتر بخوێنەوە",
"status.reblog": "بەهێزکردن", "status.reblog": "بەهێزکردن",
"status.reblog_private": "بەهێزکردن بۆ بینەرانی سەرەتایی", "status.reblog_private": "بەهێزکردن بۆ بینەرانی سەرەتایی",
@ -530,7 +525,6 @@
"status.translate": "وەریبگێرە", "status.translate": "وەریبگێرە",
"status.translated_from_with": "لە {lang} وەرگێڕدراوە بە بەکارهێنانی {provider}", "status.translated_from_with": "لە {lang} وەرگێڕدراوە بە بەکارهێنانی {provider}",
"status.unmute_conversation": "گفتوگۆی بێدەنگ", "status.unmute_conversation": "گفتوگۆی بێدەنگ",
"status.unpin": "لە سەرەوە لایبە",
"subscribed_languages.lead": "تەنها پۆستەکان بە زمانە هەڵبژێردراوەکان لە ماڵەکەتدا دەردەکەون و هێڵەکانی کاتی لیستەکەت دوای گۆڕانکارییەکە. هیچیان هەڵبژێرە بۆ وەرگرتنی پۆست بە هەموو زمانەکان.", "subscribed_languages.lead": "تەنها پۆستەکان بە زمانە هەڵبژێردراوەکان لە ماڵەکەتدا دەردەکەون و هێڵەکانی کاتی لیستەکەت دوای گۆڕانکارییەکە. هیچیان هەڵبژێرە بۆ وەرگرتنی پۆست بە هەموو زمانەکان.",
"subscribed_languages.save": "پاشکەوتی گۆڕانکاریەکان", "subscribed_languages.save": "پاشکەوتی گۆڕانکاریەکان",
"subscribed_languages.target": "گۆڕینی زمانە بەشداربووەکان بۆ {target}", "subscribed_languages.target": "گۆڕینی زمانە بەشداربووەکان بۆ {target}",

View File

@ -53,7 +53,6 @@
"column.lists": "Liste", "column.lists": "Liste",
"column.mutes": "Utilizatori piattati", "column.mutes": "Utilizatori piattati",
"column.notifications": "Nutificazione", "column.notifications": "Nutificazione",
"column.pins": "Statuti puntarulati",
"column.public": "Linea pubblica glubale", "column.public": "Linea pubblica glubale",
"column_back_button.label": "Ritornu", "column_back_button.label": "Ritornu",
"column_header.hide_settings": "Piattà i parametri", "column_header.hide_settings": "Piattà i parametri",
@ -178,7 +177,6 @@
"keyboard_shortcuts.my_profile": "per apre u vostru prufile", "keyboard_shortcuts.my_profile": "per apre u vostru prufile",
"keyboard_shortcuts.notifications": "per apre a culonna di nutificazione", "keyboard_shortcuts.notifications": "per apre a culonna di nutificazione",
"keyboard_shortcuts.open_media": "per apre i media", "keyboard_shortcuts.open_media": "per apre i media",
"keyboard_shortcuts.pinned": "per apre a lista di statuti puntarulati",
"keyboard_shortcuts.profile": "per apre u prufile di l'autore", "keyboard_shortcuts.profile": "per apre u prufile di l'autore",
"keyboard_shortcuts.reply": "risponde", "keyboard_shortcuts.reply": "risponde",
"keyboard_shortcuts.requests": "per apre a lista di dumande d'abbunamentu", "keyboard_shortcuts.requests": "per apre a lista di dumande d'abbunamentu",
@ -212,7 +210,6 @@
"navigation_bar.logout": "Scunnettassi", "navigation_bar.logout": "Scunnettassi",
"navigation_bar.mutes": "Utilizatori piattati", "navigation_bar.mutes": "Utilizatori piattati",
"navigation_bar.personal": "Persunale", "navigation_bar.personal": "Persunale",
"navigation_bar.pins": "Statuti puntarulati",
"navigation_bar.preferences": "Preferenze", "navigation_bar.preferences": "Preferenze",
"navigation_bar.public_timeline": "Linea pubblica glubale", "navigation_bar.public_timeline": "Linea pubblica glubale",
"navigation_bar.security": "Sicurità", "navigation_bar.security": "Sicurità",
@ -296,8 +293,6 @@
"status.mute": "Piattà @{name}", "status.mute": "Piattà @{name}",
"status.mute_conversation": "Piattà a cunversazione", "status.mute_conversation": "Piattà a cunversazione",
"status.open": "Apre stu statutu", "status.open": "Apre stu statutu",
"status.pin": "Puntarulà à u prufile",
"status.pinned": "Statutu puntarulatu",
"status.read_more": "Leghje di più", "status.read_more": "Leghje di più",
"status.reblog": "Sparte", "status.reblog": "Sparte",
"status.reblog_private": "Sparte à l'audienza uriginale", "status.reblog_private": "Sparte à l'audienza uriginale",
@ -314,7 +309,6 @@
"status.show_more_all": "Slibrà tuttu", "status.show_more_all": "Slibrà tuttu",
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}", "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
"status.unmute_conversation": "Ùn piattà più a cunversazione", "status.unmute_conversation": "Ùn piattà più a cunversazione",
"status.unpin": "Spuntarulà da u prufile",
"tabs_bar.home": "Accolta", "tabs_bar.home": "Accolta",
"tabs_bar.notifications": "Nutificazione", "tabs_bar.notifications": "Nutificazione",
"time_remaining.days": "{number, plural, one {# ghjornu ferma} other {# ghjorni fermanu}}", "time_remaining.days": "{number, plural, one {# ghjornu ferma} other {# ghjorni fermanu}}",

View File

@ -29,6 +29,7 @@
"account.enable_notifications": "Oznamovat mi příspěvky @{name}", "account.enable_notifications": "Oznamovat mi příspěvky @{name}",
"account.endorse": "Zvýraznit na profilu", "account.endorse": "Zvýraznit na profilu",
"account.featured": "Zvýrazněné", "account.featured": "Zvýrazněné",
"account.featured.accounts": "Profily",
"account.featured.hashtags": "Hashtagy", "account.featured.hashtags": "Hashtagy",
"account.featured.posts": "Příspěvky", "account.featured.posts": "Příspěvky",
"account.featured_tags.last_status_at": "Poslední příspěvek {date}", "account.featured_tags.last_status_at": "Poslední příspěvek {date}",
@ -168,7 +169,7 @@
"column.lists": "Seznamy", "column.lists": "Seznamy",
"column.mutes": "Skrytí uživatelé", "column.mutes": "Skrytí uživatelé",
"column.notifications": "Oznámení", "column.notifications": "Oznámení",
"column.pins": "Připnuté příspěvky", "column.pins": "Zvýrazněné příspěvky",
"column.public": "Federovaná časová osa", "column.public": "Federovaná časová osa",
"column_back_button.label": "Zpět", "column_back_button.label": "Zpět",
"column_header.hide_settings": "Skrýt nastavení", "column_header.hide_settings": "Skrýt nastavení",
@ -405,8 +406,10 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} účastník*ice} few {{counter} účastníci} other {{counter} účastníků}}", "hashtag.counter_by_accounts": "{count, plural, one {{counter} účastník*ice} few {{counter} účastníci} other {{counter} účastníků}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} other {{counter} příspěvků}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} other {{counter} příspěvků}}",
"hashtag.counter_by_uses_today": "Dnes {count, plural, one {{counter} příspěvek} few {{counter} příspěvky} other {{counter} příspěvků}}", "hashtag.counter_by_uses_today": "Dnes {count, plural, one {{counter} příspěvek} few {{counter} příspěvky} other {{counter} příspěvků}}",
"hashtag.feature": "Zvýraznit na profilu",
"hashtag.follow": "Sledovat hashtag", "hashtag.follow": "Sledovat hashtag",
"hashtag.mute": "Skrýt #{hashtag}", "hashtag.mute": "Skrýt #{hashtag}",
"hashtag.unfeature": "Nezvýrazňovat na profilu",
"hashtag.unfollow": "Přestat sledovat hashtag", "hashtag.unfollow": "Přestat sledovat hashtag",
"hashtags.and_other": "…a {count, plural, one {# další} few {# další} other {# dalších}}", "hashtags.and_other": "…a {count, plural, one {# další} few {# další} other {# dalších}}",
"hints.profiles.followers_may_be_missing": "Sledující mohou pro tento profil chybět.", "hints.profiles.followers_may_be_missing": "Sledující mohou pro tento profil chybět.",
@ -477,7 +480,7 @@
"keyboard_shortcuts.my_profile": "Otevřít váš profil", "keyboard_shortcuts.my_profile": "Otevřít váš profil",
"keyboard_shortcuts.notifications": "Otevřít sloupec oznámení", "keyboard_shortcuts.notifications": "Otevřít sloupec oznámení",
"keyboard_shortcuts.open_media": "Otevřít média", "keyboard_shortcuts.open_media": "Otevřít média",
"keyboard_shortcuts.pinned": "Otevřít seznam připnutých příspěvků", "keyboard_shortcuts.pinned": "Otevřít seznam zvýrazněných příspěvků",
"keyboard_shortcuts.profile": "Otevřít autorův profil", "keyboard_shortcuts.profile": "Otevřít autorův profil",
"keyboard_shortcuts.reply": "Odpovědět na příspěvek", "keyboard_shortcuts.reply": "Odpovědět na příspěvek",
"keyboard_shortcuts.requests": "Otevřít seznam žádostí o sledování", "keyboard_shortcuts.requests": "Otevřít seznam žádostí o sledování",
@ -561,7 +564,7 @@
"navigation_bar.mutes": "Skrytí uživatelé", "navigation_bar.mutes": "Skrytí uživatelé",
"navigation_bar.opened_in_classic_interface": "Příspěvky, účty a další specifické stránky jsou ve výchozím nastavení otevřeny v klasickém webovém rozhraní.", "navigation_bar.opened_in_classic_interface": "Příspěvky, účty a další specifické stránky jsou ve výchozím nastavení otevřeny v klasickém webovém rozhraní.",
"navigation_bar.personal": "Osobní", "navigation_bar.personal": "Osobní",
"navigation_bar.pins": "Připnuté příspěvky", "navigation_bar.pins": "Zvýrazněné příspěvky",
"navigation_bar.preferences": "Předvolby", "navigation_bar.preferences": "Předvolby",
"navigation_bar.public_timeline": "Federovaná časová osa", "navigation_bar.public_timeline": "Federovaná časová osa",
"navigation_bar.search": "Hledat", "navigation_bar.search": "Hledat",
@ -857,8 +860,7 @@
"status.mute": "Skrýt @{name}", "status.mute": "Skrýt @{name}",
"status.mute_conversation": "Skrýt konverzaci", "status.mute_conversation": "Skrýt konverzaci",
"status.open": "Rozbalit tento příspěvek", "status.open": "Rozbalit tento příspěvek",
"status.pin": "Připnout na profil", "status.pin": "Zvýraznit na profilu",
"status.pinned": "Připnutý příspěvek",
"status.read_more": "Číst více", "status.read_more": "Číst více",
"status.reblog": "Boostnout", "status.reblog": "Boostnout",
"status.reblog_private": "Boostnout s původní viditelností", "status.reblog_private": "Boostnout s původní viditelností",
@ -883,7 +885,7 @@
"status.translated_from_with": "Přeloženo z {lang} pomocí {provider}", "status.translated_from_with": "Přeloženo z {lang} pomocí {provider}",
"status.uncached_media_warning": "Náhled není k dispozici", "status.uncached_media_warning": "Náhled není k dispozici",
"status.unmute_conversation": "Zrušit skrytí konverzace", "status.unmute_conversation": "Zrušit skrytí konverzace",
"status.unpin": "Odepnout z profilu", "status.unpin": "Nezvýrazňovat na profilu",
"subscribed_languages.lead": "Ve vašem domovském kanálu a časových osách se po změně budou objevovat pouze příspěvky ve vybraných jazycích. Pro příjem příspěvků ve všech jazycích nevyberte žádný jazyk.", "subscribed_languages.lead": "Ve vašem domovském kanálu a časových osách se po změně budou objevovat pouze příspěvky ve vybraných jazycích. Pro příjem příspěvků ve všech jazycích nevyberte žádný jazyk.",
"subscribed_languages.save": "Uložit změny", "subscribed_languages.save": "Uložit změny",
"subscribed_languages.target": "Změnit odebírané jazyky na {target}", "subscribed_languages.target": "Změnit odebírané jazyky na {target}",

View File

@ -6,19 +6,19 @@
"about.domain_blocks.preamble": "Fel rheol, mae Mastodon yn caniatáu i chi weld cynnwys gan unrhyw weinyddwr arall yn y ffedysawd a rhyngweithio â hi. Dyma'r eithriadau a wnaed ar y gweinydd penodol hwn.", "about.domain_blocks.preamble": "Fel rheol, mae Mastodon yn caniatáu i chi weld cynnwys gan unrhyw weinyddwr arall yn y ffedysawd a rhyngweithio â hi. Dyma'r eithriadau a wnaed ar y gweinydd penodol hwn.",
"about.domain_blocks.silenced.explanation": "Fel rheol, fyddwch chi ddim yn gweld proffiliau a chynnwys o'r gweinydd hwn, oni bai eich bod yn chwilio'n benodol amdano neu yn ymuno drwy ei ddilyn.", "about.domain_blocks.silenced.explanation": "Fel rheol, fyddwch chi ddim yn gweld proffiliau a chynnwys o'r gweinydd hwn, oni bai eich bod yn chwilio'n benodol amdano neu yn ymuno drwy ei ddilyn.",
"about.domain_blocks.silenced.title": "Cyfyngedig", "about.domain_blocks.silenced.title": "Cyfyngedig",
"about.domain_blocks.suspended.explanation": "Ni fydd data o'r gweinydd hwn yn cael ei brosesu, ei gadw na'i gyfnewid, gan wneud unrhyw ryngweithio neu gyfathrebu gyda defnyddwyr o'r gweinydd hwn yn amhosibl.", "about.domain_blocks.suspended.explanation": "Fydd data o'r gweinydd hwn ddim yn cael ei brosesu, ei gadw na'i gyfnewid, gan wneud unrhyw ryngweithio neu gyfathrebu gyda defnyddwyr o'r gweinydd hwn yn amhosibl.",
"about.domain_blocks.suspended.title": "Wedi'i atal", "about.domain_blocks.suspended.title": "Wedi'i atal",
"about.not_available": "Nid yw'r wybodaeth hon ar gael ar y gweinydd hwn.", "about.not_available": "Dyw'r wybodaeth yma heb ei wneud ar gael ar y gweinydd hwn.",
"about.powered_by": "Cyfrwng cymdeithasol datganoledig wedi ei yrru gan {mastodon}", "about.powered_by": "Cyfrwng cymdeithasol datganoledig wedi ei yrru gan {mastodon}",
"about.rules": "Rheolau'r gweinydd", "about.rules": "Rheolau'r gweinydd",
"account.account_note_header": "Nodyn personol", "account.account_note_header": "Nodyn personol",
"account.add_or_remove_from_list": "Ychwanegu neu Ddileu o'r rhestrau", "account.add_or_remove_from_list": "Ychwanegu neu Ddileu o'r rhestrau",
"account.badges.bot": "Awtomataidd", "account.badges.bot": "Awtomataidd",
"account.badges.group": "Grŵp", "account.badges.group": "Grŵp",
"account.block": "Blocio @{name}", "account.block": "Rhwystro @{name}",
"account.block_domain": "Blocio'r parth {domain}", "account.block_domain": "Rhwystro'r parth {domain}",
"account.block_short": "Blocio", "account.block_short": "Rhwystro",
"account.blocked": "Blociwyd", "account.blocked": "Wedi'i rwystro",
"account.blocking": "Yn Rhwystro", "account.blocking": "Yn Rhwystro",
"account.cancel_follow_request": "Tynnu cais i ddilyn", "account.cancel_follow_request": "Tynnu cais i ddilyn",
"account.copy": "Copïo dolen i'r proffil", "account.copy": "Copïo dolen i'r proffil",
@ -28,7 +28,7 @@
"account.edit_profile": "Golygu'r proffil", "account.edit_profile": "Golygu'r proffil",
"account.enable_notifications": "Rhowch wybod i fi pan fydd @{name} yn postio", "account.enable_notifications": "Rhowch wybod i fi pan fydd @{name} yn postio",
"account.endorse": "Dangos ar fy mhroffil", "account.endorse": "Dangos ar fy mhroffil",
"account.featured": "Dethol", "account.featured": "Nodwedd",
"account.featured.hashtags": "Hashnodau", "account.featured.hashtags": "Hashnodau",
"account.featured.posts": "Postiadau", "account.featured.posts": "Postiadau",
"account.featured_tags.last_status_at": "Y postiad olaf ar {date}", "account.featured_tags.last_status_at": "Y postiad olaf ar {date}",
@ -40,7 +40,7 @@
"account.followers_counter": "{count, plural, one {{counter} dilynwr} two {{counter} ddilynwr} other {{counter} dilynwyr}}", "account.followers_counter": "{count, plural, one {{counter} dilynwr} two {{counter} ddilynwr} other {{counter} dilynwyr}}",
"account.following": "Yn dilyn", "account.following": "Yn dilyn",
"account.following_counter": "{count, plural, one {Yn dilyn {counter}} other {Yn dilyn {counter} arall}}", "account.following_counter": "{count, plural, one {Yn dilyn {counter}} other {Yn dilyn {counter} arall}}",
"account.follows.empty": "Nid yw'r defnyddiwr hwn yn dilyn unrhyw un eto.", "account.follows.empty": "Dyw'r defnyddiwr hwn ddim yn dilyn unrhyw un eto.",
"account.follows_you": "Yn eich dilyn chi", "account.follows_you": "Yn eich dilyn chi",
"account.go_to_profile": "Mynd i'r proffil", "account.go_to_profile": "Mynd i'r proffil",
"account.hide_reblogs": "Cuddio hybiau gan @{name}", "account.hide_reblogs": "Cuddio hybiau gan @{name}",
@ -168,7 +168,7 @@
"column.lists": "Rhestrau", "column.lists": "Rhestrau",
"column.mutes": "Defnyddwyr wedi'u tewi", "column.mutes": "Defnyddwyr wedi'u tewi",
"column.notifications": "Hysbysiadau", "column.notifications": "Hysbysiadau",
"column.pins": "Postiadau wedi eu pinio", "column.pins": "Postiadau nodwedd",
"column.public": "Ffrwd y ffederasiwn", "column.public": "Ffrwd y ffederasiwn",
"column_back_button.label": "Nôl", "column_back_button.label": "Nôl",
"column_header.hide_settings": "Cuddio'r dewisiadau", "column_header.hide_settings": "Cuddio'r dewisiadau",
@ -477,7 +477,7 @@
"keyboard_shortcuts.my_profile": "Agor eich proffil", "keyboard_shortcuts.my_profile": "Agor eich proffil",
"keyboard_shortcuts.notifications": "Agor colofn hysbysiadau", "keyboard_shortcuts.notifications": "Agor colofn hysbysiadau",
"keyboard_shortcuts.open_media": "Agor cyfryngau", "keyboard_shortcuts.open_media": "Agor cyfryngau",
"keyboard_shortcuts.pinned": "Agor rhestr postiadau wedi'u pinio", "keyboard_shortcuts.pinned": "Agor rhestr postiadau nodwedd",
"keyboard_shortcuts.profile": "Agor proffil yr awdur", "keyboard_shortcuts.profile": "Agor proffil yr awdur",
"keyboard_shortcuts.reply": "Ymateb i bostiad", "keyboard_shortcuts.reply": "Ymateb i bostiad",
"keyboard_shortcuts.requests": "Agor rhestr ceisiadau dilyn", "keyboard_shortcuts.requests": "Agor rhestr ceisiadau dilyn",
@ -561,7 +561,7 @@
"navigation_bar.mutes": "Defnyddwyr wedi'u tewi", "navigation_bar.mutes": "Defnyddwyr wedi'u tewi",
"navigation_bar.opened_in_classic_interface": "Mae postiadau, cyfrifon a thudalennau penodol eraill yn cael eu hagor fel rhagosodiad yn y rhyngwyneb gwe clasurol.", "navigation_bar.opened_in_classic_interface": "Mae postiadau, cyfrifon a thudalennau penodol eraill yn cael eu hagor fel rhagosodiad yn y rhyngwyneb gwe clasurol.",
"navigation_bar.personal": "Personol", "navigation_bar.personal": "Personol",
"navigation_bar.pins": "Postiadau wedi eu pinio", "navigation_bar.pins": "Postiadau nodwedd",
"navigation_bar.preferences": "Dewisiadau", "navigation_bar.preferences": "Dewisiadau",
"navigation_bar.public_timeline": "Ffrwd y ffederasiwn", "navigation_bar.public_timeline": "Ffrwd y ffederasiwn",
"navigation_bar.search": "Chwilio", "navigation_bar.search": "Chwilio",
@ -857,8 +857,7 @@
"status.mute": "Anwybyddu @{name}", "status.mute": "Anwybyddu @{name}",
"status.mute_conversation": "Anwybyddu sgwrs", "status.mute_conversation": "Anwybyddu sgwrs",
"status.open": "Ehangu'r post hwn", "status.open": "Ehangu'r post hwn",
"status.pin": "Pinio ar y proffil", "status.pin": "Dangos ar y proffil",
"status.pinned": "Postiad wedi'i binio",
"status.read_more": "Darllen rhagor", "status.read_more": "Darllen rhagor",
"status.reblog": "Hybu", "status.reblog": "Hybu",
"status.reblog_private": "Hybu i'r gynulleidfa wreiddiol", "status.reblog_private": "Hybu i'r gynulleidfa wreiddiol",
@ -883,7 +882,7 @@
"status.translated_from_with": "Cyfieithwyd o {lang} gan ddefnyddio {provider}", "status.translated_from_with": "Cyfieithwyd o {lang} gan ddefnyddio {provider}",
"status.uncached_media_warning": "Dim rhagolwg ar gael", "status.uncached_media_warning": "Dim rhagolwg ar gael",
"status.unmute_conversation": "Dad-dewi sgwrs", "status.unmute_conversation": "Dad-dewi sgwrs",
"status.unpin": "Dadbinio o'r proffil", "status.unpin": "Peidio a'i ddangos ar fy mhroffil",
"subscribed_languages.lead": "Dim ond postiadau mewn ieithoedd penodol fydd yn ymddangos yn eich ffrydiau cartref a rhestr ar ôl y newid. Dewiswch ddim byd i dderbyn postiadau ym mhob iaith.", "subscribed_languages.lead": "Dim ond postiadau mewn ieithoedd penodol fydd yn ymddangos yn eich ffrydiau cartref a rhestr ar ôl y newid. Dewiswch ddim byd i dderbyn postiadau ym mhob iaith.",
"subscribed_languages.save": "Cadw'r newidiadau", "subscribed_languages.save": "Cadw'r newidiadau",
"subscribed_languages.target": "Newid ieithoedd tanysgrifio {target}", "subscribed_languages.target": "Newid ieithoedd tanysgrifio {target}",

View File

@ -29,6 +29,7 @@
"account.enable_notifications": "Advisér mig, når @{name} poster", "account.enable_notifications": "Advisér mig, når @{name} poster",
"account.endorse": "Fremhæv på profil", "account.endorse": "Fremhæv på profil",
"account.featured": "Fremhævet", "account.featured": "Fremhævet",
"account.featured.accounts": "Profiler",
"account.featured.hashtags": "Hashtags", "account.featured.hashtags": "Hashtags",
"account.featured.posts": "Indlæg", "account.featured.posts": "Indlæg",
"account.featured_tags.last_status_at": "Seneste indlæg {date}", "account.featured_tags.last_status_at": "Seneste indlæg {date}",
@ -168,7 +169,7 @@
"column.lists": "Lister", "column.lists": "Lister",
"column.mutes": "Skjulte brugere", "column.mutes": "Skjulte brugere",
"column.notifications": "Notifikationer", "column.notifications": "Notifikationer",
"column.pins": "Fastgjorte indlæg", "column.pins": "Fremhævede indlæg",
"column.public": "Fælles tidslinje", "column.public": "Fælles tidslinje",
"column_back_button.label": "Tilbage", "column_back_button.label": "Tilbage",
"column_header.hide_settings": "Skjul indstillinger", "column_header.hide_settings": "Skjul indstillinger",
@ -405,8 +406,10 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} deltager} other {{counter} deltagere}}", "hashtag.counter_by_accounts": "{count, plural, one {{counter} deltager} other {{counter} deltagere}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}} i dag", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}} i dag",
"hashtag.feature": "Fremhæv på profil",
"hashtag.follow": "Følg etiket", "hashtag.follow": "Følg etiket",
"hashtag.mute": "Tavsgør #{hashtag}", "hashtag.mute": "Tavsgør #{hashtag}",
"hashtag.unfeature": "Fremhæv ikke på profil",
"hashtag.unfollow": "Stop med at følge etiket", "hashtag.unfollow": "Stop med at følge etiket",
"hashtags.and_other": "…og {count, plural, one {}other {# flere}}", "hashtags.and_other": "…og {count, plural, one {}other {# flere}}",
"hints.profiles.followers_may_be_missing": "Der kan mangle følgere for denne profil.", "hints.profiles.followers_may_be_missing": "Der kan mangle følgere for denne profil.",
@ -477,7 +480,7 @@
"keyboard_shortcuts.my_profile": "Åbn din profil", "keyboard_shortcuts.my_profile": "Åbn din profil",
"keyboard_shortcuts.notifications": "for at åbne notifikationskolonnen", "keyboard_shortcuts.notifications": "for at åbne notifikationskolonnen",
"keyboard_shortcuts.open_media": "Åbn medier", "keyboard_shortcuts.open_media": "Åbn medier",
"keyboard_shortcuts.pinned": "Åbn liste over fastgjorte indlæg", "keyboard_shortcuts.pinned": "Åbn liste over fremhævede indlæg",
"keyboard_shortcuts.profile": "Åbn forfatters profil", "keyboard_shortcuts.profile": "Åbn forfatters profil",
"keyboard_shortcuts.reply": "Besvar indlægget", "keyboard_shortcuts.reply": "Besvar indlægget",
"keyboard_shortcuts.requests": "Åbn liste over følgeanmodninger", "keyboard_shortcuts.requests": "Åbn liste over følgeanmodninger",
@ -561,7 +564,7 @@
"navigation_bar.mutes": "Skjulte brugere", "navigation_bar.mutes": "Skjulte brugere",
"navigation_bar.opened_in_classic_interface": "Indlæg, konti og visse andre sider åbnes som standard i den klassiske webgrænseflade.", "navigation_bar.opened_in_classic_interface": "Indlæg, konti og visse andre sider åbnes som standard i den klassiske webgrænseflade.",
"navigation_bar.personal": "Personlig", "navigation_bar.personal": "Personlig",
"navigation_bar.pins": "Fastgjorte indlæg", "navigation_bar.pins": "Fremhævede indlæg",
"navigation_bar.preferences": "Præferencer", "navigation_bar.preferences": "Præferencer",
"navigation_bar.public_timeline": "Fælles tidslinje", "navigation_bar.public_timeline": "Fælles tidslinje",
"navigation_bar.search": "Søg", "navigation_bar.search": "Søg",
@ -857,8 +860,7 @@
"status.mute": "Skjul @{name}", "status.mute": "Skjul @{name}",
"status.mute_conversation": "Skjul samtale", "status.mute_conversation": "Skjul samtale",
"status.open": "Udvid dette indlæg", "status.open": "Udvid dette indlæg",
"status.pin": "Fastgør til profil", "status.pin": "Fremhæv på profil",
"status.pinned": "Fastgjort indlæg",
"status.read_more": "Læs mere", "status.read_more": "Læs mere",
"status.reblog": "Fremhæv", "status.reblog": "Fremhæv",
"status.reblog_private": "Fremhæv med oprindelig synlighed", "status.reblog_private": "Fremhæv med oprindelig synlighed",
@ -883,7 +885,7 @@
"status.translated_from_with": "Oversat fra {lang} ved brug af {provider}", "status.translated_from_with": "Oversat fra {lang} ved brug af {provider}",
"status.uncached_media_warning": "Ingen forhåndsvisning", "status.uncached_media_warning": "Ingen forhåndsvisning",
"status.unmute_conversation": "Genaktivér samtale", "status.unmute_conversation": "Genaktivér samtale",
"status.unpin": "Frigør fra profil", "status.unpin": "Fremhæv ikke på profil",
"subscribed_languages.lead": "Kun indlæg på udvalgte sprog vil fremgå på dine hjemme- og listetidslinjer efter ændringen. Vælg ingen for at modtage indlæg på alle sprog.", "subscribed_languages.lead": "Kun indlæg på udvalgte sprog vil fremgå på dine hjemme- og listetidslinjer efter ændringen. Vælg ingen for at modtage indlæg på alle sprog.",
"subscribed_languages.save": "Gem ændringer", "subscribed_languages.save": "Gem ændringer",
"subscribed_languages.target": "Skift abonnementssprog for {target}", "subscribed_languages.target": "Skift abonnementssprog for {target}",

View File

@ -27,8 +27,9 @@
"account.domain_blocking": "Domain blockiert", "account.domain_blocking": "Domain blockiert",
"account.edit_profile": "Profil bearbeiten", "account.edit_profile": "Profil bearbeiten",
"account.enable_notifications": "Benachrichtige mich wenn @{name} etwas postet", "account.enable_notifications": "Benachrichtige mich wenn @{name} etwas postet",
"account.endorse": "Im Profil empfehlen", "account.endorse": "Im Profil vorstellen",
"account.featured": "Vorgestellt", "account.featured": "Vorgestellt",
"account.featured.accounts": "Profile",
"account.featured.hashtags": "Hashtags", "account.featured.hashtags": "Hashtags",
"account.featured.posts": "Beiträge", "account.featured.posts": "Beiträge",
"account.featured_tags.last_status_at": "Letzter Beitrag am {date}", "account.featured_tags.last_status_at": "Letzter Beitrag am {date}",
@ -74,7 +75,7 @@
"account.unblock_domain": "Blockierung von {domain} aufheben", "account.unblock_domain": "Blockierung von {domain} aufheben",
"account.unblock_domain_short": "Entsperren", "account.unblock_domain_short": "Entsperren",
"account.unblock_short": "Blockierung aufheben", "account.unblock_short": "Blockierung aufheben",
"account.unendorse": "Im Profil nicht mehr empfehlen", "account.unendorse": "Im Profil nicht mehr vorstellen",
"account.unfollow": "Entfolgen", "account.unfollow": "Entfolgen",
"account.unmute": "Stummschaltung von @{name} aufheben", "account.unmute": "Stummschaltung von @{name} aufheben",
"account.unmute_notifications_short": "Stummschaltung der Benachrichtigungen aufheben", "account.unmute_notifications_short": "Stummschaltung der Benachrichtigungen aufheben",
@ -168,7 +169,7 @@
"column.lists": "Listen", "column.lists": "Listen",
"column.mutes": "Stummgeschaltete Profile", "column.mutes": "Stummgeschaltete Profile",
"column.notifications": "Benachrichtigungen", "column.notifications": "Benachrichtigungen",
"column.pins": "Angeheftete Beiträge", "column.pins": "Vorgestellte Beiträge",
"column.public": "Föderierte Timeline", "column.public": "Föderierte Timeline",
"column_back_button.label": "Zurück", "column_back_button.label": "Zurück",
"column_header.hide_settings": "Einstellungen ausblenden", "column_header.hide_settings": "Einstellungen ausblenden",
@ -405,8 +406,10 @@
"hashtag.counter_by_accounts": "{count, plural, one{{counter} Beteiligte*r} other{{counter} Beteiligte}}", "hashtag.counter_by_accounts": "{count, plural, one{{counter} Beteiligte*r} other{{counter} Beteiligte}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}} heute", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}} heute",
"hashtag.feature": "Im Profil vorstellen",
"hashtag.follow": "Hashtag folgen", "hashtag.follow": "Hashtag folgen",
"hashtag.mute": "#{hashtag} stummschalten", "hashtag.mute": "#{hashtag} stummschalten",
"hashtag.unfeature": "Im Profil nicht mehr vorstellen",
"hashtag.unfollow": "Hashtag entfolgen", "hashtag.unfollow": "Hashtag entfolgen",
"hashtags.and_other": "… und {count, plural, one{# weiterer} other {# weitere}}", "hashtags.and_other": "… und {count, plural, one{# weiterer} other {# weitere}}",
"hints.profiles.followers_may_be_missing": "Möglicherweise werden für dieses Profil nicht alle Follower angezeigt.", "hints.profiles.followers_may_be_missing": "Möglicherweise werden für dieses Profil nicht alle Follower angezeigt.",
@ -426,7 +429,7 @@
"home.show_announcements": "Ankündigungen anzeigen", "home.show_announcements": "Ankündigungen anzeigen",
"ignore_notifications_modal.disclaimer": "Mastodon kann anderen Nutzer*innen nicht mitteilen, dass du deren Benachrichtigungen ignorierst. Das Ignorieren von Benachrichtigungen wird nicht das Absenden der Nachricht selbst unterbinden.", "ignore_notifications_modal.disclaimer": "Mastodon kann anderen Nutzer*innen nicht mitteilen, dass du deren Benachrichtigungen ignorierst. Das Ignorieren von Benachrichtigungen wird nicht das Absenden der Nachricht selbst unterbinden.",
"ignore_notifications_modal.filter_instead": "Stattdessen filtern", "ignore_notifications_modal.filter_instead": "Stattdessen filtern",
"ignore_notifications_modal.filter_to_act_users": "Du wirst weiterhin die Möglichkeit haben, andere Nutzer*innen zu genehmigen, abzulehnen oder zu melden", "ignore_notifications_modal.filter_to_act_users": "Du wirst weiterhin die Möglichkeit haben, andere Nutzer*innen zu akzeptieren, abzulehnen oder zu melden",
"ignore_notifications_modal.filter_to_avoid_confusion": "Filtern hilft, mögliches Durcheinander zu vermeiden", "ignore_notifications_modal.filter_to_avoid_confusion": "Filtern hilft, mögliches Durcheinander zu vermeiden",
"ignore_notifications_modal.filter_to_review_separately": "Gefilterte Benachrichtigungen können separat überprüft werden", "ignore_notifications_modal.filter_to_review_separately": "Gefilterte Benachrichtigungen können separat überprüft werden",
"ignore_notifications_modal.ignore": "Benachrichtigungen ignorieren", "ignore_notifications_modal.ignore": "Benachrichtigungen ignorieren",
@ -477,7 +480,7 @@
"keyboard_shortcuts.my_profile": "Eigenes Profil aufrufen", "keyboard_shortcuts.my_profile": "Eigenes Profil aufrufen",
"keyboard_shortcuts.notifications": "Benachrichtigungen aufrufen", "keyboard_shortcuts.notifications": "Benachrichtigungen aufrufen",
"keyboard_shortcuts.open_media": "Medieninhalt öffnen", "keyboard_shortcuts.open_media": "Medieninhalt öffnen",
"keyboard_shortcuts.pinned": "Liste angehefteter Beiträge öffnen", "keyboard_shortcuts.pinned": "Liste vorgestellter Beiträge öffnen",
"keyboard_shortcuts.profile": "Profil aufrufen", "keyboard_shortcuts.profile": "Profil aufrufen",
"keyboard_shortcuts.reply": "Auf Beitrag antworten", "keyboard_shortcuts.reply": "Auf Beitrag antworten",
"keyboard_shortcuts.requests": "Liste der Follower-Anfragen aufrufen", "keyboard_shortcuts.requests": "Liste der Follower-Anfragen aufrufen",
@ -561,7 +564,7 @@
"navigation_bar.mutes": "Stummgeschaltete Profile", "navigation_bar.mutes": "Stummgeschaltete Profile",
"navigation_bar.opened_in_classic_interface": "Beiträge, Konten und andere bestimmte Seiten werden standardmäßig im klassischen Webinterface geöffnet.", "navigation_bar.opened_in_classic_interface": "Beiträge, Konten und andere bestimmte Seiten werden standardmäßig im klassischen Webinterface geöffnet.",
"navigation_bar.personal": "Persönlich", "navigation_bar.personal": "Persönlich",
"navigation_bar.pins": "Angeheftete Beiträge", "navigation_bar.pins": "Vorgestellte Beiträge",
"navigation_bar.preferences": "Einstellungen", "navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Föderierte Timeline", "navigation_bar.public_timeline": "Föderierte Timeline",
"navigation_bar.search": "Suche", "navigation_bar.search": "Suche",
@ -610,11 +613,11 @@
"notification.relationships_severance_event.user_domain_block": "Du hast {target} blockiert {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst, wurden entfernt.", "notification.relationships_severance_event.user_domain_block": "Du hast {target} blockiert {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst, wurden entfernt.",
"notification.status": "{name} postete …", "notification.status": "{name} postete …",
"notification.update": "{name} bearbeitete einen Beitrag", "notification.update": "{name} bearbeitete einen Beitrag",
"notification_requests.accept": "Genehmigen", "notification_requests.accept": "Akzeptieren",
"notification_requests.accept_multiple": "{count, plural, one {# Anfrage genehmigen …} other {# Anfragen genehmigen …}}", "notification_requests.accept_multiple": "{count, plural, one {# Anfrage akzeptieren …} other {# Anfragen akzeptieren …}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Anfrage genehmigen} other {Anfragen genehmigen}}", "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Anfrage akzeptieren} other {Anfragen akzeptieren}}",
"notification_requests.confirm_accept_multiple.message": "Du bist dabei, {{count, plural, one {eine Benachrichtigungsanfrage} other {# Benachrichtigungsanfragen}} zu genehmigen. Möchtest du wirklich fortfahren?", "notification_requests.confirm_accept_multiple.message": "Du bist dabei, {{count, plural, one {eine Benachrichtigungsanfrage} other {# Benachrichtigungsanfragen}} zu akzeptieren. Möchtest du wirklich fortfahren?",
"notification_requests.confirm_accept_multiple.title": "Benachrichtigungsanfragen genehmigen?", "notification_requests.confirm_accept_multiple.title": "Benachrichtigungsanfragen akzeptieren?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Anfrage ablehnen} other {Anfragen ablehnen}}", "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Anfrage ablehnen} other {Anfragen ablehnen}}",
"notification_requests.confirm_dismiss_multiple.message": "Du bist dabei, {count, plural, one {eine Benachrichtigungsanfrage} other {# Benachrichtigungsanfragen}} abzulehnen. Du wirst nicht mehr ohne Weiteres auf {count, plural, one {sie} other {sie}} zugreifen können. Möchtest du wirklich fortfahren?", "notification_requests.confirm_dismiss_multiple.message": "Du bist dabei, {count, plural, one {eine Benachrichtigungsanfrage} other {# Benachrichtigungsanfragen}} abzulehnen. Du wirst nicht mehr ohne Weiteres auf {count, plural, one {sie} other {sie}} zugreifen können. Möchtest du wirklich fortfahren?",
"notification_requests.confirm_dismiss_multiple.title": "Benachrichtigungsanfragen ablehnen?", "notification_requests.confirm_dismiss_multiple.title": "Benachrichtigungsanfragen ablehnen?",
@ -857,8 +860,7 @@
"status.mute": "@{name} stummschalten", "status.mute": "@{name} stummschalten",
"status.mute_conversation": "Unterhaltung stummschalten", "status.mute_conversation": "Unterhaltung stummschalten",
"status.open": "Beitrag öffnen", "status.open": "Beitrag öffnen",
"status.pin": "Im Profil anheften", "status.pin": "Im Profil vorstellen",
"status.pinned": "Angehefteter Beitrag",
"status.read_more": "Gesamten Beitrag anschauen", "status.read_more": "Gesamten Beitrag anschauen",
"status.reblog": "Teilen", "status.reblog": "Teilen",
"status.reblog_private": "Mit der ursprünglichen Zielgruppe teilen", "status.reblog_private": "Mit der ursprünglichen Zielgruppe teilen",
@ -883,7 +885,7 @@
"status.translated_from_with": "Aus {lang} mittels {provider} übersetzt", "status.translated_from_with": "Aus {lang} mittels {provider} übersetzt",
"status.uncached_media_warning": "Vorschau nicht verfügbar", "status.uncached_media_warning": "Vorschau nicht verfügbar",
"status.unmute_conversation": "Stummschaltung der Unterhaltung aufheben", "status.unmute_conversation": "Stummschaltung der Unterhaltung aufheben",
"status.unpin": "Vom Profil lösen", "status.unpin": "Im Profil nicht mehr vorstellen",
"subscribed_languages.lead": "Nach der Änderung werden nur noch Beiträge in den ausgewählten Sprachen in den Timelines deiner Startseite und deiner Listen angezeigt. Wähle keine Sprache aus, um alle Beiträge zu sehen.", "subscribed_languages.lead": "Nach der Änderung werden nur noch Beiträge in den ausgewählten Sprachen in den Timelines deiner Startseite und deiner Listen angezeigt. Wähle keine Sprache aus, um alle Beiträge zu sehen.",
"subscribed_languages.save": "Änderungen speichern", "subscribed_languages.save": "Änderungen speichern",
"subscribed_languages.target": "Abonnierte Sprachen für {target} ändern", "subscribed_languages.target": "Abonnierte Sprachen für {target} ändern",

View File

@ -161,7 +161,6 @@
"column.lists": "Λίστες", "column.lists": "Λίστες",
"column.mutes": "Αποσιωπημένοι χρήστες", "column.mutes": "Αποσιωπημένοι χρήστες",
"column.notifications": "Ειδοποιήσεις", "column.notifications": "Ειδοποιήσεις",
"column.pins": "Καρφιτσωμένα τουτ",
"column.public": "Ομοσπονδιακή ροή", "column.public": "Ομοσπονδιακή ροή",
"column_back_button.label": "Πίσω", "column_back_button.label": "Πίσω",
"column_header.hide_settings": "Απόκρυψη ρυθμίσεων", "column_header.hide_settings": "Απόκρυψη ρυθμίσεων",
@ -464,7 +463,6 @@
"keyboard_shortcuts.my_profile": "Άνοιγμα του προφίλ σου", "keyboard_shortcuts.my_profile": "Άνοιγμα του προφίλ σου",
"keyboard_shortcuts.notifications": "Άνοιγμα στήλης ειδοποιήσεων", "keyboard_shortcuts.notifications": "Άνοιγμα στήλης ειδοποιήσεων",
"keyboard_shortcuts.open_media": "Άνοιγμα πολυμέσων", "keyboard_shortcuts.open_media": "Άνοιγμα πολυμέσων",
"keyboard_shortcuts.pinned": "Άνοιγμα λίστας καρφιτσωμένων αναρτήσεων",
"keyboard_shortcuts.profile": "Άνοιγμα προφίλ συγγραφέα", "keyboard_shortcuts.profile": "Άνοιγμα προφίλ συγγραφέα",
"keyboard_shortcuts.reply": "Απάντηση στην ανάρτηση", "keyboard_shortcuts.reply": "Απάντηση στην ανάρτηση",
"keyboard_shortcuts.requests": "Άνοιγμα λίστας αιτημάτων ακολούθησης", "keyboard_shortcuts.requests": "Άνοιγμα λίστας αιτημάτων ακολούθησης",
@ -548,7 +546,6 @@
"navigation_bar.mutes": "Αποσιωπημένοι χρήστες", "navigation_bar.mutes": "Αποσιωπημένοι χρήστες",
"navigation_bar.opened_in_classic_interface": "Δημοσιεύσεις, λογαριασμοί και άλλες συγκεκριμένες σελίδες ανοίγονται από προεπιλογή στην κλασική διεπαφή ιστού.", "navigation_bar.opened_in_classic_interface": "Δημοσιεύσεις, λογαριασμοί και άλλες συγκεκριμένες σελίδες ανοίγονται από προεπιλογή στην κλασική διεπαφή ιστού.",
"navigation_bar.personal": "Προσωπικά", "navigation_bar.personal": "Προσωπικά",
"navigation_bar.pins": "Καρφιτσωμένες αναρτήσεις",
"navigation_bar.preferences": "Προτιμήσεις", "navigation_bar.preferences": "Προτιμήσεις",
"navigation_bar.public_timeline": "Ροή συναλλαγών", "navigation_bar.public_timeline": "Ροή συναλλαγών",
"navigation_bar.search": "Αναζήτηση", "navigation_bar.search": "Αναζήτηση",
@ -844,8 +841,6 @@
"status.mute": "Σίγαση σε @{name}", "status.mute": "Σίγαση σε @{name}",
"status.mute_conversation": "Σίγαση συνομιλίας", "status.mute_conversation": "Σίγαση συνομιλίας",
"status.open": "Επέκταση ανάρτησης", "status.open": "Επέκταση ανάρτησης",
"status.pin": "Καρφίτσωσε στο προφίλ",
"status.pinned": "Καρφιτσωμένη ανάρτηση",
"status.read_more": "Διάβασε περισότερα", "status.read_more": "Διάβασε περισότερα",
"status.reblog": "Ενίσχυση", "status.reblog": "Ενίσχυση",
"status.reblog_private": "Ενίσχυση με αρχική ορατότητα", "status.reblog_private": "Ενίσχυση με αρχική ορατότητα",
@ -870,7 +865,6 @@
"status.translated_from_with": "Μεταφράστηκε από {lang} χρησιμοποιώντας {provider}", "status.translated_from_with": "Μεταφράστηκε από {lang} χρησιμοποιώντας {provider}",
"status.uncached_media_warning": "Μη διαθέσιμη προεπισκόπηση", "status.uncached_media_warning": "Μη διαθέσιμη προεπισκόπηση",
"status.unmute_conversation": "Αναίρεση σίγασης συνομιλίας", "status.unmute_conversation": "Αναίρεση σίγασης συνομιλίας",
"status.unpin": "Ξεκαρφίτσωσε από το προφίλ",
"subscribed_languages.lead": "Μόνο αναρτήσεις σε επιλεγμένες γλώσσες θα εμφανίζονται στην αρχική σου και θα παραθέτονται χρονοδιαγράμματα μετά την αλλαγή. Επέλεξε καμία για να λαμβάνεις αναρτήσεις σε όλες τις γλώσσες.", "subscribed_languages.lead": "Μόνο αναρτήσεις σε επιλεγμένες γλώσσες θα εμφανίζονται στην αρχική σου και θα παραθέτονται χρονοδιαγράμματα μετά την αλλαγή. Επέλεξε καμία για να λαμβάνεις αναρτήσεις σε όλες τις γλώσσες.",
"subscribed_languages.save": "Αποθήκευση αλλαγών", "subscribed_languages.save": "Αποθήκευση αλλαγών",
"subscribed_languages.target": "Αλλαγή εγγεγραμμένων γλωσσών για {target}", "subscribed_languages.target": "Αλλαγή εγγεγραμμένων γλωσσών για {target}",

View File

@ -157,7 +157,6 @@
"column.lists": "Lists", "column.lists": "Lists",
"column.mutes": "Muted users", "column.mutes": "Muted users",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.pins": "Pinned posts",
"column.public": "Federated timeline", "column.public": "Federated timeline",
"column_back_button.label": "Back", "column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -457,7 +456,6 @@
"keyboard_shortcuts.my_profile": "to open your profile", "keyboard_shortcuts.my_profile": "to open your profile",
"keyboard_shortcuts.notifications": "Open notifications column", "keyboard_shortcuts.notifications": "Open notifications column",
"keyboard_shortcuts.open_media": "to open media", "keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.pinned": "to open pinned posts list",
"keyboard_shortcuts.profile": "to open author's profile", "keyboard_shortcuts.profile": "to open author's profile",
"keyboard_shortcuts.reply": "to reply", "keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list", "keyboard_shortcuts.requests": "to open follow requests list",
@ -541,7 +539,6 @@
"navigation_bar.mutes": "Muted users", "navigation_bar.mutes": "Muted users",
"navigation_bar.opened_in_classic_interface": "Posts, accounts, and other specific pages are opened by default in the classic web interface.", "navigation_bar.opened_in_classic_interface": "Posts, accounts, and other specific pages are opened by default in the classic web interface.",
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Personal",
"navigation_bar.pins": "Pinned posts",
"navigation_bar.preferences": "Preferences", "navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.search": "Search", "navigation_bar.search": "Search",
@ -837,8 +834,6 @@
"status.mute": "Mute @{name}", "status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expand this post", "status.open": "Expand this post",
"status.pin": "Pin on profile",
"status.pinned": "Pinned post",
"status.read_more": "Read more", "status.read_more": "Read more",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility", "status.reblog_private": "Boost with original visibility",
@ -863,7 +858,6 @@
"status.translated_from_with": "Translated from {lang} using {provider}", "status.translated_from_with": "Translated from {lang} using {provider}",
"status.uncached_media_warning": "Preview not available", "status.uncached_media_warning": "Preview not available",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.", "subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
"subscribed_languages.save": "Save changes", "subscribed_languages.save": "Save changes",
"subscribed_languages.target": "Change subscribed languages for {target}", "subscribed_languages.target": "Change subscribed languages for {target}",

View File

@ -29,6 +29,7 @@
"account.enable_notifications": "Notify me when @{name} posts", "account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Feature on profile", "account.endorse": "Feature on profile",
"account.featured": "Featured", "account.featured": "Featured",
"account.featured.accounts": "Profiles",
"account.featured.hashtags": "Hashtags", "account.featured.hashtags": "Hashtags",
"account.featured.posts": "Posts", "account.featured.posts": "Posts",
"account.featured_tags.last_status_at": "Last post on {date}", "account.featured_tags.last_status_at": "Last post on {date}",
@ -168,7 +169,7 @@
"column.lists": "Lists", "column.lists": "Lists",
"column.mutes": "Muted users", "column.mutes": "Muted users",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.pins": "Pinned posts", "column.pins": "Featured posts",
"column.public": "Federated timeline", "column.public": "Federated timeline",
"column_back_button.label": "Back", "column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -405,8 +406,10 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participant} other {{counter} participants}}", "hashtag.counter_by_accounts": "{count, plural, one {{counter} participant} other {{counter} participants}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} post} other {{counter} posts}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} post} other {{counter} posts}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today",
"hashtag.feature": "Feature on profile",
"hashtag.follow": "Follow hashtag", "hashtag.follow": "Follow hashtag",
"hashtag.mute": "Mute #{hashtag}", "hashtag.mute": "Mute #{hashtag}",
"hashtag.unfeature": "Don't feature on profile",
"hashtag.unfollow": "Unfollow hashtag", "hashtag.unfollow": "Unfollow hashtag",
"hashtags.and_other": "…and {count, plural, other {# more}}", "hashtags.and_other": "…and {count, plural, other {# more}}",
"hints.profiles.followers_may_be_missing": "Followers for this profile may be missing.", "hints.profiles.followers_may_be_missing": "Followers for this profile may be missing.",
@ -477,7 +480,7 @@
"keyboard_shortcuts.my_profile": "Open your profile", "keyboard_shortcuts.my_profile": "Open your profile",
"keyboard_shortcuts.notifications": "Open notifications column", "keyboard_shortcuts.notifications": "Open notifications column",
"keyboard_shortcuts.open_media": "Open media", "keyboard_shortcuts.open_media": "Open media",
"keyboard_shortcuts.pinned": "Open pinned posts list", "keyboard_shortcuts.pinned": "Open featured posts list",
"keyboard_shortcuts.profile": "Open author's profile", "keyboard_shortcuts.profile": "Open author's profile",
"keyboard_shortcuts.reply": "Reply to post", "keyboard_shortcuts.reply": "Reply to post",
"keyboard_shortcuts.requests": "Open follow requests list", "keyboard_shortcuts.requests": "Open follow requests list",
@ -561,7 +564,7 @@
"navigation_bar.mutes": "Muted users", "navigation_bar.mutes": "Muted users",
"navigation_bar.opened_in_classic_interface": "Posts, accounts, and other specific pages are opened by default in the classic web interface.", "navigation_bar.opened_in_classic_interface": "Posts, accounts, and other specific pages are opened by default in the classic web interface.",
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Personal",
"navigation_bar.pins": "Pinned posts", "navigation_bar.pins": "Featured posts",
"navigation_bar.preferences": "Preferences", "navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.search": "Search", "navigation_bar.search": "Search",
@ -857,8 +860,7 @@
"status.mute": "Mute @{name}", "status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expand this post", "status.open": "Expand this post",
"status.pin": "Pin on profile", "status.pin": "Feature on profile",
"status.pinned": "Pinned post",
"status.read_more": "Read more", "status.read_more": "Read more",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility", "status.reblog_private": "Boost with original visibility",
@ -883,7 +885,7 @@
"status.translated_from_with": "Translated from {lang} using {provider}", "status.translated_from_with": "Translated from {lang} using {provider}",
"status.uncached_media_warning": "Preview not available", "status.uncached_media_warning": "Preview not available",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile", "status.unpin": "Don't feature on profile",
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.", "subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
"subscribed_languages.save": "Save changes", "subscribed_languages.save": "Save changes",
"subscribed_languages.target": "Change subscribed languages for {target}", "subscribed_languages.target": "Change subscribed languages for {target}",

View File

@ -26,7 +26,7 @@
"account.disable_notifications": "Ĉesu sciigi min kiam @{name} afiŝas", "account.disable_notifications": "Ĉesu sciigi min kiam @{name} afiŝas",
"account.edit_profile": "Redakti la profilon", "account.edit_profile": "Redakti la profilon",
"account.enable_notifications": "Sciigu min kiam @{name} afiŝos", "account.enable_notifications": "Sciigu min kiam @{name} afiŝos",
"account.endorse": "Prezenti ĉe via profilo", "account.endorse": "Montri en profilo",
"account.featured.hashtags": "Kradvortoj", "account.featured.hashtags": "Kradvortoj",
"account.featured.posts": "Afiŝoj", "account.featured.posts": "Afiŝoj",
"account.featured_tags.last_status_at": "Lasta afîŝo je {date}", "account.featured_tags.last_status_at": "Lasta afîŝo je {date}",
@ -162,7 +162,6 @@
"column.lists": "Listoj", "column.lists": "Listoj",
"column.mutes": "Silentigitaj uzantoj", "column.mutes": "Silentigitaj uzantoj",
"column.notifications": "Sciigoj", "column.notifications": "Sciigoj",
"column.pins": "Alpinglitaj afiŝoj",
"column.public": "Fratara templinio", "column.public": "Fratara templinio",
"column_back_button.label": "Reveni", "column_back_button.label": "Reveni",
"column_header.hide_settings": "Kaŝi la agordojn", "column_header.hide_settings": "Kaŝi la agordojn",
@ -462,7 +461,6 @@
"keyboard_shortcuts.my_profile": "Malfermu vian profilon", "keyboard_shortcuts.my_profile": "Malfermu vian profilon",
"keyboard_shortcuts.notifications": "Malfermu la sciigajn kolumnon", "keyboard_shortcuts.notifications": "Malfermu la sciigajn kolumnon",
"keyboard_shortcuts.open_media": "Malfermi vidaŭdaĵon", "keyboard_shortcuts.open_media": "Malfermi vidaŭdaĵon",
"keyboard_shortcuts.pinned": "Malfermu alpinglitajn afiŝojn-liston",
"keyboard_shortcuts.profile": "Malfermu la profilon de aŭtoroprofilo", "keyboard_shortcuts.profile": "Malfermu la profilon de aŭtoroprofilo",
"keyboard_shortcuts.reply": "Respondu al afiŝo", "keyboard_shortcuts.reply": "Respondu al afiŝo",
"keyboard_shortcuts.requests": "Malfermi la liston de petoj por sekvado", "keyboard_shortcuts.requests": "Malfermi la liston de petoj por sekvado",
@ -546,7 +544,6 @@
"navigation_bar.mutes": "Silentigitaj uzantoj", "navigation_bar.mutes": "Silentigitaj uzantoj",
"navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.", "navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.",
"navigation_bar.personal": "Persone", "navigation_bar.personal": "Persone",
"navigation_bar.pins": "Alpinglitaj afiŝoj",
"navigation_bar.preferences": "Preferoj", "navigation_bar.preferences": "Preferoj",
"navigation_bar.public_timeline": "Fratara templinio", "navigation_bar.public_timeline": "Fratara templinio",
"navigation_bar.search": "Serĉi", "navigation_bar.search": "Serĉi",
@ -842,8 +839,6 @@
"status.mute": "Silentigi @{name}", "status.mute": "Silentigi @{name}",
"status.mute_conversation": "Silentigi konversacion", "status.mute_conversation": "Silentigi konversacion",
"status.open": "Pligrandigu ĉi tiun afiŝon", "status.open": "Pligrandigu ĉi tiun afiŝon",
"status.pin": "Alpingli al la profilo",
"status.pinned": "Alpinglita afiŝo",
"status.read_more": "Legi pli", "status.read_more": "Legi pli",
"status.reblog": "Diskonigi", "status.reblog": "Diskonigi",
"status.reblog_private": "Diskonigi kun la sama videbleco", "status.reblog_private": "Diskonigi kun la sama videbleco",
@ -868,7 +863,6 @@
"status.translated_from_with": "Tradukita el {lang} per {provider}", "status.translated_from_with": "Tradukita el {lang} per {provider}",
"status.uncached_media_warning": "Antaŭrigardo ne disponebla", "status.uncached_media_warning": "Antaŭrigardo ne disponebla",
"status.unmute_conversation": "Malsilentigi la konversacion", "status.unmute_conversation": "Malsilentigi la konversacion",
"status.unpin": "Depingli de profilo",
"subscribed_languages.lead": "Nur afiŝoj en elektitaj lingvoj aperos en viaj hejma kaj lista templinioj post la ŝanĝo. Elektu nenion por ricevi afiŝojn en ĉiuj lingvoj.", "subscribed_languages.lead": "Nur afiŝoj en elektitaj lingvoj aperos en viaj hejma kaj lista templinioj post la ŝanĝo. Elektu nenion por ricevi afiŝojn en ĉiuj lingvoj.",
"subscribed_languages.save": "Konservi ŝanĝojn", "subscribed_languages.save": "Konservi ŝanĝojn",
"subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}", "subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}",

View File

@ -24,11 +24,12 @@
"account.copy": "Copiar enlace al perfil", "account.copy": "Copiar enlace al perfil",
"account.direct": "Mención privada a @{name}", "account.direct": "Mención privada a @{name}",
"account.disable_notifications": "Dejar de notificarme cuando @{name} envíe mensajes", "account.disable_notifications": "Dejar de notificarme cuando @{name} envíe mensajes",
"account.domain_blocking": "Bloqueando dominio", "account.domain_blocking": "Dominio bloqueado",
"account.edit_profile": "Editar perfil", "account.edit_profile": "Editar perfil",
"account.enable_notifications": "Notificarme cuando @{name} envíe mensajes", "account.enable_notifications": "Notificarme cuando @{name} envíe mensajes",
"account.endorse": "Destacar en el perfil", "account.endorse": "Destacar en el perfil",
"account.featured": "Destacados", "account.featured": "Destacados",
"account.featured.accounts": "Perfiles",
"account.featured.hashtags": "Etiquetas", "account.featured.hashtags": "Etiquetas",
"account.featured.posts": "Mensajes", "account.featured.posts": "Mensajes",
"account.featured_tags.last_status_at": "Último mensaje: {date}", "account.featured_tags.last_status_at": "Último mensaje: {date}",
@ -168,7 +169,7 @@
"column.lists": "Listas", "column.lists": "Listas",
"column.mutes": "Usuarios silenciados", "column.mutes": "Usuarios silenciados",
"column.notifications": "Notificaciones", "column.notifications": "Notificaciones",
"column.pins": "Mensajes fijados", "column.pins": "Mensajes destacados",
"column.public": "Línea temporal federada", "column.public": "Línea temporal federada",
"column_back_button.label": "Volver", "column_back_button.label": "Volver",
"column_header.hide_settings": "Ocultar configuración", "column_header.hide_settings": "Ocultar configuración",
@ -405,8 +406,10 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}", "hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}} hoy", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}} hoy",
"hashtag.feature": "Destacar en el perfil",
"hashtag.follow": "Seguir etiqueta", "hashtag.follow": "Seguir etiqueta",
"hashtag.mute": "Silenciar #{hashtag}", "hashtag.mute": "Silenciar #{hashtag}",
"hashtag.unfeature": "No destacar en el perfil",
"hashtag.unfollow": "Dejar de seguir etiqueta", "hashtag.unfollow": "Dejar de seguir etiqueta",
"hashtags.and_other": "…y {count, plural, other {# más}}", "hashtags.and_other": "…y {count, plural, other {# más}}",
"hints.profiles.followers_may_be_missing": "Es posible que falten seguidores de este perfil.", "hints.profiles.followers_may_be_missing": "Es posible que falten seguidores de este perfil.",
@ -477,7 +480,7 @@
"keyboard_shortcuts.my_profile": "Abrir tu perfil", "keyboard_shortcuts.my_profile": "Abrir tu perfil",
"keyboard_shortcuts.notifications": "Abrir columna de notificaciones", "keyboard_shortcuts.notifications": "Abrir columna de notificaciones",
"keyboard_shortcuts.open_media": "Abrir archivos de medios", "keyboard_shortcuts.open_media": "Abrir archivos de medios",
"keyboard_shortcuts.pinned": "Abrir lista de mensajes fijados", "keyboard_shortcuts.pinned": "Abrir lista de mensajes destacados",
"keyboard_shortcuts.profile": "Abrir perfil del autor", "keyboard_shortcuts.profile": "Abrir perfil del autor",
"keyboard_shortcuts.reply": "Responder al mensaje", "keyboard_shortcuts.reply": "Responder al mensaje",
"keyboard_shortcuts.requests": "Abrir lista de solicitudes de seguimiento", "keyboard_shortcuts.requests": "Abrir lista de solicitudes de seguimiento",
@ -561,7 +564,7 @@
"navigation_bar.mutes": "Usuarios silenciados", "navigation_bar.mutes": "Usuarios silenciados",
"navigation_bar.opened_in_classic_interface": "Los mensajes, las cuentas y otras páginas específicas se abren predeterminadamente en la interface web clásica.", "navigation_bar.opened_in_classic_interface": "Los mensajes, las cuentas y otras páginas específicas se abren predeterminadamente en la interface web clásica.",
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Personal",
"navigation_bar.pins": "Mensajes fijados", "navigation_bar.pins": "Mensajes destacados",
"navigation_bar.preferences": "Configuración", "navigation_bar.preferences": "Configuración",
"navigation_bar.public_timeline": "Línea temporal federada", "navigation_bar.public_timeline": "Línea temporal federada",
"navigation_bar.search": "Buscar", "navigation_bar.search": "Buscar",
@ -857,8 +860,7 @@
"status.mute": "Silenciar a @{name}", "status.mute": "Silenciar a @{name}",
"status.mute_conversation": "Silenciar conversación", "status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir este mensaje", "status.open": "Expandir este mensaje",
"status.pin": "Fijar en el perfil", "status.pin": "Destacar en el perfil",
"status.pinned": "Mensaje fijado",
"status.read_more": "Leé más", "status.read_more": "Leé más",
"status.reblog": "Adherir", "status.reblog": "Adherir",
"status.reblog_private": "Adherir a la audiencia original", "status.reblog_private": "Adherir a la audiencia original",
@ -883,7 +885,7 @@
"status.translated_from_with": "Traducido desde el {lang} vía {provider}", "status.translated_from_with": "Traducido desde el {lang} vía {provider}",
"status.uncached_media_warning": "Previsualización no disponible", "status.uncached_media_warning": "Previsualización no disponible",
"status.unmute_conversation": "Dejar de silenciar conversación", "status.unmute_conversation": "Dejar de silenciar conversación",
"status.unpin": "Dejar de fijar", "status.unpin": "No destacar en el perfil",
"subscribed_languages.lead": "Después del cambio, sólo los mensajes en los idiomas seleccionados aparecerán en tu línea temporal Principal y en las líneas de tiempo de lista. No seleccionés ningún idioma para poder recibir mensajes en todos los idiomas.", "subscribed_languages.lead": "Después del cambio, sólo los mensajes en los idiomas seleccionados aparecerán en tu línea temporal Principal y en las líneas de tiempo de lista. No seleccionés ningún idioma para poder recibir mensajes en todos los idiomas.",
"subscribed_languages.save": "Guardar cambios", "subscribed_languages.save": "Guardar cambios",
"subscribed_languages.target": "Cambiar idiomas suscritos para {target}", "subscribed_languages.target": "Cambiar idiomas suscritos para {target}",

View File

@ -29,6 +29,7 @@
"account.enable_notifications": "Notificarme cuando @{name} publique algo", "account.enable_notifications": "Notificarme cuando @{name} publique algo",
"account.endorse": "Destacar en mi perfil", "account.endorse": "Destacar en mi perfil",
"account.featured": "Destacado", "account.featured": "Destacado",
"account.featured.accounts": "Perfiles",
"account.featured.hashtags": "Etiquetas", "account.featured.hashtags": "Etiquetas",
"account.featured.posts": "Publicaciones", "account.featured.posts": "Publicaciones",
"account.featured_tags.last_status_at": "Última publicación el {date}", "account.featured_tags.last_status_at": "Última publicación el {date}",
@ -168,7 +169,7 @@
"column.lists": "Listas", "column.lists": "Listas",
"column.mutes": "Usuarios silenciados", "column.mutes": "Usuarios silenciados",
"column.notifications": "Notificaciones", "column.notifications": "Notificaciones",
"column.pins": "Publicaciones fijadas", "column.pins": "Publicaciones destacadas",
"column.public": "Línea de tiempo federada", "column.public": "Línea de tiempo federada",
"column_back_button.label": "Atrás", "column_back_button.label": "Atrás",
"column_header.hide_settings": "Ocultar configuración", "column_header.hide_settings": "Ocultar configuración",
@ -405,8 +406,10 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}", "hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}} hoy", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}} hoy",
"hashtag.feature": "Destacar en el perfil",
"hashtag.follow": "Seguir etiqueta", "hashtag.follow": "Seguir etiqueta",
"hashtag.mute": "Silenciar #{hashtag}", "hashtag.mute": "Silenciar #{hashtag}",
"hashtag.unfeature": "No destacar en el perfil",
"hashtag.unfollow": "Dejar de seguir etiqueta", "hashtag.unfollow": "Dejar de seguir etiqueta",
"hashtags.and_other": "…y {count, plural, other {# más}}", "hashtags.and_other": "…y {count, plural, other {# más}}",
"hints.profiles.followers_may_be_missing": "Puede que no se muestren todos los seguidores de este perfil.", "hints.profiles.followers_may_be_missing": "Puede que no se muestren todos los seguidores de este perfil.",
@ -477,7 +480,7 @@
"keyboard_shortcuts.my_profile": "Abrir tu perfil", "keyboard_shortcuts.my_profile": "Abrir tu perfil",
"keyboard_shortcuts.notifications": "Abrir la columna de notificaciones", "keyboard_shortcuts.notifications": "Abrir la columna de notificaciones",
"keyboard_shortcuts.open_media": "Abrir multimedia", "keyboard_shortcuts.open_media": "Abrir multimedia",
"keyboard_shortcuts.pinned": "Abrir la lista de publicaciones fijadas", "keyboard_shortcuts.pinned": "Abrir lista de publicaciones destacadas",
"keyboard_shortcuts.profile": "Abrir perfil del autor", "keyboard_shortcuts.profile": "Abrir perfil del autor",
"keyboard_shortcuts.reply": "Responder a la publicación", "keyboard_shortcuts.reply": "Responder a la publicación",
"keyboard_shortcuts.requests": "Abrir lista de solicitudes de seguimiento", "keyboard_shortcuts.requests": "Abrir lista de solicitudes de seguimiento",
@ -561,7 +564,7 @@
"navigation_bar.mutes": "Usuarios silenciados", "navigation_bar.mutes": "Usuarios silenciados",
"navigation_bar.opened_in_classic_interface": "Publicaciones, cuentas y otras páginas específicas se abren por defecto en la interfaz web clásica.", "navigation_bar.opened_in_classic_interface": "Publicaciones, cuentas y otras páginas específicas se abren por defecto en la interfaz web clásica.",
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Personal",
"navigation_bar.pins": "Publicaciones fijadas", "navigation_bar.pins": "Publicaciones destacadas",
"navigation_bar.preferences": "Preferencias", "navigation_bar.preferences": "Preferencias",
"navigation_bar.public_timeline": "Historia federada", "navigation_bar.public_timeline": "Historia federada",
"navigation_bar.search": "Buscar", "navigation_bar.search": "Buscar",
@ -857,8 +860,7 @@
"status.mute": "Silenciar @{name}", "status.mute": "Silenciar @{name}",
"status.mute_conversation": "Silenciar conversación", "status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir estado", "status.open": "Expandir estado",
"status.pin": "Fijar", "status.pin": "Destacar en el perfil",
"status.pinned": "Publicación fijada",
"status.read_more": "Leer más", "status.read_more": "Leer más",
"status.reblog": "Impulsar", "status.reblog": "Impulsar",
"status.reblog_private": "Implusar a la audiencia original", "status.reblog_private": "Implusar a la audiencia original",
@ -883,7 +885,7 @@
"status.translated_from_with": "Traducido del {lang} usando {provider}", "status.translated_from_with": "Traducido del {lang} usando {provider}",
"status.uncached_media_warning": "Vista previa no disponible", "status.uncached_media_warning": "Vista previa no disponible",
"status.unmute_conversation": "Dejar de silenciar conversación", "status.unmute_conversation": "Dejar de silenciar conversación",
"status.unpin": "Dejar de fijar", "status.unpin": "No destacar en el perfil",
"subscribed_languages.lead": "Solo las publicaciones en los idiomas seleccionados aparecerán en tu inicio y enlistará las líneas de tiempo después del cambio. Selecciona ninguno para recibir publicaciones en todos los idiomas.", "subscribed_languages.lead": "Solo las publicaciones en los idiomas seleccionados aparecerán en tu inicio y enlistará las líneas de tiempo después del cambio. Selecciona ninguno para recibir publicaciones en todos los idiomas.",
"subscribed_languages.save": "Guardar cambios", "subscribed_languages.save": "Guardar cambios",
"subscribed_languages.target": "Cambiar idiomas suscritos para {target}", "subscribed_languages.target": "Cambiar idiomas suscritos para {target}",

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