Compare commits

...
Sign in to create a new pull request.

38 commits

Author SHA1 Message Date
Mario
15c51c7afb Modern fix 2025-06-15 02:13:16 -04:00
Mario
36751b6b0a Theme Modern contrast 2025-06-15 02:07:47 -04:00
Mario
a48a693145 Modern Theme 2025-06-15 02:03:15 -04:00
Mario
be07032ad7 Fix 2025-06-15 01:45:10 -04:00
Mario
ab55cad02e Changes 2025-06-15 01:38:49 -04:00
Mario
3438454d2a Footer fix 2025-06-15 01:33:05 -04:00
Mario
181ce90aa1 Changes 2025-06-15 01:31:02 -04:00
Mario
c2ef6ac354 Foter last 2025-06-15 01:22:32 -04:00
Mario
b8aa1ec133 Footer 2025-06-15 01:17:22 -04:00
5c122a035b revert 75c4430778
revert Footer
2025-06-15 07:13:19 +02:00
Mario
75c4430778 Footer 2025-06-15 01:11:38 -04:00
571f037d8a revert 86191f3ce1
revert Footer
2025-06-15 07:05:40 +02:00
Mario
86191f3ce1 Footer 2025-06-15 01:03:30 -04:00
Mario
19c8848507 En 2025-06-15 00:58:20 -04:00
b768870895 revert 750f5f4885
Some checks failed
CodeQL / Analyze (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
Check i18n / check-i18n (push) Has been cancelled
Ruby Testing / build (production) (push) Has been cancelled
Ruby Testing / build (test) (push) Has been cancelled
Ruby Testing / End to End testing (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 (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (push) Has been cancelled
Ruby Testing / Back to original and return test (push) Has been cancelled
revert EN translation edit
2025-06-15 06:25:53 +02:00
3e7972c5b1 Update app/validators/status_length_validator.rb
Some checks failed
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (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 (push) Blocked by required conditions
Ruby Testing / End to End testing (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Back to original and return test (push) Blocked by required conditions
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
2025-06-15 06:23:37 +02:00
7a74c056ee JP to EN
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
Check formatting / lint (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / build (production) (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 (push) Blocked by required conditions
Ruby Testing / End to End testing (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Back to original and return test (push) Blocked by required conditions
2025-06-15 06:18:55 +02:00
750f5f4885 EN translation edit
Some checks are pending
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
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (push) Blocked by required conditions
Ruby Testing / End to End testing (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Back to original and return test (push) Blocked by required conditions
2025-06-15 06:18:29 +02:00
3daed614d5 En edit
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (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 / Libvips tests (push) Blocked by required conditions
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 / End to End testing (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Back to original and return test (push) Blocked by required conditions
2025-06-15 06:17:29 +02:00
f600d9f95b En edit
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (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 (push) Blocked by required conditions
Ruby Testing / End to End testing (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Back to original and return test (push) Blocked by required conditions
2025-06-15 06:17:06 +02:00
5267d19474 Set max trending tags
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (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 (push) Blocked by required conditions
Ruby Testing / End to End testing (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Back to original and return test (push) Blocked by required conditions
2025-06-15 06:16:01 +02:00
Mario
59657684e1 fix
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (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 (push) Blocked by required conditions
Ruby Testing / End to End testing (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Back to original and return test (push) Blocked by required conditions
2025-06-15 00:05:01 -04:00
Mario
44efbc7141 Fix
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (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 (push) Blocked by required conditions
Ruby Testing / End to End testing (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Back to original and return test (push) Blocked by required conditions
2025-06-14 23:54:06 -04:00
Mario
25d176c280 Followed tags + dir
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (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 / End to End testing (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
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 / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Back to original and return test (push) Blocked by required conditions
2025-06-14 23:40:51 -04:00
Mario
f6447c0f4d New icons set
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (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 (push) Blocked by required conditions
Ruby Testing / End to End testing (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Back to original and return test (push) Blocked by required conditions
2025-06-14 23:11:27 -04:00
KMY(雪あすか)
da60c95317
Merge pull request #1005 from kmycode/kb-draft-18.1
Release: 18.1
2025-05-07 16:35:45 +09:00
KMY
49e05d9f2b Fix test 2025-05-07 11:27:14 +09:00
KMY
9524487909 Bump version to v4.3.8 (#34626) 2025-05-07 08:00:56 +09:00
Claire
d21a4f8c39 Fix code style issue (#34624) 2025-05-07 07:59:03 +09:00
Claire
d510b5d8b9 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-07 07:57:43 +09:00
Claire
ff90575e2c Add warning for REDIS_NAMESPACE deprecation at startup (#34581) 2025-05-07 07:57:29 +09:00
Claire
f41c09f291 Remove double-query for signed query strings (#34610) 2025-05-07 07:57:08 +09:00
Claire
a7bc288569 Add built-in context for interaction policies (#34574) 2025-05-07 07:56:54 +09:00
Claire
843a5446e8 Fix incorrect redirect in response to unauthenticated API requests in limited federation mode (#34549) 2025-05-07 07:55:10 +09:00
Claire
038d3b1513 Fix sign-up e-mail confirmation page reloading on error or redirect (#34548) 2025-05-07 07:54:41 +09:00
KMY(雪あすか)
b091cbdbea
Merge pull request #1001 from kmycode/kb-draft-18.0
Release: kb18.0
2025-04-14 17:43:53 +09:00
KMY
8992602acf Bump version to 18.0 2025-04-14 13:34:50 +09:00
KMY
c68762e2bf Fix: フルダークで文字が薄く表示される問題 2025-04-14 10:52:33 +09:00
68 changed files with 3289 additions and 275 deletions

View file

@ -2,6 +2,54 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.3.8] - 2025-05-06
### Security
- Update dependencies
- Check scheme on account, profile, and media URLs ([GHSA-x2rc-v5wx-g3m5](https://github.com/mastodon/mastodon/security/advisories/GHSA-x2rc-v5wx-g3m5))
### Added
- Add warning for REDIS_NAMESPACE deprecation at startup (#34581 by @ClearlyClaire)
- Add built-in context for interaction policies (#34574 by @ClearlyClaire)
### Changed
- Change activity distribution error handling to skip retrying for deleted accounts (#33617 by @ClearlyClaire)
### Removed
- Remove double-query for signed query strings (#34610 by @ClearlyClaire)
### Fixed
- Fix incorrect redirect in response to unauthenticated API requests in limited federation mode (#34549 by @ClearlyClaire)
- Fix sign-up e-mail confirmation page reloading on error or redirect (#34548 by @ClearlyClaire)
## [4.3.7] - 2025-04-02
### 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)
### Changed
- Change account suspensions to be federated to recently-followed accounts as well (#34294 by @ClearlyClaire)
- Change `AccountReachFinder` to consider statuses based on suspension date (#32805 and #34291 by @ClearlyClaire and @mjankowski)
- Change user archive signed URL TTL from 10 seconds to 1 hour (#34254 by @ClearlyClaire)
### Fixed
- Fix static version of animated PNG emojis not being properly extracted (#34337 by @ClearlyClaire)
- Fix filters not applying in detailed view, favourites and bookmarks (#34259 and #34260 by @ClearlyClaire)
- Fix handling of malformed/unusual HTML (#34201 by @ClearlyClaire)
- Fix `CacheBuster` being queued for missing media attachments (#34253 by @ClearlyClaire)
- Fix incorrect URL being used when cache busting (#34189 by @ClearlyClaire)
- Fix streaming server refusing unix socket path in `DATABASE_URL` (#34091 by @ClearlyClaire)
- Fix “x” hotkey not working on boosted filtered posts (#33758 by @ClearlyClaire)
## [4.3.6] - 2025-03-13 ## [4.3.6] - 2025-03-13
### Security ### Security

View file

@ -435,7 +435,7 @@ GEM
mutex_m (0.3.0) mutex_m (0.3.0)
net-http (0.6.0) net-http (0.6.0)
uri uri
net-imap (0.5.6) net-imap (0.5.7)
date date
net-protocol net-protocol
net-ldap (0.19.0) net-ldap (0.19.0)
@ -446,7 +446,7 @@ GEM
net-smtp (0.5.1) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.18.7) nokogiri (1.18.8)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.10) oj (3.16.10)

View file

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

View file

@ -7,7 +7,7 @@ class Api::V1::Trends::TagsController < Api::BaseController
after_action :insert_pagination_headers after_action :insert_pagination_headers
DEFAULT_TAGS_LIMIT = 10 DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i
deprecate_api '2022-03-30', only: :index, if: -> { request.path == '/api/v1/trends' } deprecate_api '2022-03-30', only: :index, if: -> { request.path == '/api/v1/trends' }

View file

@ -72,6 +72,8 @@ class ApplicationController < ActionController::Base
def require_functional! def require_functional!
return if current_user.functional? return if current_user.functional?
respond_to do |format|
format.any do
if current_user.confirmed? if current_user.confirmed?
redirect_to edit_user_registration_path redirect_to edit_user_registration_path
else else
@ -79,6 +81,18 @@ class ApplicationController < ActionController::Base
end end
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
def skip_csrf_meta_tags? def skip_csrf_meta_tags?
false false
end end

View file

@ -35,6 +35,13 @@ module ContextHelper
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } }, attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
misskey_license: { 'misskey' => 'https://misskey-hub.net/ns#', '_misskey_license' => 'misskey:_misskey_license' }, misskey_license: { 'misskey' => 'https://misskey-hub.net/ns#', '_misskey_license' => 'misskey:_misskey_license' },
interaction_policies: {
'gts' => 'https://gotosocial.org/ns#',
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },
'canQuote' => { '@id' => 'gts:canQuote', '@type' => '@id' },
'automaticApproval' => { '@id' => 'gts:automaticApproval', '@type' => '@id' },
'manualApproval' => { '@id' => 'gts:manualApproval', '@type' => '@id' },
},
}.freeze }.freeze
def full_context def full_context

View file

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

BIN
app/javascript/icons/android-chrome-144x144.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-192x192.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-256x256.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-36x36.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 950 B

Before After
Before After

BIN
app/javascript/icons/android-chrome-384x384.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-48x48.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-512x512.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-72x72.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-96x96.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-1024x1024.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-114x114.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-120x120.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-144x144.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-152x152.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-167x167.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-180x180.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
app/javascript/icons/apple-touch-icon-57x57.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-60x60.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-72x72.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-76x76.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
app/javascript/icons/favicon-16x16.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 986 B

After

Width:  |  Height:  |  Size: 6 KiB

Before After
Before After

BIN
app/javascript/icons/favicon-32x32.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Before After
Before After

BIN
app/javascript/icons/favicon-48x48.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Before After
Before After

View file

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

View file

@ -42,7 +42,7 @@ class ServerBanner extends PureComponent {
return ( return (
<div className='server-banner'> <div className='server-banner'>
<div className='server-banner__introduction'> <div className='server-banner__introduction'>
<FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank' rel='noopener'>Mastodon</a> }} /> <FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank' rel='noopener'>Mastodon</a> }} />
</div> </div>
<Link to='/about'> <Link to='/about'>

View file

@ -20,7 +20,7 @@ import Column from 'mastodon/components/column';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { ServerHeroImage } from 'mastodon/components/server_hero_image'; import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import { Skeleton } from 'mastodon/components/skeleton'; import { Skeleton } from 'mastodon/components/skeleton';
import { LinkFooter} from 'mastodon/features/ui/components/link_footer'; import { LinkFooter } from 'mastodon/features/ui/components/link_footer';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.about', defaultMessage: 'About' }, title: { id: 'column.about', defaultMessage: 'About' },
@ -40,6 +40,10 @@ const messages = defineMessages({
enabled: { id: 'about.enabled', defaultMessage: 'Enabled' }, enabled: { id: 'about.enabled', defaultMessage: 'Enabled' },
disabled: { id: 'about.disabled', defaultMessage: 'Disabled' }, disabled: { id: 'about.disabled', defaultMessage: 'Disabled' },
capabilities: { id: 'about.kmyblue_capabilities', defaultMessage: 'Features available in this server' }, capabilities: { id: 'about.kmyblue_capabilities', defaultMessage: 'Features available in this server' },
joinFediverse: {
id: 'about.join_fediverse',
defaultMessage: "Join the Fediverse, become part of a community, and break free from Big Tech™'s stranglehold on public discourse."
},
}); });
const severityMessages = { const severityMessages = {
@ -59,14 +63,13 @@ const severityMessages = {
}, },
}; };
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
server: state.getIn(['server', 'server']), server: state.getIn(['server', 'server']),
extendedDescription: state.getIn(['server', 'extendedDescription']), extendedDescription: state.getIn(['server', 'extendedDescription']),
domainBlocks: state.getIn(['server', 'domainBlocks']), domainBlocks: state.getIn(['server', 'domainBlocks']),
}); });
class Section extends PureComponent { class Section extends PureComponent {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
@ -85,49 +88,64 @@ class Section extends PureComponent {
this.setState({ collapsed: !collapsed }, () => onOpen && onOpen()); this.setState({ collapsed: !collapsed }, () => onOpen && onOpen());
}; };
render () { handleKeyDown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.handleClick();
}
};
render() {
const { title, children } = this.props; const { title, children } = this.props;
const { collapsed } = this.state; const { collapsed } = this.state;
return ( return (
<div className={classNames('about__section', { active: !collapsed })}> <div className={classNames('about__section', { active: !collapsed })}>
<div className='about__section__title' role='button' tabIndex={0} onClick={this.handleClick}> <div
className="about__section__title"
role="button"
tabIndex={0}
onClick={this.handleClick}
onKeyDown={this.handleKeyDown}
aria-expanded={!collapsed}
>
<Icon id={collapsed ? 'chevron-right' : 'chevron-down'} icon={collapsed ? ChevronRightIcon : ExpandMoreIcon} /> {title} <Icon id={collapsed ? 'chevron-right' : 'chevron-down'} icon={collapsed ? ChevronRightIcon : ExpandMoreIcon} /> {title}
</div> </div>
{!collapsed && ( {!collapsed && <div className="about__section__body">{children}</div>}
<div className='about__section__body'>{children}</div>
)}
</div> </div>
); );
} }
} }
class CapabilityIcon extends PureComponent { class CapabilityIcon extends PureComponent {
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
state: PropTypes.bool, state: PropTypes.bool,
}; };
render () { render() {
const { intl, state } = this.props; const { intl, state } = this.props;
if (state) { if (state) {
return ( return (
<span className='capability-icon enabled'><Icon id='check' icon={EnabledIcon} title={intl.formatMessage(messages.enabled)} />{intl.formatMessage(messages.enabled)}</span> <span className="capability-icon enabled">
<Icon id="check" icon={EnabledIcon} title={intl.formatMessage(messages.enabled)} />
{intl.formatMessage(messages.enabled)}
</span>
); );
} else { } else {
return ( return (
<span className='capability-icon disabled'><Icon id='times' icon={DisabledIcon} title={intl.formatMessage(messages.disabled)} />{intl.formatMessage(messages.disabled)}</span> <span className="capability-icon disabled">
<Icon id="times" icon={DisabledIcon} title={intl.formatMessage(messages.disabled)} />
{intl.formatMessage(messages.disabled)}
</span>
); );
} }
} }
} }
class About extends PureComponent { class About extends PureComponent {
static propTypes = { static propTypes = {
server: ImmutablePropTypes.map, server: ImmutablePropTypes.map,
extendedDescription: ImmutablePropTypes.map, extendedDescription: ImmutablePropTypes.map,
@ -141,7 +159,7 @@ class About extends PureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentDidMount () { componentDidMount() {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(fetchServer()); dispatch(fetchServer());
dispatch(fetchExtendedDescription()); dispatch(fetchExtendedDescription());
@ -152,11 +170,11 @@ class About extends PureComponent {
dispatch(fetchDomainBlocks()); dispatch(fetchDomainBlocks());
}; };
render () { render() {
const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props; const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props;
const isLoading = server.get('isLoading'); const isLoading = server.get('isLoading');
const fedibirdCapabilities = server.get('fedibird_capabilities') || []; // thinking about isLoading is true const fedibirdCapabilities = server.get('fedibird_capabilities') || [];
const isPublicUnlistedVisibility = fedibirdCapabilities.includes('kmyblue_visibility_public_unlisted'); const isPublicUnlistedVisibility = fedibirdCapabilities.includes('kmyblue_visibility_public_unlisted');
const isPublicVisibility = !fedibirdCapabilities.includes('kmyblue_no_public_visibility'); const isPublicVisibility = !fedibirdCapabilities.includes('kmyblue_no_public_visibility');
const isEmojiReaction = fedibirdCapabilities.includes('emoji_reaction'); const isEmojiReaction = fedibirdCapabilities.includes('emoji_reaction');
@ -169,59 +187,88 @@ class About extends PureComponent {
return ( return (
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}> <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
<div className='scrollable about'> <div className="scrollable about">
<div className='about__header'> <div className="about__header">
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> <ServerHeroImage
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1> blurhash={server.getIn(['thumbnail', 'blurhash'])}
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank' rel='noopener'>Mastodon</a> }} /></p> src={server.getIn(['thumbnail', 'url'])}
srcSet={server
.getIn(['thumbnail', 'versions'])
?.map((value, key) => `${value} ${key.replace('@', '')}`)
.join(', ')}
className="about__header__hero"
/>
<h1>{isLoading ? <Skeleton width="10ch" /> : server.get('domain')}</h1>
<p>
<FormattedMessage id="about.powered_by" defaultMessage="Social media powered by You!" />
</p>
</div> </div>
<div className='about__meta'> <div className="about__meta">
<div className='about__meta__column'> <div className="about__meta__column">
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4> <h4>
<FormattedMessage id="server_banner.administered_by" defaultMessage="Administered by:" />
</h4>
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal /> <Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
</div> </div>
<hr className='about__meta__divider' /> <hr className="about__meta__divider" />
<div className='about__meta__column'> <div className="about__meta__column">
<h4><FormattedMessage id='about.contact' defaultMessage='Contact:' /></h4> <h4>
<FormattedMessage id="about.contact" defaultMessage="Contact:" />
</h4>
{isLoading ? <Skeleton width='10ch' /> : <a className='about__mail' href={emailLink}>{server.getIn(['contact', 'email'])}</a>} {isLoading ? (
<Skeleton width="10ch" />
) : (
<a className="about__mail" href={emailLink}>
{server.getIn(['contact', 'email'])}
</a>
)}
</div> </div>
</div> </div>
<Section open title={intl.formatMessage(messages.title)}> <Section open title={intl.formatMessage(messages.title)}>
{extendedDescription.get('isLoading') ? ( {extendedDescription.get('isLoading') ? (
<> <>
<Skeleton width='100%' /> <Skeleton width="100%" />
<br /> <br />
<Skeleton width='100%' /> <Skeleton width="100%" />
<br /> <br />
<Skeleton width='100%' /> <Skeleton width="100%" />
<br /> <br />
<Skeleton width='70%' /> <Skeleton width="70%" />
</> </>
) : (extendedDescription.get('content')?.length > 0 ? ( ) : extendedDescription.get('content')?.length > 0 ? (
<div <div className="prose" dangerouslySetInnerHTML={{ __html: extendedDescription.get('content') }} />
className='prose'
dangerouslySetInnerHTML={{ __html: extendedDescription.get('content') }}
/>
) : ( ) : (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p> <p>
))} <FormattedMessage
id="about.not_available"
defaultMessage="This information has not been made available on this server."
/>
</p>
)}
</Section> </Section>
<Section title={intl.formatMessage(messages.rules)}> <Section title={intl.formatMessage(messages.rules)}>
{!isLoading && (server.get('rules', ImmutableList()).isEmpty() ? ( {!isLoading && (server.get('rules', ImmutableList()).isEmpty() ? (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p> <p>
<FormattedMessage
id="about.not_available"
defaultMessage="This information has not been made available on this server."
/>
</p>
) : ( ) : (
<ol className='rules-list'> <ol className="rules-list">
{server.get('rules').map(rule => ( {server.get('rules').map((rule) => (
<li key={rule.get('id')}> <li key={rule.get('id')}>
<div className='rules-list__text'>{rule.get('text')}</div> <div className="rules-list__text">{rule.get('text')}</div>
{rule.get('hint').length > 0 && (<div className='rules-list__hint'>{rule.get('hint')}</div>)} {!!rule.get('hint') && rule.get('hint').length > 0 && (
<div className="rules-list__hint">{rule.get('hint')}</div>
)}
</li> </li>
))} ))}
</ol> </ol>
@ -229,23 +276,38 @@ class About extends PureComponent {
</Section> </Section>
<Section title={intl.formatMessage(messages.capabilities)}> <Section title={intl.formatMessage(messages.capabilities)}>
<p><FormattedMessage id='about.kmyblue_capability' defaultMessage='This server is using kmyblue, a fork of Mastodon. On this server, kmyblues unique features are configured as follows.' /></p> <p>
<FormattedMessage
id="about.kmyblue_capability"
defaultMessage="This server is using unique features are configured as follows."
/>
</p>
{!isLoading && ( {!isLoading && (
<ol className='rules-list'> <ol className="rules-list">
<li> <li>
<span className='rules-list__text'>{intl.formatMessage(messages.emojiReaction)}: <CapabilityIcon state={isEmojiReaction} intl={intl} /></span> <span className="rules-list__text">
{intl.formatMessage(messages.emojiReaction)}: <CapabilityIcon state={isEmojiReaction} intl={intl} />
</span>
</li> </li>
<li> <li>
<span className='rules-list__text'>{intl.formatMessage(messages.publicVisibility)}: <CapabilityIcon state={isPublicVisibility} intl={intl} /></span> <span className="rules-list__text">
{intl.formatMessage(messages.publicVisibility)}: <CapabilityIcon state={isPublicVisibility} intl={intl} />
</span>
</li> </li>
<li> <li>
<span className='rules-list__text'>{intl.formatMessage(messages.publicUnlistedVisibility)}: <CapabilityIcon state={isPublicUnlistedVisibility} intl={intl} /></span> <span className="rules-list__text">
{intl.formatMessage(messages.publicUnlistedVisibility)}: <CapabilityIcon state={isPublicUnlistedVisibility} intl={intl} />
</span>
</li> </li>
<li> <li>
<span className='rules-list__text'>{intl.formatMessage(messages.localTimeline)}: <CapabilityIcon state={isLocalTimeline} intl={intl} /></span> <span className="rules-list__text">
{intl.formatMessage(messages.localTimeline)}: <CapabilityIcon state={isLocalTimeline} intl={intl} />
</span>
</li> </li>
<li> <li>
<span className='rules-list__text'>{intl.formatMessage(messages.fullTextSearch)}: <CapabilityIcon state={isFullTextSearch} intl={intl} /></span> <span className="rules-list__text">
{intl.formatMessage(messages.fullTextSearch)}: <CapabilityIcon state={isFullTextSearch} intl={intl} />
</span>
</li> </li>
</ol> </ol>
)} )}
@ -254,49 +316,75 @@ class About extends PureComponent {
<Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}> <Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}>
{domainBlocks.get('isLoading') ? ( {domainBlocks.get('isLoading') ? (
<> <>
<Skeleton width='100%' /> <Skeleton width="100%" />
<br /> <br />
<Skeleton width='70%' /> <Skeleton width="70%" />
</> </>
) : (domainBlocks.get('isAvailable') ? ( ) : domainBlocks.get('isAvailable') ? (
<> <>
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p> <p>
<FormattedMessage
id="about.domain_blocks.preamble"
defaultMessage="Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server."
/>
</p>
{domainBlocks.get('items').size > 0 && ( {domainBlocks.get('items').size > 0 && (
<div className='about__domain-blocks'> <div className="about__domain-blocks">
{domainBlocks.get('items').map(block => ( {domainBlocks.get('items').map((block) => (
<div className='about__domain-blocks__domain' key={block.get('domain')}> <div className="about__domain-blocks__domain" key={block.get('domain')}>
<div className='about__domain-blocks__domain__header'> <div className="about__domain-blocks__domain__header">
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6> <h6>
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity_ex') || block.get('severity')].title)}</span> <span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span>
</h6>
<span
className="about__domain-blocks__domain__type"
title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}
>
{intl.formatMessage(
severityMessages[block.get('severity_ex') || block.get('severity')].title
)}
</span>
</div> </div>
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p> <p>
{(block.get('comment') || '').length > 0 ? (
block.get('comment')
) : (
<FormattedMessage id="about.domain_blocks.no_reason_available" defaultMessage="Reason not available" />
)}
</p>
</div> </div>
))} ))}
</div> </div>
)} )}
</> </>
) : ( ) : (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p> <p>
))} <FormattedMessage
id="about.not_available"
defaultMessage="This information has not been made available on this server."
/>
</p>
)}
</Section> </Section>
<LinkFooter /> <LinkFooter />
<div className='about__footer'> <div className="about__footer">
<p><FormattedMessage id='about.disclaimer' defaultMessage='Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.' /></p> <p>
<FormattedMessage {...messages.joinFediverse} />
</p>
</div> </div>
</div> </div>
<Helmet> <Helmet>
<title>{intl.formatMessage(messages.title)}</title> <title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='all' /> <meta name="robots" content="all" />
</Helmet> </Helmet>
</Column> </Column>
); );
} }
} }
export default connect(mapStateToProps)(injectIntl(About)); export default connect(mapStateToProps)(injectIntl(About));

View file

@ -14,6 +14,8 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react'; import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react'; import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react';
import HashtagIcon from '@/material-icons/400-24px/tag.svg?react';
import Directory from '@/material-icons/400-24px/group.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
@ -42,6 +44,8 @@ const messages = defineMessages({
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed Hashtags' },
directory: { id: 'navigation_bar.directory', defaultMessage: 'Profile directory' },
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
deep_timeline: { id: 'navigation_bar.deep_timeline', defaultMessage: 'Deep timeline' }, deep_timeline: { id: 'navigation_bar.deep_timeline', defaultMessage: 'Deep timeline' },
@ -144,6 +148,7 @@ class GettingStarted extends ImmutablePureComponent {
<ColumnLink key='direct' icon='at' iconComponent={AlternateEmailIcon} text={intl.formatMessage(messages.direct)} to='/conversations' />, <ColumnLink key='direct' icon='at' iconComponent={AlternateEmailIcon} text={intl.formatMessage(messages.direct)} to='/conversations' />,
<ColumnLink key='bookmark' icon='bookmarks' iconComponent={BookmarksIcon} text={intl.formatMessage(messages.bookmarks)} to='/bookmark_categories' />, <ColumnLink key='bookmark' icon='bookmarks' iconComponent={BookmarksIcon} text={intl.formatMessage(messages.bookmarks)} to='/bookmark_categories' />,
<ColumnLink key='favourites' icon='star' iconComponent={StarIcon} text={intl.formatMessage(messages.favourites)} to='/favourites' />, <ColumnLink key='favourites' icon='star' iconComponent={StarIcon} text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='followed_tags' icon='tag' iconComponent={followed_tagsIcon} text={intl.formatMessage(messages.followed_tags)} to='/followed_tags' />,
<ColumnLink key='lists' icon='list-ul' iconComponent={ListAltIcon} text={intl.formatMessage(messages.lists)} to='/lists' />, <ColumnLink key='lists' icon='list-ul' iconComponent={ListAltIcon} text={intl.formatMessage(messages.lists)} to='/lists' />,
<ColumnLink key='antennas' icon='wifi' iconComponent={AntennaIcon} text={intl.formatMessage(messages.antennas)} to='/antennas' />, <ColumnLink key='antennas' icon='wifi' iconComponent={AntennaIcon} text={intl.formatMessage(messages.antennas)} to='/antennas' />,
<ColumnLink key='circles' icon='user-circle' iconComponent={CirclesIcon} text={intl.formatMessage(messages.circles)} to='/circles' />, <ColumnLink key='circles' icon='user-circle' iconComponent={CirclesIcon} text={intl.formatMessage(messages.circles)} to='/circles' />,

View file

@ -1,101 +1,63 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import {
domain, domain,
version,
source_url, source_url,
statusPageUrl, statusPageUrl,
profile_directory as canProfileDirectory,
termsOfServiceEnabled, termsOfServiceEnabled,
} from 'mastodon/initial_state'; } from 'mastodon/initial_state';
const DividingCircle: React.FC = () => <span aria-hidden>{' · '}</span>; const DividingCircle: React.FC = () => <span aria-hidden={true}>{' · '}</span>;
export const LinkFooter: React.FC<{ export const LinkFooter: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
multiColumn: boolean;
}> = ({ multiColumn }) => {
return ( return (
<div className='link-footer'> <footer className='link-footer' role='contentinfo'>
<p> <p>
<strong>{domain}</strong>:{' '} <strong>{domain}</strong>:{' '}
<Link to='/about' target={multiColumn ? '_blank' : undefined}> <Link to='/about' target={multiColumn ? '_blank' : undefined}>
<FormattedMessage id='footer.about' defaultMessage='About' /> <FormattedMessage id='footer.about' defaultMessage='About' />
</Link> </Link>
{statusPageUrl && ( {statusPageUrl && (
<> <>
<DividingCircle /> <DividingCircle />
<a href={statusPageUrl} target='_blank' rel='noopener'> <a href={statusPageUrl} target='_blank' rel='noopener noreferrer'>
<FormattedMessage id='footer.status' defaultMessage='Status' /> <FormattedMessage id='footer.status' defaultMessage='Status' />
</a> </a>
</> </>
)} )}
{canProfileDirectory && (
<>
<DividingCircle /> <DividingCircle />
<Link to='/directory'> <Link to='/privacy-policy' target={multiColumn ? '_blank' : undefined}>
<FormattedMessage <FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' />
id='footer.directory'
defaultMessage='Profiles directory'
/>
</Link>
</>
)}
<DividingCircle />
<Link
to='/privacy-policy'
target={multiColumn ? '_blank' : undefined}
rel='privacy-policy'
>
<FormattedMessage
id='footer.privacy_policy'
defaultMessage='Privacy policy'
/>
</Link> </Link>
{termsOfServiceEnabled && ( {termsOfServiceEnabled && (
<> <>
<DividingCircle /> <DividingCircle />
<Link <Link to='/terms-of-service' target={multiColumn ? '_blank' : undefined}>
to='/terms-of-service' <FormattedMessage id='footer.terms_of_service' defaultMessage='Terms of service' />
target={multiColumn ? '_blank' : undefined}
rel='terms-of-service'
>
<FormattedMessage
id='footer.terms_of_service'
defaultMessage='Terms of service'
/>
</Link> </Link>
</> </>
)} )}
<DividingCircle />
<Link to='/keyboard-shortcuts'>
<FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' />
</Link>
<DividingCircle />
<a href={source_url} rel='noopener noreferrer' target='_blank'>
<FormattedMessage id='footer.source_code' defaultMessage='View source code' />
</a>
</p> </p>
<p> <p>
<strong>Mastodon</strong>:{' '} <span title="Crafted with love for the fediverse">
<a href='https://joinmastodon.org' target='_blank' rel='noopener'> Made with <span aria-label='heart' role='img'></span>
<FormattedMessage id='footer.about' defaultMessage='About' /> </span>
</a>
<DividingCircle />
<a href='https://joinmastodon.org/apps' target='_blank' rel='noopener'>
<FormattedMessage id='footer.get_app' defaultMessage='Get the app' />
</a>
<DividingCircle />
<Link to='/keyboard-shortcuts'>
<FormattedMessage
id='footer.keyboard_shortcuts'
defaultMessage='Keyboard shortcuts'
/>
</Link>
<DividingCircle />
<a href={source_url} rel='noopener' target='_blank'>
<FormattedMessage
id='footer.source_code'
defaultMessage='View source code'
/>
</a>
<DividingCircle />
<span className='version'>v{version}</span>
</p> </p>
</div> </footer>
); );
}; };

View file

@ -1,10 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Component, useEffect } from 'react'; import { Component, useEffect } from 'react';
import { defineMessages, injectIntl, useIntl } from 'react-intl'; import { defineMessages, injectIntl, useIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import CirclesIcon from '@/material-icons/400-24px/account_circle-fill.svg?react'; import CirclesIcon from '@/material-icons/400-24px/account_circle-fill.svg?react';
@ -13,6 +10,8 @@ import BookmarksActiveIcon from '@/material-icons/400-24px/bookmarks-fill.svg?re
import BookmarksIcon from '@/material-icons/400-24px/bookmarks.svg?react'; import BookmarksIcon from '@/material-icons/400-24px/bookmarks.svg?react';
import ExploreActiveIcon from '@/material-icons/400-24px/explore-fill.svg?react'; import ExploreActiveIcon from '@/material-icons/400-24px/explore-fill.svg?react';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import HashtagIcon from '@/material-icons/400-24px/tag.svg?react';
import DirectoryIcon from '@/material-icons/400-24px/group.svg?react';
import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react'; import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react'; import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react';
@ -31,6 +30,7 @@ import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
import StarActiveIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarActiveIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react'; import StarIcon from '@/material-icons/400-24px/star.svg?react';
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react'; import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
import { fetchFollowRequests } from 'mastodon/actions/accounts'; import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { IconWithBadge } from 'mastodon/components/icon_with_badge'; import { IconWithBadge } from 'mastodon/components/icon_with_badge';
import { WordmarkLogo } from 'mastodon/components/logo'; import { WordmarkLogo } from 'mastodon/components/logo';
@ -50,6 +50,8 @@ const messages = defineMessages({
home: { id: 'tabs_bar.home', defaultMessage: 'Home' }, home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
explore: { id: 'explore.title', defaultMessage: 'Explore' }, explore: { id: 'explore.title', defaultMessage: 'Explore' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
directory: { id: 'navigation_bar.directory', defaultMessage: 'Profiles directory' },
local: { id: 'column.local', defaultMessage: 'Local' }, local: { id: 'column.local', defaultMessage: 'Local' },
deepLocal: { id: 'column.deep_local', defaultMessage: 'Deep' }, deepLocal: { id: 'column.deep_local', defaultMessage: 'Deep' },
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' }, firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
@ -71,7 +73,6 @@ const messages = defineMessages({
}); });
const NotificationsLink = () => { const NotificationsLink = () => {
const count = useSelector(selectUnreadNotificationGroupsCount); const count = useSelector(selectUnreadNotificationGroupsCount);
const intl = useIntl(); const intl = useIntl();
@ -129,25 +130,20 @@ class NavigationPanel extends Component {
const { intl } = this.props; const { intl } = this.props;
const { signedIn, disabledAccountId, permissions } = this.props.identity; const { signedIn, disabledAccountId, permissions } = this.props.identity;
const explorer = (trendsEnabled ? ( const explorer = trendsEnabled ? (
<ColumnLink transparent to='/explore' icon='explore' iconComponent={ExploreIcon} activeIconComponent={ExploreActiveIcon} text={intl.formatMessage(messages.explore)} /> <ColumnLink transparent to='/explore' icon='explore' iconComponent={ExploreIcon} activeIconComponent={ExploreActiveIcon} text={intl.formatMessage(messages.explore)} />
) : ( ) : (
<ColumnLink transparent to='/search' icon='search' iconComponent={SearchIcon} text={intl.formatMessage(messages.search)} /> <ColumnLink transparent to='/search' icon='search' iconComponent={SearchIcon} text={intl.formatMessage(messages.search)} />
)); );
let banner = undefined; const banner = transientSingleColumn ? (
if (transientSingleColumn) {
banner = (
<div className='switch-to-advanced'> <div className='switch-to-advanced'>
{intl.formatMessage(messages.openedInClassicInterface)} {intl.formatMessage(messages.openedInClassicInterface)}{' '}
{" "}
<a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'> <a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
{intl.formatMessage(messages.advancedInterface)} {intl.formatMessage(messages.advancedInterface)}
</a> </a>
</div> </div>
); ) : null;
}
return ( return (
<div className='navigation-panel'> <div className='navigation-panel'>
@ -155,90 +151,59 @@ class NavigationPanel extends Component {
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link> <Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
</div> </div>
{banner && {banner && <div className='navigation-panel__banner'>{banner}</div>}
<div className='navigation-panel__banner'>
{banner}
</div>
}
<div className='navigation-panel__menu'> <div className='navigation-panel__menu'>
{signedIn && ( {signedIn && (
<> <>
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} /> <ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} />
<NotificationsLink /> <NotificationsLink />
</> {enableLocalTimeline && <ColumnLink transparent to='/public/local/fixed' icon='users' iconComponent={PeopleIcon} text={intl.formatMessage(messages.local)} />}
)} {enableDtlMenu && dtlTag && <ColumnLink transparent to={`/tags/${dtlTag}`} icon='users' iconComponent={PeopleIcon} text={intl.formatMessage(messages.deepLocal)} />}
{signedIn && enableLocalTimeline && (
<ColumnLink transparent to='/public/local/fixed' icon='users' iconComponent={PeopleIcon} text={intl.formatMessage(messages.local)} />
)}
{signedIn && enableDtlMenu && dtlTag && (
<ColumnLink transparent to={`/tags/${dtlTag}`} icon='users' iconComponent={PeopleIcon} text={intl.formatMessage(messages.deepLocal)} />
)}
{!signedIn && explorer}
{signedIn && (
<ColumnLink transparent to='/public' isActive={this.isFirehoseActive} icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.firehose)} /> <ColumnLink transparent to='/public' isActive={this.isFirehoseActive} icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.firehose)} />
)}
{(!signedIn && timelinePreview) && (
<ColumnLink transparent to={enableLocalTimeline ? '/public/local' : '/public'} isActive={this.isFirehoseActive} icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.firehose)} />
)}
{signedIn && (
<>
<ListPanel /> <ListPanel />
<hr /> <hr />
</>
)}
{signedIn && (
<>
<ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={intl.formatMessage(messages.lists)} /> <ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={intl.formatMessage(messages.lists)} />
<ColumnLink transparent to='/antennas' icon='wifi' iconComponent={AntennaIcon} text={intl.formatMessage(messages.antennas)} isActive={this.isAntennasActive} /> <ColumnLink transparent to='/antennas' icon='wifi' iconComponent={AntennaIcon} text={intl.formatMessage(messages.antennas)} isActive={this.isAntennasActive} />
<ColumnLink transparent to='/circles' icon='user-circle' iconComponent={CirclesIcon} text={intl.formatMessage(messages.circles)} /> <ColumnLink transparent to='/circles' icon='user-circle' iconComponent={CirclesIcon} text={intl.formatMessage(messages.circles)} />
<ColumnLink transparent to='/followed_tags' icon='tag' iconComponent={HashtagIcon} text={intl.formatMessage(messages.followed_tags)} />
<ColumnLink transparent to='/directory' icon='group' iconComponent={DirectoryIcon} text={intl.formatMessage(messages.directory)} />
<FollowRequestsLink /> <FollowRequestsLink />
<ColumnLink transparent to='/conversations' icon='at' iconComponent={AlternateEmailIcon} text={intl.formatMessage(messages.direct)} /> <ColumnLink transparent to='/conversations' icon='at' iconComponent={AlternateEmailIcon} text={intl.formatMessage(messages.direct)} />
</> {explorer}
)}
{signedIn && explorer}
{signedIn && (
<>
<ColumnLink transparent to='/bookmark_categories' icon='bookmarks' iconComponent={BookmarksIcon} activeIconComponent={BookmarksActiveIcon} text={intl.formatMessage(messages.bookmarks)} /> <ColumnLink transparent to='/bookmark_categories' icon='bookmarks' iconComponent={BookmarksIcon} activeIconComponent={BookmarksActiveIcon} text={intl.formatMessage(messages.bookmarks)} />
{ !isHideItem('favourite_menu') && <ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} activeIconComponent={StarActiveIcon} text={intl.formatMessage(messages.favourites)} /> } {!isHideItem('favourite_menu') && <ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} activeIconComponent={StarActiveIcon} text={intl.formatMessage(messages.favourites)} />}
<hr /> <hr />
<ColumnLink transparent href='/settings/preferences' icon='cog' iconComponent={SettingsIcon} text={intl.formatMessage(messages.preferences)} /> <ColumnLink transparent href='/settings/preferences' icon='cog' iconComponent={SettingsIcon} text={intl.formatMessage(messages.preferences)} />
{canManageReports(permissions) && <ColumnLink transparent href='/admin/reports' icon='flag' iconComponent={ModerationIcon} text={intl.formatMessage(messages.moderation)} />} {canManageReports(permissions) && <ColumnLink transparent href='/admin/reports' icon='flag' iconComponent={ModerationIcon} text={intl.formatMessage(messages.moderation)} />}
{canViewAdminDashboard(permissions) && <ColumnLink transparent href='/admin/dashboard' icon='tachometer' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.administration)} />} {canViewAdminDashboard(permissions) && <ColumnLink transparent href='/admin/dashboard' icon='tachometer' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.administration)} />}
</> </>
)} )}
{!signedIn && ( {!signedIn && (
<>
{explorer}
{(timelinePreview || enableLocalTimeline) && (
<ColumnLink transparent to={enableLocalTimeline ? '/public/local' : '/public'} isActive={this.isFirehoseActive} icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.firehose)} />
)}
<div className='navigation-panel__sign-in-banner'> <div className='navigation-panel__sign-in-banner'>
<hr /> <hr />
{ disabledAccountId ? <DisabledAccountBanner /> : <SignInBanner /> } {disabledAccountId ? <DisabledAccountBanner /> : <SignInBanner />}
</div> </div>
</>
)} )}
</div>
<div className='navigation-panel__legal'> <div className='navigation-panel__legal'>
<hr /> <hr />
<ColumnLink transparent to='/about' icon='ellipsis-h' iconComponent={MoreHorizIcon} text={intl.formatMessage(messages.about)} /> <ColumnLink transparent to='/about' icon='ellipsis-h' iconComponent={MoreHorizIcon} text={intl.formatMessage(messages.about)} />
</div> </div>
</div>
<div className='flex-spacer' /> <div className='flex-spacer' />
<NavigationPortal /> <NavigationPortal />
</div> </div>
); );
} }
} }
export default injectIntl(withIdentity(NavigationPanel)); export default injectIntl(withIdentity(NavigationPanel));

View file

@ -22,7 +22,7 @@ const SignInBanner = () => {
if (sso_redirect) { if (sso_redirect) {
return ( return (
<div className='sign-in-banner'> <div className='sign-in-banner'>
<p><strong><FormattedMessage id='sign_in_banner.mastodon_is' defaultMessage="Mastodon is the best way to keep up with what's happening." /></strong></p> <p><strong><FormattedMessage id='sign_in_banner.mastodon_is' defaultMessage="Join the Fediverse, become part of a community, and break free from Big Tech™'s stranglehold on public discourse." /></strong></p>
<p><FormattedMessage id='sign_in_banner.follow_anyone' defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.' /></p> <p><FormattedMessage id='sign_in_banner.follow_anyone' defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.' /></p>
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a> <a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
</div> </div>
@ -45,7 +45,7 @@ const SignInBanner = () => {
return ( return (
<div className='sign-in-banner'> <div className='sign-in-banner'>
<p><strong><FormattedMessage id='sign_in_banner.mastodon_is' defaultMessage="Mastodon is the best way to keep up with what's happening." /></strong></p> <p><strong><FormattedMessage id='sign_in_banner.mastodon_is' defaultMessage="Join the Fediverse, become part of a community, and break free from Big Tech™'s stranglehold on public discourse." /></strong></p>
<p><FormattedMessage id='sign_in_banner.follow_anyone' defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.' /></p> <p><FormattedMessage id='sign_in_banner.follow_anyone' defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.' /></p>
{signupButton} {signupButton}
<a href='/auth/sign_in' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a> <a href='/auth/sign_in' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>

View file

@ -1,7 +1,7 @@
{ {
"about.blocks": "Moderated servers", "about.blocks": "Moderated servers",
"about.contact": "Contact:", "about.contact": "Contact:",
"about.disclaimer": "Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.", "about.disclaimer": "Join the Fediverse, become part of a community, and break free from Big Tech™'s stranglehold on public discourse.",
"about.domain_blocks.no_reason_available": "Reason not available", "about.domain_blocks.no_reason_available": "Reason not available",
"about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the Fediverse. These are the exceptions that have been made on this particular server.", "about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the Fediverse. These are the exceptions that have been made on this particular server.",
"about.domain_blocks.silenced.explanation": "You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.", "about.domain_blocks.silenced.explanation": "You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.",
@ -802,11 +802,11 @@
"server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)", "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)",
"server_banner.active_users": "active users", "server_banner.active_users": "active users",
"server_banner.administered_by": "Administered by:", "server_banner.administered_by": "Administered by:",
"server_banner.is_one_of_many": "{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.", "server_banner.is_one_of_many": "{domain} is one of the many independent servers you can use to participate in the fediverse.",
"server_banner.server_stats": "Server stats:", "server_banner.server_stats": "Server stats:",
"sign_in_banner.create_account": "Create account", "sign_in_banner.create_account": "Create account",
"sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.", "sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.",
"sign_in_banner.mastodon_is": "Mastodon is the best way to keep up with what's happening.", "sign_in_banner.mastodon_is": "Join the Fediverse, become part of a community, and break free from Big Tech™'s stranglehold on public discourse.",
"sign_in_banner.sign_in": "Sign in", "sign_in_banner.sign_in": "Sign in",
"sign_in_banner.sso_redirect": "Login or Register", "sign_in_banner.sso_redirect": "Login or Register",
"status.admin_account": "Open moderation interface for @{name}", "status.admin_account": "Open moderation interface for @{name}",

View file

@ -2,7 +2,7 @@
"about.blocks": "Moderated servers", "about.blocks": "Moderated servers",
"about.contact": "Contact:", "about.contact": "Contact:",
"about.disabled": "Disabled", "about.disabled": "Disabled",
"about.disclaimer": "Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.", "about.disclaimer": "Join the Fediverse, become part of a community, and break free from Big Tech™'s stranglehold on public discourse.",
"about.domain_blocks.no_reason_available": "Reason not available", "about.domain_blocks.no_reason_available": "Reason not available",
"about.domain_blocks.noop.explanation": "This server is limited partically.", "about.domain_blocks.noop.explanation": "This server is limited partically.",
"about.domain_blocks.noop.title": "Soft limited", "about.domain_blocks.noop.title": "Soft limited",
@ -14,9 +14,9 @@
"about.enabled": "Enabled", "about.enabled": "Enabled",
"about.full_text_search": "Full text search", "about.full_text_search": "Full text search",
"about.kmyblue_capabilities": "Features available in this server", "about.kmyblue_capabilities": "Features available in this server",
"about.kmyblue_capability": "This server is using kmyblue, a fork of Mastodon. On this server, kmyblues unique features are configured as follows.", "about.kmyblue_capability": "Server unique features are configured as follows.",
"about.not_available": "This information has not been made available on this server.", "about.not_available": "This information has not been made available on this server.",
"about.powered_by": "Decentralized social media powered by {mastodon}", "about.powered_by": "Social media powered by You!",
"about.public_visibility": "Public visibility", "about.public_visibility": "Public visibility",
"about.rules": "Server rules", "about.rules": "Server rules",
"account.account_note_header": "Personal note", "account.account_note_header": "Personal note",
@ -1000,11 +1000,11 @@
"server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)", "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)",
"server_banner.active_users": "active users", "server_banner.active_users": "active users",
"server_banner.administered_by": "Administered by:", "server_banner.administered_by": "Administered by:",
"server_banner.is_one_of_many": "{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.", "server_banner.is_one_of_many": "{domain} is one of the many independent servers you can use to participate in the fediverse.",
"server_banner.server_stats": "Server stats:", "server_banner.server_stats": "Server stats:",
"sign_in_banner.create_account": "Create account", "sign_in_banner.create_account": "Create account",
"sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.", "sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.",
"sign_in_banner.mastodon_is": "Mastodon is the best way to keep up with what's happening.", "sign_in_banner.mastodon_is": "Join the Fediverse, become part of a community, and break free from Big Tech™'s stranglehold on public discourse.",
"sign_in_banner.sign_in": "Login", "sign_in_banner.sign_in": "Login",
"sign_in_banner.sso_redirect": "Login or Register", "sign_in_banner.sso_redirect": "Login or Register",
"status.admin_account": "Open moderation interface for @{name}", "status.admin_account": "Open moderation interface for @{name}",

View file

@ -1,7 +1,7 @@
{ {
"about.blocks": "Moderatit servers", "about.blocks": "Moderatit servers",
"about.contact": "Contack:", "about.contact": "Contack:",
"about.disclaimer": "Mastodon is free, open-soorced saftware, an a trademairk o Mastodon gGmbH.", "about.disclaimer": "Join the Fediverse, become part of a community, and break free from Big Tech™'s stranglehold on public discourse.",
"about.domain_blocks.no_reason_available": "Raison no available", "about.domain_blocks.no_reason_available": "Raison no available",
"about.domain_blocks.preamble": "On the hail, Mastodon lats ye view content frae an interack wi uisers fae onie ither server in the fediverse.", "about.domain_blocks.preamble": "On the hail, Mastodon lats ye view content frae an interack wi uisers fae onie ither server in the fediverse.",
"about.domain_blocks.silenced.explanation": "Ye'll generally no see profiles an content frae this server, unless ye explicitly luik it up or opt intae it bi follaein.", "about.domain_blocks.silenced.explanation": "Ye'll generally no see profiles an content frae this server, unless ye explicitly luik it up or opt intae it bi follaein.",

View file

@ -178,5 +178,10 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
), ),
note_emojified: emojify(accountJSON.note, emojiMap), note_emojified: emojify(accountJSON.note, emojiMap),
note_plain: unescapeHTML(accountJSON.note), note_plain: unescapeHTML(accountJSON.note),
url:
accountJSON.url.startsWith('http://') ||
accountJSON.url.startsWith('https://')
? accountJSON.url
: accountJSON.uri,
}); });
} }

View file

@ -1,11 +1,14 @@
$classic-base-color: #282c37; // Midnight Express $classic-base-color: #282c37; // Midnight Express
$classic-secondary-color: #d9e1e8; // Pattens Blue $classic-secondary-color: #d9e1e8; // Pattens Blue
// Variables for defaults in UI @use '../mastodon/variables' with (
$simple-background-color: $classic-base-color !default; // Variables for defaults in UI
$simple-background-color: $classic-base-color,
// Tell UI to use selected colors // Tell UI to use selected colors
$ui-base-lighter-color: #969fbc !default; // Lighter darkest $ui-base-lighter-color: #969fbc,
// For texts on inverted backgrounds // Lighter darkest
$inverted-text-color: $classic-secondary-color !default; // For texts on inverted backgrounds
$inverted-text-color: $classic-secondary-color
);

View file

@ -0,0 +1,7 @@
// Mastodon Modern theme by Freeplay! Check the original repo for more info: https://git.gay/freeplay/Mastodon-Modern
// Everything in the "modern" directory is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License: http://creativecommons.org/licenses/by-sa/4.0/
@use 'contrast/variables';
@use 'application';
@use 'modern/style';
@use 'contrast/diff';

View file

@ -0,0 +1,6 @@
// Mastodon Modern theme by Freeplay! Check the original repo for more info: https://git.gay/freeplay/Mastodon-Modern
// Everything in the "modern" directory is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License: http://creativecommons.org/licenses/by-sa/4.0/
@use 'mastodon/variables';
@use 'application';
@use 'modern/style';

View file

@ -0,0 +1,8 @@
// Mastodon Modern theme by Freeplay! Check the original repo for more info: https://git.gay/freeplay/Mastodon-Modern
// Everything in the "modern" directory is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License: http://creativecommons.org/licenses/by-sa/4.0/
@use 'mastodon-light/variables';
@use 'mastodon-light/css_variables';
@use 'application';
@use 'modern/style';
@use 'mastodon-light/diff';

File diff suppressed because it is too large Load diff

View file

@ -15,13 +15,15 @@ class ActivityPub::Parser::MediaAttachmentParser
end end
def remote_url def remote_url
Addressable::URI.parse(@json['url'])&.normalize&.to_s url = Addressable::URI.parse(@json['url'])&.normalize&.to_s
url unless unsupported_uri_scheme?(url)
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
nil nil
end end
def thumbnail_remote_url def thumbnail_remote_url
Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s url = Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s
url unless unsupported_uri_scheme?(url)
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
nil nil
end end

View file

@ -33,7 +33,10 @@ class ActivityPub::Parser::StatusParser
end end
def url def url
url_to_href(@object['url'], 'text/html') if @object['url'].present? return if @object['url'].blank?
url = url_to_href(@object['url'], 'text/html')
url unless unsupported_uri_scheme?(url)
end end
def text def text

View file

@ -4,6 +4,7 @@ require 'singleton'
class ActivityPub::TagManager class ActivityPub::TagManager
include Singleton include Singleton
include JsonLdHelper
include RoutingHelper include RoutingHelper
CONTEXT = 'https://www.w3.org/ns/activitystreams' CONTEXT = 'https://www.w3.org/ns/activitystreams'
@ -17,7 +18,7 @@ class ActivityPub::TagManager
end end
def url_for(target) def url_for(target)
return target.url if target.respond_to?(:local?) && !target.local? return unsupported_uri_scheme?(target.url) ? nil : target.url if target.respond_to?(:local?) && !target.local?
return unless target.respond_to?(:object_type) return unless target.respond_to?(:object_type)

View file

@ -6,14 +6,13 @@
class HttpSignatureDraft class HttpSignatureDraft
REQUEST_TARGET = '(request-target)' REQUEST_TARGET = '(request-target)'
def initialize(keypair, key_id, full_path: true) def initialize(keypair, key_id)
@keypair = keypair @keypair = keypair
@key_id = key_id @key_id = key_id
@full_path = full_path
end end
def request_target(verb, url) def request_target(verb, url)
if url.query.nil? || !@full_path if url.query.nil?
"#{verb} #{url.path}" "#{verb} #{url.path}"
else else
"#{verb} #{url.path}?#{url.query}" "#{verb} #{url.path}?#{url.query}"

View file

@ -75,7 +75,6 @@ class Request
@url = Addressable::URI.parse(url).normalize @url = Addressable::URI.parse(url).normalize
@http_client = options.delete(:http_client) @http_client = options.delete(:http_client)
@allow_local = options.delete(:allow_local) @allow_local = options.delete(:allow_local)
@full_path = !options.delete(:omit_query_string)
@options = { @options = {
follow: { follow: {
max_hops: 3, max_hops: 3,
@ -102,7 +101,7 @@ class Request
key_id = ActivityPub::TagManager.instance.key_uri_for(actor) key_id = ActivityPub::TagManager.instance.key_uri_for(actor)
keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : actor.keypair keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : actor.keypair
@signing = HttpSignatureDraft.new(keypair, key_id, full_path: @full_path) @signing = HttpSignatureDraft.new(keypair, key_id)
self self
end end

View file

@ -57,20 +57,7 @@ class ActivityPub::FetchRepliesService < BaseService
return unless @allow_synchronous_requests return unless @allow_synchronous_requests
return if non_matching_uri_hosts?(@reference_uri, collection_or_uri) return if non_matching_uri_hosts?(@reference_uri, collection_or_uri)
# NOTE: For backward compatibility reasons, Mastodon signs outgoing
# queries incorrectly by default.
#
# While this is relevant for all URLs with query strings, this is
# the only code path where this happens in practice.
#
# Therefore, retry with correct signatures if this fails.
begin
fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary) fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary)
rescue Mastodon::UnexpectedResponseError => e
raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present?
fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary, request_options: { omit_query_string: false })
end
end end
def filter_replies(items) def filter_replies(items)

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class StatusLengthValidator < ActiveModel::Validator class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = 500 MAX_CHARS = (ENV['MAX_CHARS'] || 500).to_i
URL_PLACEHOLDER_CHARS = 23 URL_PLACEHOLDER_CHARS = 23
URL_PLACEHOLDER = 'x' * 23 URL_PLACEHOLDER = 'x' * 23

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
if ENV['REDIS_NAMESPACE']
es_configured = ENV['ES_ENABLED'] == 'true' || ENV.fetch('ES_HOST', 'localhost') != 'localhost' || ENV.fetch('ES_PORT', '9200') != '9200' || ENV.fetch('ES_PASS', 'password') != 'password'
warn <<~MESSAGE
WARNING: the REDIS_NAMESPACE environment variable is deprecated and will be removed in Mastodon 4.4.0.
Please see documentation at https://github.com/mastodon/redis_namespace_migration
MESSAGE
warn <<~MESSAGE if es_configured && !ENV['ES_PREFIX']
In addition, as REDIS_NAMESPACE is being used as a prefix for Elasticsearch, please do not forget to set ES_PREFIX to "#{ENV.fetch('REDIS_NAMESPACE')}".
MESSAGE
end

View file

@ -2280,7 +2280,10 @@ en:
themes: themes:
contrast: Mastodon (High contrast) contrast: Mastodon (High contrast)
default: Mastodon (Dark) default: Mastodon (Dark)
full-dark: フルダーク modern-dark: Modern Dark
modern-light: Modern Light
modern-contrast: Modern Contrast
full-dark: Full Dark
mastodon-light: Mastodon (Light) mastodon-light: Mastodon (Light)
system: Automatic (use system theme) system: Automatic (use system theme)
time: time:

View file

@ -66,18 +66,18 @@ en:
setting_custom_css_lead: 'Be sure to remember: In the unlikely event that you make a mistake in entering your custom CSS and the screen does not display properly, you can disable your custom CSS from the link at the bottom of the sign-in screen. Open the sign-in screen in private mode of your browser, for example, and disable it.' setting_custom_css_lead: 'Be sure to remember: In the unlikely event that you make a mistake in entering your custom CSS and the screen does not display properly, you can disable your custom CSS from the link at the bottom of the sign-in screen. Open the sign-in screen in private mode of your browser, for example, and disable it.'
setting_default_searchability: On kmyblue and Fedibird, the search is based on the search permission setting; on Misskey, all public, local public, and non-public posts are searched regardless of this setting; on Mastodon and Firefish, instead of search permission, the "Make public posts freely searchable on other servers" setting in the profile settings is applied. In Mastodon and Firefish, the "Make public posts freely searchable on other servers" setting in the profile settings is applied instead of the search permission. setting_default_searchability: On kmyblue and Fedibird, the search is based on the search permission setting; on Misskey, all public, local public, and non-public posts are searched regardless of this setting; on Mastodon and Firefish, instead of search permission, the "Make public posts freely searchable on other servers" setting in the profile settings is applied. In Mastodon and Firefish, the "Make public posts freely searchable on other servers" setting in the profile settings is applied instead of the search permission.
setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click
setting_disallow_unlisted_public_searchability: この設定を有効にすると、非収載投稿と検索範囲「誰でも」は両立できず不特定多数からの検索が不可になります。Fedibirdと同じ挙動になります setting_disallow_unlisted_public_searchability: If you enable this setting, unlisted posts and the “everyone” search scope cannot coexist, making it impossible for unspecified users to search your posts.
setting_display_media_default: Hide media marked as sensitive setting_display_media_default: Hide media marked as sensitive
setting_display_media_hide_all: Always hide media setting_display_media_hide_all: Always hide media
setting_display_media_show_all: Always show media setting_display_media_show_all: Always show media
setting_dtl_force_searchability: 'With using #%{tag} tag, your post settings will be changed forcibly' setting_dtl_force_searchability: 'With using #%{tag} tag, your post settings will be changed forcibly'
setting_dtl_force_visibility: 'With using #%{tag} tag, your post settings will be changed forcibly' setting_dtl_force_visibility: 'With using #%{tag} tag, your post settings will be changed forcibly'
setting_emoji_reaction_policy: Even with this setting, users on non-kmyblue servers are free to put their emoji reaction on the post and share it within the same server. If you simply want to remove the emoji reaction from your own screen, you can disable it from the appearance settings setting_emoji_reaction_policy: Even with this setting, users on non-kmyblue servers are free to put their emoji reaction on the post and share it within the same server. If you simply want to remove the emoji reaction from your own screen, you can disable it from the appearance settings
setting_emoji_reaction_streaming_notify_impl2: 当該サーバーの独自機能に対応したアプリを利用時に、絵文字リアクション機能を利用できます。動作確認していないため(そもそもそのようなアプリ自体を確認できていないため)正しく動かない場合があります setting_emoji_reaction_streaming_notify_impl2: You can use the emoji reaction feature when using an app that supports this servers unique functionality. However, since this has not been tested (and such apps have not even been identified), it may not work correctly.
setting_enable_emoji_reaction: If turn off, other users still can react your posts setting_enable_emoji_reaction: If turn off, other users still can react your posts
setting_enabled_visibilities: If turn off, you cannot select and post the privacy. setting_enabled_visibilities: If turn off, you cannot select and post the privacy.
setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします setting_hide_network: It will hide the following and follower information from the profile page.
setting_public_post_to_unlisted: 未対応のサードパーティアプリからもローカル公開で投稿できますが、公開投稿はWeb以外できなくなります setting_public_post_to_unlisted: You can still post with local visibility using unsupported third-party apps, but public posts will no longer be possible outside of the web interface.
setting_reject_send_limited_to_suspects: This applies to "Mutual Only" posts. Circle posts will be delivered without exception. Some Misskey servers have independently supported limited posting, but this is a setting for those who are concerned about it, as mutual-only posting exposes some of the users you are mutual with to Misskey users! setting_reject_send_limited_to_suspects: This applies to "Mutual Only" posts. Circle posts will be delivered without exception. Some Misskey servers have independently supported limited posting, but this is a setting for those who are concerned about it, as mutual-only posting exposes some of the users you are mutual with to Misskey users!
setting_reject_unlisted_subscription: Misskey and its forks can **subscribe and search** for "non-following" posts from accounts they do not follow. This differs from kmyblue's behavior. It delivers posts in the specified public range to such servers as "followers only". Please understand, however, that due to its structure, it is difficult to handle perfectly and will occasionally be delivered as non-subscribed. setting_reject_unlisted_subscription: Misskey and its forks can **subscribe and search** for "non-following" posts from accounts they do not follow. This differs from kmyblue's behavior. It delivers posts in the specified public range to such servers as "followers only". Please understand, however, that due to its structure, it is difficult to handle perfectly and will occasionally be delivered as non-subscribed.
setting_reverse_search_quote: Double-quotes will result in a search with a wider range of notation, which is the opposite of Mastodon's default behavior. setting_reverse_search_quote: Double-quotes will result in a search with a wider range of notation, which is the opposite of Mastodon's default behavior.
@ -121,10 +121,10 @@ en:
peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense. peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense.
profile_directory: The profile directory lists all users who have opted-in to be discoverable. profile_directory: The profile directory lists all users who have opted-in to be discoverable.
receive_other_servers_emoji_reaction: It can cause load. It is recommended to enable it only when there are few people. receive_other_servers_emoji_reaction: It can cause load. It is recommended to enable it only when there are few people.
registrations_end_hour: 新規登録が承認なしで可能な時間帯の開始時間を指定します。これより前の時間に登録することはできません。終了時間より後にすることはできません。この時間帯から外れた新規登録には、別途承認が必要となります。 registrations_end_hour: Specifies the end of the time window during which new registrations can be made without approval. Registrations cannot be made before the start time or after this end time. Registrations outside this time window will require separate approval.
registrations_limit: 現在のユーザー数がこれを超過すると、管理者がこの数値を増やさない限り新規登録できません。0を指定すると、この制限を無効化します。 registrations_limit: If the current number of users exceeds this value, new registrations will not be possible unless the administrator increases the limit. Setting this to 0 disables the restriction.
registrations_limit_per_day: 本日登録されたユーザー数がこれを超過すると、UTC時刻で翌日0時にならない限り新規登録できません。0を指定すると、この制限を無効化します。 registrations_limit_per_day: If the number of users registered today exceeds this value, no new registrations will be allowed until 00:00 UTC the next day. Setting this to 0 disables the restriction.
registrations_start_hour: 新規登録が承認なしで可能な時間帯の終了時間を指定します。これより後の時間に登録することはできません。開始時間より前にすることはできません。この時間帯から外れた新規登録には、別途承認が必要となります。 registrations_start_hour: Specifies the start of the time window during which new registrations can be made without approval. Registrations cannot be made before this time or after the end time. Registrations outside this time window will require separate approval.
require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
site_contact_email: How people can reach you for legal or support inquiries. site_contact_email: How people can reach you for legal or support inquiries.
site_contact_username: How people can reach you on Mastodon. site_contact_username: How people can reach you on Mastodon.
@ -201,8 +201,8 @@ en:
discoverable: Feature profile and posts in discovery algorithms discoverable: Feature profile and posts in discovery algorithms
fields: fields:
examples: examples:
name_1: 例) GitHub name_1: Example Gitea
value_1: 例) https://github.com/xxxxxx value_1: Example https://giteahub.com
name: Label name: Label
value: Content value: Content
indexable: Include public posts in search results indexable: Include public posts in search results

View file

@ -1,4 +1,10 @@
default: styles/application.scss default: styles/application.scss
contrast: styles/contrast.scss contrast: styles/contrast.scss
mastodon-light: styles/mastodon-light.scss mastodon-light: styles/mastodon-light.scss
modern-dark: styles/modern-dark.scss
modern-light: styles/modern-light.scss
modern-contrast: styles/modern-contrast.scss
full-dark: styles/full-dark.scss full-dark: styles/full-dark.scss

View file

@ -59,7 +59,7 @@ services:
web: web:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
build: . build: .
image: kmyblue:18.0-dev image: kmyblue:18.1
restart: always restart: always
env_file: .env.production env_file: .env.production
command: bundle exec puma -C config/puma.rb command: bundle exec puma -C config/puma.rb
@ -83,7 +83,7 @@ services:
build: build:
dockerfile: ./streaming/Dockerfile dockerfile: ./streaming/Dockerfile
context: . context: .
image: kmyblue-streaming:18.0-dev image: kmyblue-streaming:18.1
restart: always restart: always
env_file: .env.production env_file: .env.production
command: node ./streaming/index.js command: node ./streaming/index.js
@ -101,7 +101,7 @@ services:
sidekiq: sidekiq:
build: . build: .
image: kmyblue:18.0-dev image: kmyblue:18.1
restart: always restart: always
env_file: .env.production env_file: .env.production
command: bundle exec sidekiq command: bundle exec sidekiq

View file

@ -13,13 +13,13 @@ module Mastodon
end end
def kmyblue_minor def kmyblue_minor
0 1
end end
def kmyblue_flag def kmyblue_flag
# 'LTS' # 'LTS'
'dev' # 'dev'
# nil nil
end end
def major def major
@ -35,7 +35,7 @@ module Mastodon
end end
def default_prerelease def default_prerelease
'alpha.4' 'alpha.5'
end end
def prerelease def prerelease

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

BIN
public/favicon.ico Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Before After
Before After