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
/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
/app/javascript/mastodon/locales/*.json

View File

@ -1,6 +1,6 @@
# This configuration was generated by
# `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
# one by one as the offenses are removed from the code base.
# 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.
## [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
### Add
### Added
- Add delay to profile updates to debounce them (#34137 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 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'redis-namespace', '~> 1.10'
gem 'rqrcode', '~> 2.2'
gem 'rqrcode', '~> 3.0'
gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 7.0'
gem 'scenic', '~> 1.7'
@ -212,7 +212,7 @@ group :development, :test do
gem 'test-prof', require: false
# RSpec runner for rails
gem 'rspec-rails', '~> 7.0'
gem 'rspec-rails', '~> 8.0'
end
group :production do

View File

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

View File

@ -19,9 +19,16 @@ class AccountsIndex < Chewy::Index
type: 'stemmer',
language: 'possessive_english',
},
word_joiner: {
type: 'shingle',
output_unigrams: true,
token_separator: '',
},
},
analyzer: {
# "The FOOING's bar" becomes "foo bar"
natural: {
tokenizer: 'standard',
filter: %w(
@ -35,11 +42,20 @@ class AccountsIndex < Chewy::Index
),
},
# "FOO bar" becomes "foo bar"
verbatim: {
tokenizer: 'standard',
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: {
tokenizer: 'edge_ngram',
filter: %w(lowercase asciifolding cjk_width),

View File

@ -72,6 +72,13 @@ class Api::BaseController < ApplicationController
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
render json: {}, status: 200
end

View File

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

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
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 :set_or_create_tag
@ -23,6 +24,16 @@ class Api::V1::TagsController < Api::BaseController
render json: @tag, serializer: REST::TagSerializer
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
def set_or_create_tag

View File

@ -72,10 +72,24 @@ class ApplicationController < ActionController::Base
def require_functional!
return if current_user.functional?
if current_user.confirmed?
redirect_to edit_user_registration_path
else
redirect_to auth_setup_path
respond_to do |format|
format.any do
if current_user.confirmed?
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

View File

@ -16,7 +16,7 @@ module Localized
def requested_locale
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 ||= http_accept_language if ENV['DEFAULT_LOCALE'].blank?
requested_locale_name ||= http_accept_language unless ENV['FORCE_DEFAULT_LOCALE'] == 'true'
requested_locale_name
end

View File

@ -12,7 +12,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
end
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?
redirect_to settings_featured_tags_path

View File

@ -25,6 +25,14 @@ module ContextHelper
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
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
def full_context

View File

@ -72,6 +72,18 @@ module JsonLdHelper
!haystack.casecmp(needle).zero?
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)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
graph.dump(:normalize)

View File

@ -4,9 +4,12 @@ import axios from 'axios';
import ready from '../mastodon/ready';
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';
}
}

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 { apiRemoveAccountFromFollowers } from 'mastodon/api/accounts';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import {
apiRemoveAccountFromFollowers,
apiGetEndorsedAccounts,
} from 'mastodon/api/accounts';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import { importFetchedAccounts } from './importer';
export const revealAccount = createAction<{
id: string;
}>('accounts/revealAccount');
export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>(
'accounts/importAccounts',
);
function actionWithSkipLoadingTrue<Args extends object>(args: Args) {
return {
payload: {
@ -104,3 +104,12 @@ export const removeAccountFromFollowers = createDataLoadingThunk(
apiRemoveAccountFromFollowers(accountId),
(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 { importAccounts } from '../accounts_typed';
import { importAccounts } from './accounts';
import { normalizeStatus } from './normalizer';
import { importPolls } from './polls';

View File

@ -77,6 +77,17 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
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) {

View File

@ -4,8 +4,11 @@ import api from '../api';
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
import { fetchContext } from './statuses_typed';
import { deleteFromTimelines } from './timelines';
export * from './statuses_typed';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
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_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_SUCCESS = 'STATUS_MUTE_SUCCESS';
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;
if (alsoFetchContext) {
dispatch(fetchContext(id));
dispatch(fetchContext({ statusId: id }));
}
if (skipLoading) {
@ -178,50 +177,6 @@ export function deleteStatusFail(id, error) {
export const updateStatus = status => dispatch =>
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) {
return (dispatch) => {
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';
export const fetchHashtag = createDataLoadingThunk(
@ -15,3 +21,13 @@ export const unfollowHashtag = createDataLoadingThunk(
'tags/unfollow',
({ 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 { ApiHashtagJSON } from 'mastodon/api_types/tags';
export const apiSubmitAccountNote = (id: string, value: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
@ -23,3 +25,9 @@ export const apiRemoveAccountFromFollowers = (id: string) =>
apiRequestPost<ApiRelationshipJSON>(
`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) =>
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) => {
const response = await api().request<ApiHashtagJSON[]>({
method: 'GET',

View File

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

View File

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

View File

@ -96,13 +96,19 @@ export const decode83 = (str: string) => {
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),
g: Math.max(0, (int >> 8) & 255),
b: Math.max(0, int & 255),
});
export const getAverageFromBlurhash = (blurhash: string) => {
export const getAverageFromBlurhash = (blurhash: string | null) => {
if (!blurhash) {
return null;
}

View File

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

View File

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

View File

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

View File

@ -1,24 +1,32 @@
import { FormattedMessage } from 'react-intl';
import { LoadingIndicator } from './loading_indicator';
interface Props {
onClick: (event: React.MouseEvent) => void;
disabled?: boolean;
visible?: boolean;
loading?: boolean;
}
export const LoadMore: React.FC<Props> = ({
onClick,
disabled,
visible = true,
loading = false,
}) => {
return (
<button
type='button'
className='load-more'
disabled={disabled || !visible}
disabled={disabled || loading || !visible}
style={{ visibility: visible ? 'visible' : 'hidden' }}
onClick={onClick}
>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
{loading ? (
<LoadingIndicator />
) : (
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
)}
</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 { FilterWarning } from 'mastodon/components/filter_warning';
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 Card from '../features/status/components/card';
@ -403,14 +403,7 @@ class Status extends ImmutablePureComponent {
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
const matchedFilters = status.get('matched_filters');
if (featured) {
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') {
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
prepend = (
@ -491,9 +484,6 @@ class Status extends ImmutablePureComponent {
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
width={this.props.cachedMediaWidth}
height={110}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
sensitive={status.get('sensitive')}
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 ModalRoot from 'mastodon/components/modal_root';
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 MediaModal from 'mastodon/features/ui/components/media_modal';
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 { List as ImmutableList } from 'immutable';
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
import { expandAccountFeaturedTimeline } from 'mastodon/actions/timelines';
import { Account } from 'mastodon/components/account';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RemoteHint } from 'mastodon/components/remote_hint';
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 { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
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 { FeaturedTag } from './components/featured_tag';
import type { TagMap } from './components/featured_tag';
@ -29,7 +31,9 @@ interface Params {
id?: string;
}
const AccountFeatured = () => {
const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
multiColumn,
}) => {
const accountId = useAccountId();
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
const forceEmptyState = suspended || blockedBy || hidden;
@ -40,7 +44,8 @@ const AccountFeatured = () => {
useEffect(() => {
if (accountId) {
void dispatch(expandAccountFeaturedTimeline(accountId));
dispatch(fetchFeaturedTags(accountId));
void dispatch(fetchFeaturedTags({ accountId }));
void dispatch(fetchEndorsedAccounts({ accountId }));
}
}, [accountId, dispatch]);
@ -67,6 +72,17 @@ const AccountFeatured = () => {
ImmutableList(),
) 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) {
return (
@ -78,7 +94,11 @@ const AccountFeatured = () => {
);
}
if (featuredStatusIds.isEmpty() && featuredTags.isEmpty()) {
if (
featuredStatusIds.isEmpty() &&
featuredTags.isEmpty() &&
featuredAccountIds.isEmpty()
) {
return (
<AccountFeaturedWrapper accountId={accountId}>
<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} />
</div>
</Column>

View File

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

View File

@ -107,7 +107,6 @@ const messages = defineMessages({
id: 'account.disable_notifications',
defaultMessage: 'Stop notifying me when @{name} posts',
},
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: {
id: 'navigation_bar.preferences',
defaultMessage: 'Preferences',
@ -451,7 +450,6 @@ export const AccountHeader: React.FC<{
text: intl.formatMessage(messages.preferences),
href: '/settings/preferences',
});
arr.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
arr.push(null);
arr.push({
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 { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags';
import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines';
import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
@ -27,7 +26,7 @@ import { LimitedAccountHint } from './components/limited_account_hint';
const emptyList = ImmutableList();
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) {
return {
@ -86,7 +85,6 @@ class AccountTimeline extends ImmutablePureComponent {
dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
}
dispatch(fetchFeaturedTags(accountId));
dispatch(expandAccountTimeline(accountId, { withReplies, tagged }));
if (accountId === me) {

View File

@ -27,7 +27,7 @@ import { Button } from 'mastodon/components/button';
import { GIFV } from 'mastodon/components/gifv';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
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 { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import { Video, getPointerPosition } from 'mastodon/features/video';
@ -212,11 +212,11 @@ const Preview: React.FC<{
return (
<Audio
src={media.get('url') as string}
duration={media.getIn(['meta', 'original', 'duration'], 0) as number}
poster={
(media.get('preview_url') as string | undefined) ??
account?.avatar_static
}
duration={media.getIn(['meta', 'original', 'duration'], 0) as number}
backgroundColor={
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({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: {
id: 'navigation_bar.preferences',
defaultMessage: 'Preferences',
@ -53,7 +52,6 @@ export const ActionBar: React.FC = () => {
text: intl.formatMessage(messages.preferences),
href: '/settings/preferences',
},
{ text: intl.formatMessage(messages.pins), to: '/pinned' },
null,
{
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 backgroundImageFn = () => `${assetHost}/emoji/sheet_15.png`;
const backgroundImageFn = () => `${assetHost}/emoji/sheet_15_1.png`;
const notFoundFn = () => (
<div className='emoji-mart-no-results'>

View File

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

View File

@ -15,16 +15,6 @@ const mapDispatchToProps = dispatch => ({
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);

View File

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

View File

@ -7,94 +7,21 @@
// This version comment should be bumped each time the emoji data is changed
// to ensure that the prevaled file is regenerated by Babel
// version: 3
// version: 4
// This json file contains the names of the categories.
const emojiMart5LocalesData = require('@emoji-mart/data/i18n/en.json');
const emojiMart5Data = require('@emoji-mart/data/sets/15/all.json');
const { NimbleEmojiIndex } = require('emoji-mart');
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
const _ = require('lodash');
let data = require('./emoji_data.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 { unicodeToUnifiedName } = require('./unicode_to_unified_name');
// Grabbed from `emoji_utils` to avoid circular dependency
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);
}
emojiMartUncompress(data);
const emojiMartData = data;
const emojiIndex = new NimbleEmojiIndex(emojiMartData);
const excluded = ['®', '©', '™'];
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
@ -103,10 +30,15 @@ const shortcodeMap = {};
const shortCodesToEmojiData = {};
const emojisWithoutShortCodes = [];
Object.keys(emojiMart5Data.emojis).forEach(key => {
let emoji = emojiMart5Data.emojis[key];
Object.keys(emojiIndex.emojis).forEach(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 => {
@ -150,9 +82,13 @@ Object.keys(emojiMap).forEach(key => {
}
});
Object.keys(emojiMartData.emojis).forEach(key => {
let emoji = emojiMartData.emojis[key];
Object.keys(emojiIndex.emojis).forEach(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;
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';
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) {
return {

View File

@ -29,7 +29,7 @@ import { LimitedAccountHint } from '../account_timeline/components/limited_accou
import Column from '../ui/components/column';
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) {
return {

View File

@ -9,6 +9,8 @@ import {
fetchHashtag,
followHashtag,
unfollowHashtag,
featureHashtag,
unfeatureHashtag,
} from 'mastodon/actions/tags_typed';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
import { Button } from 'mastodon/components/button';
@ -28,6 +30,11 @@ const messages = defineMessages({
id: 'hashtag.admin_moderation',
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) => (
@ -88,22 +95,51 @@ export const HashtagHeader: React.FC<{
}, [dispatch, tagId, setTag]);
const menu = useMemo(() => {
const tmp = [];
const arr = [];
if (
tag &&
signedIn &&
(permissions & PERMISSION_MANAGE_TAXONOMIES) ===
PERMISSION_MANAGE_TAXONOMIES
) {
tmp.push({
text: intl.formatMessage(messages.adminModeration, { name: tag.id }),
href: `/admin/tags/${tag.id}`,
if (tag && signedIn) {
const handleFeature = () => {
if (tag.featuring) {
void dispatch(unfeatureHashtag({ tagId })).then((result) => {
if (isFulfilled(result)) {
setTag(result.payload);
}
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;
}, [signedIn, permissions, intl, tag]);
return arr;
}, [setTag, dispatch, tagId, signedIn, permissions, intl, tag]);
const handleFollow = useCallback(() => {
if (!signedIn || !tag) {

View File

@ -13,7 +13,6 @@ import { Avatar } from 'mastodon/components/avatar';
import { ContentWarning } from 'mastodon/components/content_warning';
import { DisplayName } from 'mastodon/components/display_name';
import { Icon } from 'mastodon/components/icon';
import type { Status } from 'mastodon/models/status';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { EmbeddedStatusContent } from './embedded_status_content';
@ -27,9 +26,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
const clickCoordinatesRef = useRef<[number, number] | null>();
const dispatch = useAppDispatch();
const status = useAppSelector(
(state) => state.statuses.get(statusId) as Status | undefined,
);
const status = useAppSelector((state) => state.statuses.get(statusId));
const account = useAppSelector((state) =>
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 { me } from 'mastodon/initial_state';
import type { NotificationGroupMention } from 'mastodon/models/notification_group';
import type { Status } from 'mastodon/models/status';
import { useAppSelector } from 'mastodon/store';
import type { LabelRenderer } from './notification_group_with_status';
@ -39,9 +38,9 @@ export const NotificationMention: React.FC<{
unread: boolean;
}> = ({ notification, unread }) => {
const [isDirect, isReply] = useAppSelector((state) => {
const status = state.statuses.get(notification.statusId) as
| Status
| undefined;
const status = notification.statusId
? state.statuses.get(notification.statusId)
: undefined;
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 { 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 { useAppDispatch, useAppSelector } from 'mastodon/store/typed_functions';
import Footer from './components/footer';
import { Footer } from './components/footer';
import { Header } from './components/header';
export const PictureInPicture: React.FC = () => {
@ -58,14 +58,14 @@ export const PictureInPicture: React.FC = () => {
player = (
<Audio
src={src}
currentTime={currentTime}
volume={volume}
muted={muted}
startTime={currentTime}
startVolume={volume}
startMuted={muted}
startPlaying
poster={poster}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
accentColor={accentColor}
autoPlay
/>
);
}
@ -76,7 +76,7 @@ export const PictureInPicture: React.FC = () => {
{player}
<Footer statusId={statusId} />
<Footer statusId={statusId} onClose={handleClose} />
</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 { AnimatedNumber } from 'mastodon/components/animated_number';
import { Avatar } from 'mastodon/components/avatar';
import { ContentWarning } from 'mastodon/components/content_warning';
import { DisplayName } from 'mastodon/components/display_name';
import { EditedTimestamp } from 'mastodon/components/edited_timestamp';
import { FilterWarning } from 'mastodon/components/filter_warning';
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 { Icon } from 'mastodon/components/icon';
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 { Audio } from 'mastodon/features/audio';
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
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';
interface VideoModalOptions {
@ -189,18 +188,17 @@ export const DetailedStatus: React.FC<{
src={attachment.get('url')}
alt={description}
lang={language}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
poster={
attachment.get('preview_url') ||
status.getIn(['account', 'avatar_static'])
}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
sensitive={status.get('sensitive')}
visible={showMedia}
blurhash={attachment.get('blurhash')}
height={150}
onToggleVisibility={onToggleMediaVisibility}
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 { deleteModal } from '../../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import { getAncestorsIds, getDescendantsIds } from 'mastodon/selectors/contexts';
import Column from '../ui/components/column';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
@ -83,69 +84,15 @@ const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
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 status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' });
let ancestorsIds = ImmutableList();
let descendantsIds = ImmutableList();
let ancestorsIds = [];
let descendantsIds = [];
if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
ancestorsIds = getAncestorsIds(state, status.get('in_reply_to_id'));
descendantsIds = getDescendantsIds(state, status.get('id'));
}
return {
@ -188,8 +135,8 @@ class Status extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map,
isLoading: PropTypes.bool,
ancestorsIds: ImmutablePropTypes.list.isRequired,
descendantsIds: ImmutablePropTypes.list.isRequired,
ancestorsIds: PropTypes.arrayOf(PropTypes.string).isRequired,
descendantsIds: PropTypes.arrayOf(PropTypes.string).isRequired,
intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool,
@ -383,7 +330,7 @@ class Status extends ImmutablePureComponent {
handleToggleAll = () => {
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')) {
this.props.dispatch(revealStatus(statusIds));
@ -482,13 +429,13 @@ class Status extends ImmutablePureComponent {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size - 1, true);
this._selectChild(ancestorsIds.length - 1, true);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index, true);
this._selectChild(ancestorsIds.length + index, true);
} else {
this._selectChild(index - 1, true);
}
@ -499,13 +446,13 @@ class Status extends ImmutablePureComponent {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size + 1, false);
this._selectChild(ancestorsIds.length + 1, false);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index + 2, false);
this._selectChild(ancestorsIds.length + index + 2, false);
} else {
this._selectChild(index + 1, false);
}
@ -536,8 +483,8 @@ class Status extends ImmutablePureComponent {
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
previousId={i > 0 ? list.get(i - 1) : undefined}
nextId={list.get(i + 1) || (ancestors && statusId)}
previousId={i > 0 ? list[i - 1] : undefined}
nextId={list[i + 1] || (ancestors && statusId)}
rootId={statusId}
/>
));
@ -574,7 +521,7 @@ class Status extends ImmutablePureComponent {
componentDidUpdate (prevProps) {
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();
}
}
@ -621,11 +568,11 @@ class Status extends ImmutablePureComponent {
);
}
if (ancestorsIds && ancestorsIds.size > 0) {
if (ancestorsIds && ancestorsIds.length > 0) {
ancestors = <>{this.renderChildren(ancestorsIds, true)}</>;
}
if (descendantsIds && descendantsIds.size > 0) {
if (descendantsIds && descendantsIds.length > 0) {
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 { Icon } from 'mastodon/components/icon';
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 { disableSwiping } from 'mastodon/initial_state';

View File

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

View File

@ -5,7 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
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';
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
return (
<div>
<div
<div /* eslint-disable-line jsx-a11y/click-events-have-key-events */
role='menuitem'
className={classNames('video-player', {
inactive: !revealed,
@ -820,7 +820,7 @@ export const Video: React.FC<{
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={handleClickRoot}
onKeyDown={handleKeyDown}
onKeyDownCapture={handleKeyDown}
tabIndex={0}
>
{blurhash && (
@ -845,7 +845,7 @@ export const Video: React.FC<{
title={alt}
lang={lang}
onClick={handleClick}
onKeyDown={handleVideoKeyDown}
onKeyDownCapture={handleVideoKeyDown}
onPlay={handlePlay}
onPause={handlePause}
onLoadedData={handleLoadedData}

View File

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

View File

@ -1,12 +1,14 @@
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store';
export function useAccountVisibility(accountId?: string) {
const blockedBy = useAppSelector(
(state) => !!state.relationships.getIn([accountId, 'blocked_by'], false),
export function useAccountVisibility(accountId?: string | null) {
const blockedBy = useAppSelector((state) =>
accountId
? !!state.relationships.getIn([accountId, 'blocked_by'], false)
: false,
);
const suspended = useAppSelector(
(state) => !!state.accounts.getIn([accountId, 'suspended'], false),
const suspended = useAppSelector((state) =>
accountId ? !!state.accounts.getIn([accountId, 'suspended'], false) : false,
);
const hidden = useAppSelector((state) =>
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.mutes": "Uitgedoofte gebruikers",
"column.notifications": "Kennisgewings",
"column.pins": "Vasgemaakte plasings",
"column.public": "Gefedereerde tydlyn",
"column_back_button.label": "Terug",
"column_header.hide_settings": "Versteek instellings",
@ -196,7 +195,6 @@
"keyboard_shortcuts.my_profile": "to open your profile",
"keyboard_shortcuts.notifications": "Vertoon kennisgewingkolom",
"keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.pinned": "Vertoon vasgemaakte plasings",
"keyboard_shortcuts.profile": "Vertoon skrywersprofiel",
"keyboard_shortcuts.reply": "Reageer op plasing",
"keyboard_shortcuts.requests": "Sien volgversoeke",
@ -224,7 +222,6 @@
"navigation_bar.lists": "Lyste",
"navigation_bar.logout": "Teken uit",
"navigation_bar.personal": "Persoonlik",
"navigation_bar.pins": "Vasgemaakte plasings",
"navigation_bar.preferences": "Voorkeure",
"navigation_bar.public_timeline": "Gefedereerde tydlyn",
"navigation_bar.search": "Soek",
@ -262,7 +259,6 @@
"status.copy": "Kopieer skakel na hierdie plasing",
"status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
"status.open": "Brei hierdie plasing uit",
"status.pinned": "Vasgemaakte plasing",
"status.reblog": "Stuur aan",
"status.reblog_private": "Stuur aan met oorspronklike sigbaarheid",
"status.reblogged_by": "Aangestuur deur {name}",

View File

@ -96,7 +96,6 @@
"column.lists": "Listas",
"column.mutes": "Usuarios silenciaus",
"column.notifications": "Notificacions",
"column.pins": "Publicacions fixadas",
"column.public": "Linia de tiempo federada",
"column_back_button.label": "Dezaga",
"column_header.hide_settings": "Amagar configuración",
@ -264,7 +263,6 @@
"keyboard_shortcuts.my_profile": "Ubrir lo tuyo perfil",
"keyboard_shortcuts.notifications": "Ubrir la columna de notificacions",
"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.reply": "Responder publicación",
"keyboard_shortcuts.requests": "Ubrir la lista de peticions de seguidores",
@ -303,7 +301,6 @@
"navigation_bar.logout": "Zarrar sesión",
"navigation_bar.mutes": "Usuarios silenciaus",
"navigation_bar.personal": "Personal",
"navigation_bar.pins": "Publicacions fixadas",
"navigation_bar.preferences": "Preferencias",
"navigation_bar.public_timeline": "Linia de tiempo federada",
"navigation_bar.search": "Buscar",
@ -452,8 +449,6 @@
"status.mute": "Silenciar @{name}",
"status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir estau",
"status.pin": "Fixar",
"status.pinned": "Publicación fixada",
"status.read_more": "Leyer mas",
"status.reblog": "Retutar",
"status.reblog_private": "Empentar con l'audiencia orichinal",
@ -474,7 +469,6 @@
"status.translate": "Traducir",
"status.translated_from_with": "Traduciu de {lang} usando {provider}",
"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.save": "Alzar cambios",
"subscribed_languages.target": "Cambiar idiomas suscritos pa {target}",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@
"account.badges.bot": "Bot",
"account.cancel_follow_request": "Withdraw follow request",
"account_note.placeholder": "Click to add a note",
"column.pins": "Pinned post",
"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.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.notifications": "to open notifications column",
"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.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list",

View File

@ -29,6 +29,7 @@
"account.enable_notifications": "Notifica'm els tuts de @{name}",
"account.endorse": "Recomana en el perfil",
"account.featured": "Destacat",
"account.featured.accounts": "Perfils",
"account.featured.hashtags": "Etiquetes",
"account.featured.posts": "Publicacions",
"account.featured_tags.last_status_at": "Darrer tut el {date}",
@ -168,7 +169,7 @@
"column.lists": "Llistes",
"column.mutes": "Usuaris silenciats",
"column.notifications": "Notificacions",
"column.pins": "Tuts fixats",
"column.pins": "Publicacions destacades",
"column.public": "Línia de temps federada",
"column_back_button.label": "Enrere",
"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_uses": "{count, plural, one {{counter} tut} other {{counter} tuts}}",
"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.mute": "Silencia #{hashtag}",
"hashtag.unfeature": "No destaquis al perfil",
"hashtag.unfollow": "Deixa de seguir l'etiqueta",
"hashtags.and_other": "…i {count, plural, other {# més}}",
"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.notifications": "Obre la columna de notificacions",
"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.reply": "Respon al tut",
"keyboard_shortcuts.requests": "Obre la llista de sol·licituds de seguiment",
@ -560,7 +563,7 @@
"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.personal": "Personal",
"navigation_bar.pins": "Tuts fixats",
"navigation_bar.pins": "Publicacions destacades",
"navigation_bar.preferences": "Preferències",
"navigation_bar.public_timeline": "Línia de temps federada",
"navigation_bar.search": "Cerca",
@ -856,8 +859,7 @@
"status.mute": "Silencia @{name}",
"status.mute_conversation": "Silencia la conversa",
"status.open": "Amplia el tut",
"status.pin": "Fixa en el perfil",
"status.pinned": "Tut fixat",
"status.pin": "Destaca al perfil",
"status.read_more": "Més informació",
"status.reblog": "Impulsa",
"status.reblog_private": "Impulsa amb la visibilitat original",
@ -882,7 +884,7 @@
"status.translated_from_with": "Traduït del {lang} fent servir {provider}",
"status.uncached_media_warning": "Previsualització no disponible",
"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.save": "Desa els canvis",
"subscribed_languages.target": "Canvia les llengües subscrites per a {target}",

View File

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

View File

@ -53,7 +53,6 @@
"column.lists": "Liste",
"column.mutes": "Utilizatori piattati",
"column.notifications": "Nutificazione",
"column.pins": "Statuti puntarulati",
"column.public": "Linea pubblica glubale",
"column_back_button.label": "Ritornu",
"column_header.hide_settings": "Piattà i parametri",
@ -178,7 +177,6 @@
"keyboard_shortcuts.my_profile": "per apre u vostru prufile",
"keyboard_shortcuts.notifications": "per apre a culonna di nutificazione",
"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.reply": "risponde",
"keyboard_shortcuts.requests": "per apre a lista di dumande d'abbunamentu",
@ -212,7 +210,6 @@
"navigation_bar.logout": "Scunnettassi",
"navigation_bar.mutes": "Utilizatori piattati",
"navigation_bar.personal": "Persunale",
"navigation_bar.pins": "Statuti puntarulati",
"navigation_bar.preferences": "Preferenze",
"navigation_bar.public_timeline": "Linea pubblica glubale",
"navigation_bar.security": "Sicurità",
@ -296,8 +293,6 @@
"status.mute": "Piattà @{name}",
"status.mute_conversation": "Piattà a cunversazione",
"status.open": "Apre stu statutu",
"status.pin": "Puntarulà à u prufile",
"status.pinned": "Statutu puntarulatu",
"status.read_more": "Leghje di più",
"status.reblog": "Sparte",
"status.reblog_private": "Sparte à l'audienza uriginale",
@ -314,7 +309,6 @@
"status.show_more_all": "Slibrà tuttu",
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
"status.unmute_conversation": "Ùn piattà più a cunversazione",
"status.unpin": "Spuntarulà da u prufile",
"tabs_bar.home": "Accolta",
"tabs_bar.notifications": "Nutificazione",
"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.endorse": "Zvýraznit na profilu",
"account.featured": "Zvýrazněné",
"account.featured.accounts": "Profily",
"account.featured.hashtags": "Hashtagy",
"account.featured.posts": "Příspěvky",
"account.featured_tags.last_status_at": "Poslední příspěvek {date}",
@ -168,7 +169,7 @@
"column.lists": "Seznamy",
"column.mutes": "Skrytí uživatelé",
"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_back_button.label": "Zpět",
"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_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.feature": "Zvýraznit na profilu",
"hashtag.follow": "Sledovat hashtag",
"hashtag.mute": "Skrýt #{hashtag}",
"hashtag.unfeature": "Nezvýrazňovat na profilu",
"hashtag.unfollow": "Přestat sledovat hashtag",
"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.",
@ -477,7 +480,7 @@
"keyboard_shortcuts.my_profile": "Otevřít váš profil",
"keyboard_shortcuts.notifications": "Otevřít sloupec oznámení",
"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.reply": "Odpovědět na příspěvek",
"keyboard_shortcuts.requests": "Otevřít seznam žádostí o sledování",
@ -561,7 +564,7 @@
"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.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.public_timeline": "Federovaná časová osa",
"navigation_bar.search": "Hledat",
@ -857,8 +860,7 @@
"status.mute": "Skrýt @{name}",
"status.mute_conversation": "Skrýt konverzaci",
"status.open": "Rozbalit tento příspěvek",
"status.pin": "Připnout na profil",
"status.pinned": "Připnutý příspěvek",
"status.pin": "Zvýraznit na profilu",
"status.read_more": "Číst více",
"status.reblog": "Boostnout",
"status.reblog_private": "Boostnout s původní viditelností",
@ -883,7 +885,7 @@
"status.translated_from_with": "Přeloženo z {lang} pomocí {provider}",
"status.uncached_media_warning": "Náhled není k dispozici",
"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.save": "Uložit změny",
"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.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.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.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.rules": "Rheolau'r gweinydd",
"account.account_note_header": "Nodyn personol",
"account.add_or_remove_from_list": "Ychwanegu neu Ddileu o'r rhestrau",
"account.badges.bot": "Awtomataidd",
"account.badges.group": "Grŵp",
"account.block": "Blocio @{name}",
"account.block_domain": "Blocio'r parth {domain}",
"account.block_short": "Blocio",
"account.blocked": "Blociwyd",
"account.block": "Rhwystro @{name}",
"account.block_domain": "Rhwystro'r parth {domain}",
"account.block_short": "Rhwystro",
"account.blocked": "Wedi'i rwystro",
"account.blocking": "Yn Rhwystro",
"account.cancel_follow_request": "Tynnu cais i ddilyn",
"account.copy": "Copïo dolen i'r proffil",
@ -28,7 +28,7 @@
"account.edit_profile": "Golygu'r proffil",
"account.enable_notifications": "Rhowch wybod i fi pan fydd @{name} yn postio",
"account.endorse": "Dangos ar fy mhroffil",
"account.featured": "Dethol",
"account.featured": "Nodwedd",
"account.featured.hashtags": "Hashnodau",
"account.featured.posts": "Postiadau",
"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.following": "Yn dilyn",
"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.go_to_profile": "Mynd i'r proffil",
"account.hide_reblogs": "Cuddio hybiau gan @{name}",
@ -168,7 +168,7 @@
"column.lists": "Rhestrau",
"column.mutes": "Defnyddwyr wedi'u tewi",
"column.notifications": "Hysbysiadau",
"column.pins": "Postiadau wedi eu pinio",
"column.pins": "Postiadau nodwedd",
"column.public": "Ffrwd y ffederasiwn",
"column_back_button.label": "Nôl",
"column_header.hide_settings": "Cuddio'r dewisiadau",
@ -477,7 +477,7 @@
"keyboard_shortcuts.my_profile": "Agor eich proffil",
"keyboard_shortcuts.notifications": "Agor colofn hysbysiadau",
"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.reply": "Ymateb i bostiad",
"keyboard_shortcuts.requests": "Agor rhestr ceisiadau dilyn",
@ -561,7 +561,7 @@
"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.personal": "Personol",
"navigation_bar.pins": "Postiadau wedi eu pinio",
"navigation_bar.pins": "Postiadau nodwedd",
"navigation_bar.preferences": "Dewisiadau",
"navigation_bar.public_timeline": "Ffrwd y ffederasiwn",
"navigation_bar.search": "Chwilio",
@ -857,8 +857,7 @@
"status.mute": "Anwybyddu @{name}",
"status.mute_conversation": "Anwybyddu sgwrs",
"status.open": "Ehangu'r post hwn",
"status.pin": "Pinio ar y proffil",
"status.pinned": "Postiad wedi'i binio",
"status.pin": "Dangos ar y proffil",
"status.read_more": "Darllen rhagor",
"status.reblog": "Hybu",
"status.reblog_private": "Hybu i'r gynulleidfa wreiddiol",
@ -883,7 +882,7 @@
"status.translated_from_with": "Cyfieithwyd o {lang} gan ddefnyddio {provider}",
"status.uncached_media_warning": "Dim rhagolwg ar gael",
"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.save": "Cadw'r newidiadau",
"subscribed_languages.target": "Newid ieithoedd tanysgrifio {target}",

View File

@ -29,6 +29,7 @@
"account.enable_notifications": "Advisér mig, når @{name} poster",
"account.endorse": "Fremhæv på profil",
"account.featured": "Fremhævet",
"account.featured.accounts": "Profiler",
"account.featured.hashtags": "Hashtags",
"account.featured.posts": "Indlæg",
"account.featured_tags.last_status_at": "Seneste indlæg {date}",
@ -168,7 +169,7 @@
"column.lists": "Lister",
"column.mutes": "Skjulte brugere",
"column.notifications": "Notifikationer",
"column.pins": "Fastgjorte indlæg",
"column.pins": "Fremhævede indlæg",
"column.public": "Fælles tidslinje",
"column_back_button.label": "Tilbage",
"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_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.feature": "Fremhæv på profil",
"hashtag.follow": "Følg etiket",
"hashtag.mute": "Tavsgør #{hashtag}",
"hashtag.unfeature": "Fremhæv ikke på profil",
"hashtag.unfollow": "Stop med at følge etiket",
"hashtags.and_other": "…og {count, plural, one {}other {# flere}}",
"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.notifications": "for at åbne notifikationskolonnen",
"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.reply": "Besvar indlægget",
"keyboard_shortcuts.requests": "Åbn liste over følgeanmodninger",
@ -561,7 +564,7 @@
"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.personal": "Personlig",
"navigation_bar.pins": "Fastgjorte indlæg",
"navigation_bar.pins": "Fremhævede indlæg",
"navigation_bar.preferences": "Præferencer",
"navigation_bar.public_timeline": "Fælles tidslinje",
"navigation_bar.search": "Søg",
@ -857,8 +860,7 @@
"status.mute": "Skjul @{name}",
"status.mute_conversation": "Skjul samtale",
"status.open": "Udvid dette indlæg",
"status.pin": "Fastgør til profil",
"status.pinned": "Fastgjort indlæg",
"status.pin": "Fremhæv på profil",
"status.read_more": "Læs mere",
"status.reblog": "Fremhæv",
"status.reblog_private": "Fremhæv med oprindelig synlighed",
@ -883,7 +885,7 @@
"status.translated_from_with": "Oversat fra {lang} ved brug af {provider}",
"status.uncached_media_warning": "Ingen forhåndsvisning",
"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.save": "Gem ændringer",
"subscribed_languages.target": "Skift abonnementssprog for {target}",

View File

@ -27,8 +27,9 @@
"account.domain_blocking": "Domain blockiert",
"account.edit_profile": "Profil bearbeiten",
"account.enable_notifications": "Benachrichtige mich wenn @{name} etwas postet",
"account.endorse": "Im Profil empfehlen",
"account.endorse": "Im Profil vorstellen",
"account.featured": "Vorgestellt",
"account.featured.accounts": "Profile",
"account.featured.hashtags": "Hashtags",
"account.featured.posts": "Beiträge",
"account.featured_tags.last_status_at": "Letzter Beitrag am {date}",
@ -74,7 +75,7 @@
"account.unblock_domain": "Blockierung von {domain} aufheben",
"account.unblock_domain_short": "Entsperren",
"account.unblock_short": "Blockierung aufheben",
"account.unendorse": "Im Profil nicht mehr empfehlen",
"account.unendorse": "Im Profil nicht mehr vorstellen",
"account.unfollow": "Entfolgen",
"account.unmute": "Stummschaltung von @{name} aufheben",
"account.unmute_notifications_short": "Stummschaltung der Benachrichtigungen aufheben",
@ -168,7 +169,7 @@
"column.lists": "Listen",
"column.mutes": "Stummgeschaltete Profile",
"column.notifications": "Benachrichtigungen",
"column.pins": "Angeheftete Beiträge",
"column.pins": "Vorgestellte Beiträge",
"column.public": "Föderierte Timeline",
"column_back_button.label": "Zurück",
"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_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.feature": "Im Profil vorstellen",
"hashtag.follow": "Hashtag folgen",
"hashtag.mute": "#{hashtag} stummschalten",
"hashtag.unfeature": "Im Profil nicht mehr vorstellen",
"hashtag.unfollow": "Hashtag entfolgen",
"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.",
@ -426,7 +429,7 @@
"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.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_review_separately": "Gefilterte Benachrichtigungen können separat überprüft werden",
"ignore_notifications_modal.ignore": "Benachrichtigungen ignorieren",
@ -477,7 +480,7 @@
"keyboard_shortcuts.my_profile": "Eigenes Profil aufrufen",
"keyboard_shortcuts.notifications": "Benachrichtigungen aufrufen",
"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.reply": "Auf Beitrag antworten",
"keyboard_shortcuts.requests": "Liste der Follower-Anfragen aufrufen",
@ -561,7 +564,7 @@
"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.personal": "Persönlich",
"navigation_bar.pins": "Angeheftete Beiträge",
"navigation_bar.pins": "Vorgestellte Beiträge",
"navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Föderierte Timeline",
"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.status": "{name} postete …",
"notification.update": "{name} bearbeitete einen Beitrag",
"notification_requests.accept": "Genehmigen",
"notification_requests.accept_multiple": "{count, plural, one {# Anfrage genehmigen …} other {# Anfragen genehmigen …}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Anfrage genehmigen} other {Anfragen genehmigen}}",
"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.title": "Benachrichtigungsanfragen genehmigen?",
"notification_requests.accept": "Akzeptieren",
"notification_requests.accept_multiple": "{count, plural, one {# Anfrage akzeptieren …} other {# Anfragen akzeptieren …}}",
"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 akzeptieren. Möchtest du wirklich fortfahren?",
"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.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?",
@ -857,8 +860,7 @@
"status.mute": "@{name} stummschalten",
"status.mute_conversation": "Unterhaltung stummschalten",
"status.open": "Beitrag öffnen",
"status.pin": "Im Profil anheften",
"status.pinned": "Angehefteter Beitrag",
"status.pin": "Im Profil vorstellen",
"status.read_more": "Gesamten Beitrag anschauen",
"status.reblog": "Teilen",
"status.reblog_private": "Mit der ursprünglichen Zielgruppe teilen",
@ -883,7 +885,7 @@
"status.translated_from_with": "Aus {lang} mittels {provider} übersetzt",
"status.uncached_media_warning": "Vorschau nicht verfügbar",
"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.save": "Änderungen speichern",
"subscribed_languages.target": "Abonnierte Sprachen für {target} ändern",

View File

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

View File

@ -157,7 +157,6 @@
"column.lists": "Lists",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
"column.pins": "Pinned posts",
"column.public": "Federated timeline",
"column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
@ -457,7 +456,6 @@
"keyboard_shortcuts.my_profile": "to open your profile",
"keyboard_shortcuts.notifications": "Open notifications column",
"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.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list",
@ -541,7 +539,6 @@
"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.personal": "Personal",
"navigation_bar.pins": "Pinned posts",
"navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.search": "Search",
@ -837,8 +834,6 @@
"status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this post",
"status.pin": "Pin on profile",
"status.pinned": "Pinned post",
"status.read_more": "Read more",
"status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility",
@ -863,7 +858,6 @@
"status.translated_from_with": "Translated from {lang} using {provider}",
"status.uncached_media_warning": "Preview not available",
"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.save": "Save changes",
"subscribed_languages.target": "Change subscribed languages for {target}",

View File

@ -29,6 +29,7 @@
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Feature on profile",
"account.featured": "Featured",
"account.featured.accounts": "Profiles",
"account.featured.hashtags": "Hashtags",
"account.featured.posts": "Posts",
"account.featured_tags.last_status_at": "Last post on {date}",
@ -168,7 +169,7 @@
"column.lists": "Lists",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
"column.pins": "Pinned posts",
"column.pins": "Featured posts",
"column.public": "Federated timeline",
"column_back_button.label": "Back",
"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_uses": "{count, plural, one {{counter} post} other {{counter} posts}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today",
"hashtag.feature": "Feature on profile",
"hashtag.follow": "Follow hashtag",
"hashtag.mute": "Mute #{hashtag}",
"hashtag.unfeature": "Don't feature on profile",
"hashtag.unfollow": "Unfollow hashtag",
"hashtags.and_other": "…and {count, plural, other {# more}}",
"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.notifications": "Open notifications column",
"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.reply": "Reply to post",
"keyboard_shortcuts.requests": "Open follow requests list",
@ -561,7 +564,7 @@
"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.personal": "Personal",
"navigation_bar.pins": "Pinned posts",
"navigation_bar.pins": "Featured posts",
"navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.search": "Search",
@ -857,8 +860,7 @@
"status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this post",
"status.pin": "Pin on profile",
"status.pinned": "Pinned post",
"status.pin": "Feature on profile",
"status.read_more": "Read more",
"status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility",
@ -883,7 +885,7 @@
"status.translated_from_with": "Translated from {lang} using {provider}",
"status.uncached_media_warning": "Preview not available",
"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.save": "Save changes",
"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.edit_profile": "Redakti la profilon",
"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.posts": "Afiŝoj",
"account.featured_tags.last_status_at": "Lasta afîŝo je {date}",
@ -162,7 +162,6 @@
"column.lists": "Listoj",
"column.mutes": "Silentigitaj uzantoj",
"column.notifications": "Sciigoj",
"column.pins": "Alpinglitaj afiŝoj",
"column.public": "Fratara templinio",
"column_back_button.label": "Reveni",
"column_header.hide_settings": "Kaŝi la agordojn",
@ -462,7 +461,6 @@
"keyboard_shortcuts.my_profile": "Malfermu vian profilon",
"keyboard_shortcuts.notifications": "Malfermu la sciigajn kolumnon",
"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.reply": "Respondu al afiŝo",
"keyboard_shortcuts.requests": "Malfermi la liston de petoj por sekvado",
@ -546,7 +544,6 @@
"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.personal": "Persone",
"navigation_bar.pins": "Alpinglitaj afiŝoj",
"navigation_bar.preferences": "Preferoj",
"navigation_bar.public_timeline": "Fratara templinio",
"navigation_bar.search": "Serĉi",
@ -842,8 +839,6 @@
"status.mute": "Silentigi @{name}",
"status.mute_conversation": "Silentigi konversacion",
"status.open": "Pligrandigu ĉi tiun afiŝon",
"status.pin": "Alpingli al la profilo",
"status.pinned": "Alpinglita afiŝo",
"status.read_more": "Legi pli",
"status.reblog": "Diskonigi",
"status.reblog_private": "Diskonigi kun la sama videbleco",
@ -868,7 +863,6 @@
"status.translated_from_with": "Tradukita el {lang} per {provider}",
"status.uncached_media_warning": "Antaŭrigardo ne disponebla",
"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.save": "Konservi ŝanĝojn",
"subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}",

View File

@ -24,11 +24,12 @@
"account.copy": "Copiar enlace al perfil",
"account.direct": "Mención privada a @{name}",
"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.enable_notifications": "Notificarme cuando @{name} envíe mensajes",
"account.endorse": "Destacar en el perfil",
"account.featured": "Destacados",
"account.featured.accounts": "Perfiles",
"account.featured.hashtags": "Etiquetas",
"account.featured.posts": "Mensajes",
"account.featured_tags.last_status_at": "Último mensaje: {date}",
@ -168,7 +169,7 @@
"column.lists": "Listas",
"column.mutes": "Usuarios silenciados",
"column.notifications": "Notificaciones",
"column.pins": "Mensajes fijados",
"column.pins": "Mensajes destacados",
"column.public": "Línea temporal federada",
"column_back_button.label": "Volver",
"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_uses": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}",
"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.mute": "Silenciar #{hashtag}",
"hashtag.unfeature": "No destacar en el perfil",
"hashtag.unfollow": "Dejar de seguir etiqueta",
"hashtags.and_other": "…y {count, plural, other {# más}}",
"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.notifications": "Abrir columna de notificaciones",
"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.reply": "Responder al mensaje",
"keyboard_shortcuts.requests": "Abrir lista de solicitudes de seguimiento",
@ -561,7 +564,7 @@
"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.personal": "Personal",
"navigation_bar.pins": "Mensajes fijados",
"navigation_bar.pins": "Mensajes destacados",
"navigation_bar.preferences": "Configuración",
"navigation_bar.public_timeline": "Línea temporal federada",
"navigation_bar.search": "Buscar",
@ -857,8 +860,7 @@
"status.mute": "Silenciar a @{name}",
"status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir este mensaje",
"status.pin": "Fijar en el perfil",
"status.pinned": "Mensaje fijado",
"status.pin": "Destacar en el perfil",
"status.read_more": "Leé más",
"status.reblog": "Adherir",
"status.reblog_private": "Adherir a la audiencia original",
@ -883,7 +885,7 @@
"status.translated_from_with": "Traducido desde el {lang} vía {provider}",
"status.uncached_media_warning": "Previsualización no disponible",
"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.save": "Guardar cambios",
"subscribed_languages.target": "Cambiar idiomas suscritos para {target}",

View File

@ -29,6 +29,7 @@
"account.enable_notifications": "Notificarme cuando @{name} publique algo",
"account.endorse": "Destacar en mi perfil",
"account.featured": "Destacado",
"account.featured.accounts": "Perfiles",
"account.featured.hashtags": "Etiquetas",
"account.featured.posts": "Publicaciones",
"account.featured_tags.last_status_at": "Última publicación el {date}",
@ -168,7 +169,7 @@
"column.lists": "Listas",
"column.mutes": "Usuarios silenciados",
"column.notifications": "Notificaciones",
"column.pins": "Publicaciones fijadas",
"column.pins": "Publicaciones destacadas",
"column.public": "Línea de tiempo federada",
"column_back_button.label": "Atrás",
"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_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.feature": "Destacar en el perfil",
"hashtag.follow": "Seguir etiqueta",
"hashtag.mute": "Silenciar #{hashtag}",
"hashtag.unfeature": "No destacar en el perfil",
"hashtag.unfollow": "Dejar de seguir etiqueta",
"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.",
@ -477,7 +480,7 @@
"keyboard_shortcuts.my_profile": "Abrir tu perfil",
"keyboard_shortcuts.notifications": "Abrir la columna de notificaciones",
"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.reply": "Responder a la publicación",
"keyboard_shortcuts.requests": "Abrir lista de solicitudes de seguimiento",
@ -561,7 +564,7 @@
"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.personal": "Personal",
"navigation_bar.pins": "Publicaciones fijadas",
"navigation_bar.pins": "Publicaciones destacadas",
"navigation_bar.preferences": "Preferencias",
"navigation_bar.public_timeline": "Historia federada",
"navigation_bar.search": "Buscar",
@ -857,8 +860,7 @@
"status.mute": "Silenciar @{name}",
"status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir estado",
"status.pin": "Fijar",
"status.pinned": "Publicación fijada",
"status.pin": "Destacar en el perfil",
"status.read_more": "Leer más",
"status.reblog": "Impulsar",
"status.reblog_private": "Implusar a la audiencia original",
@ -883,7 +885,7 @@
"status.translated_from_with": "Traducido del {lang} usando {provider}",
"status.uncached_media_warning": "Vista previa no disponible",
"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.save": "Guardar cambios",
"subscribed_languages.target": "Cambiar idiomas suscritos para {target}",

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