Merge remote-tracking branch 'upstream/main' into develop

# Conflicts:
#	app/javascript/flavours/glitch/features/ui/components/link_footer.js
#	lib/mastodon/version.rb
#	public/avatars/original/missing.png
This commit is contained in:
Jeremy Kescher 2022-11-06 20:59:47 +01:00
commit 03f50f2f00
No known key found for this signature in database
GPG key ID: 48DFE4BB15BA5940
540 changed files with 9806 additions and 4673 deletions

View file

@ -2,7 +2,4 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: GitHub Discussions - name: GitHub Discussions
url: https://github.com/mastodon/mastodon/discussions url: https://github.com/mastodon/mastodon/discussions
about: Please ask and answer questions here. about: Please ask and answer questions here.
- name: Bug Bounty Program
url: https://app.intigriti.com/programs/mastodon/mastodonio/detail
about: Please report security vulnerabilities here.

View file

@ -1,8 +1,8 @@
ffmpeg ffmpeg
libicu[0-9][0-9] libicu[0-9][0-9]
libicu-dev libicu-dev
libidn11 libidn12
libidn11-dev libidn-dev
libpq-dev libpq-dev
libxdamage1 libxdamage1
libxfixes3 libxfixes3

View file

@ -13,7 +13,7 @@ Some of the features in this release have been funded through the [NGI0 Discover
- **Add ability to follow hashtags** ([Gargron](https://github.com/mastodon/mastodon/pull/18809), [Gargron](https://github.com/mastodon/mastodon/pull/18862), [Gargron](https://github.com/mastodon/mastodon/pull/19472), [noellabo](https://github.com/mastodon/mastodon/pull/18924)) - **Add ability to follow hashtags** ([Gargron](https://github.com/mastodon/mastodon/pull/18809), [Gargron](https://github.com/mastodon/mastodon/pull/18862), [Gargron](https://github.com/mastodon/mastodon/pull/19472), [noellabo](https://github.com/mastodon/mastodon/pull/18924))
- Add ability to filter individual posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18945)) - Add ability to filter individual posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18945))
- **Add ability to translate posts** ([Gargron](https://github.com/mastodon/mastodon/pull/19218), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19433), [Gargron](https://github.com/mastodon/mastodon/pull/19453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19434), [Gargron](https://github.com/mastodon/mastodon/pull/19388), [ykzts](https://github.com/mastodon/mastodon/pull/19244), [Gargron](https://github.com/mastodon/mastodon/pull/19245)) - **Add ability to translate posts** ([Gargron](https://github.com/mastodon/mastodon/pull/19218), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19433), [Gargron](https://github.com/mastodon/mastodon/pull/19453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19434), [Gargron](https://github.com/mastodon/mastodon/pull/19388), [ykzts](https://github.com/mastodon/mastodon/pull/19244), [Gargron](https://github.com/mastodon/mastodon/pull/19245))
- Add featured tags to web UI ([noellabo](https://github.com/mastodon/mastodon/pull/19408), [noellabo](https://github.com/mastodon/mastodon/pull/19380), [noellabo](https://github.com/mastodon/mastodon/pull/19358), [noellabo](https://github.com/mastodon/mastodon/pull/19409), [Gargron](https://github.com/mastodon/mastodon/pull/19382), [ykzts](https://github.com/mastodon/mastodon/pull/19418), [noellabo](https://github.com/mastodon/mastodon/pull/19403), [noellabo](https://github.com/mastodon/mastodon/pull/19404), [Gargron](https://github.com/mastodon/mastodon/pull/19398)) - Add featured tags to web UI ([noellabo](https://github.com/mastodon/mastodon/pull/19408), [noellabo](https://github.com/mastodon/mastodon/pull/19380), [noellabo](https://github.com/mastodon/mastodon/pull/19358), [noellabo](https://github.com/mastodon/mastodon/pull/19409), [Gargron](https://github.com/mastodon/mastodon/pull/19382), [ykzts](https://github.com/mastodon/mastodon/pull/19418), [noellabo](https://github.com/mastodon/mastodon/pull/19403), [noellabo](https://github.com/mastodon/mastodon/pull/19404), [Gargron](https://github.com/mastodon/mastodon/pull/19398), [Gargron](https://github.com/mastodon/mastodon/pull/19712))
- **Add support for language preferences for trending statuses and links** ([Gargron](https://github.com/mastodon/mastodon/pull/18288), [Gargron](https://github.com/mastodon/mastodon/pull/19349), [ykzts](https://github.com/mastodon/mastodon/pull/19335)) - **Add support for language preferences for trending statuses and links** ([Gargron](https://github.com/mastodon/mastodon/pull/18288), [Gargron](https://github.com/mastodon/mastodon/pull/19349), [ykzts](https://github.com/mastodon/mastodon/pull/19335))
- Previously, you could only see trends in your current language - Previously, you could only see trends in your current language
- For less popular languages, that meant empty trends - For less popular languages, that meant empty trends
@ -23,9 +23,10 @@ Some of the features in this release have been funded through the [NGI0 Discover
- Add `noopener` to links to remote profiles in web UI ([shleeable](https://github.com/mastodon/mastodon/pull/19014)) - Add `noopener` to links to remote profiles in web UI ([shleeable](https://github.com/mastodon/mastodon/pull/19014))
- Add warning for sensitive audio posts in web UI ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/17885)) - Add warning for sensitive audio posts in web UI ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/17885))
- Add language attribute to posts in web UI ([tribela](https://github.com/mastodon/mastodon/pull/18544)) - Add language attribute to posts in web UI ([tribela](https://github.com/mastodon/mastodon/pull/18544))
- Add meta tag for official iOS app ([Gargron](https://github.com/mastodon/mastodon/pull/16599))
- Add support for uploading WebP files ([Saiv46](https://github.com/mastodon/mastodon/pull/18506)) - Add support for uploading WebP files ([Saiv46](https://github.com/mastodon/mastodon/pull/18506))
- Add support for uploading `audio/vnd.wave` files ([tribela](https://github.com/mastodon/mastodon/pull/18737)) - Add support for uploading `audio/vnd.wave` files ([tribela](https://github.com/mastodon/mastodon/pull/18737))
- Add support for uploading AVIF files ([txt-file](https://github.com/mastodon/mastodon/pull/19647))
- Add support for uploading HEIC files ([Gargron](https://github.com/mastodon/mastodon/pull/19618))
- Add more debug information when processing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19209)) - Add more debug information when processing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19209))
- **Add retention policy for cached content and media** ([Gargron](https://github.com/mastodon/mastodon/pull/19232), [zunda](https://github.com/mastodon/mastodon/pull/19478), [Gargron](https://github.com/mastodon/mastodon/pull/19458), [Gargron](https://github.com/mastodon/mastodon/pull/19248)) - **Add retention policy for cached content and media** ([Gargron](https://github.com/mastodon/mastodon/pull/19232), [zunda](https://github.com/mastodon/mastodon/pull/19478), [Gargron](https://github.com/mastodon/mastodon/pull/19458), [Gargron](https://github.com/mastodon/mastodon/pull/19248))
- Set for how long remote posts or media should be cached on your server - Set for how long remote posts or media should be cached on your server
@ -49,12 +50,15 @@ Some of the features in this release have been funded through the [NGI0 Discover
- Add `EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18642)) - Add `EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18642))
- Add `IP_RETENTION_PERIOD` and `SESSION_RETENTION_PERIOD` environment variables ([kescherCode](https://github.com/mastodon/mastodon/pull/18757)) - Add `IP_RETENTION_PERIOD` and `SESSION_RETENTION_PERIOD` environment variables ([kescherCode](https://github.com/mastodon/mastodon/pull/18757))
- Add `http_hidden_proxy` environment variable ([tribela](https://github.com/mastodon/mastodon/pull/18427)) - Add `http_hidden_proxy` environment variable ([tribela](https://github.com/mastodon/mastodon/pull/18427))
- Add caching for payload serialization during fan-out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19637), [Gargron](https://github.com/mastodon/mastodon/pull/19642), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19746), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19747))
- Add assets from Twemoji 14.0 ([Gargron](https://github.com/mastodon/mastodon/pull/19733))
- Add reputation and followers score boost to SQL-only account search ([Gargron](https://github.com/mastodon/mastodon/pull/19251))
### Changed ### Changed
- **Change brand color and logotypes** ([Gargron](https://github.com/mastodon/mastodon/pull/18592), [Gargron](https://github.com/mastodon/mastodon/pull/18639), [Gargron](https://github.com/mastodon/mastodon/pull/18691), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18634), [Gargron](https://github.com/mastodon/mastodon/pull/19254), [mayaeh](https://github.com/mastodon/mastodon/pull/18710)) - **Change brand color and logotypes** ([Gargron](https://github.com/mastodon/mastodon/pull/18592), [Gargron](https://github.com/mastodon/mastodon/pull/18639), [Gargron](https://github.com/mastodon/mastodon/pull/18691), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18634), [Gargron](https://github.com/mastodon/mastodon/pull/19254), [mayaeh](https://github.com/mastodon/mastodon/pull/18710))
- **Change post editing to be enabled in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/19103)) - **Change post editing to be enabled in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/19103))
- **Change web UI to work for logged-out users** ([Gargron](https://github.com/mastodon/mastodon/pull/18961), [Gargron](https://github.com/mastodon/mastodon/pull/19250), [Gargron](https://github.com/mastodon/mastodon/pull/19294), [Gargron](https://github.com/mastodon/mastodon/pull/19306), [Gargron](https://github.com/mastodon/mastodon/pull/19315), [ykzts](https://github.com/mastodon/mastodon/pull/19322), [Gargron](https://github.com/mastodon/mastodon/pull/19412), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19437), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19415), [Gargron](https://github.com/mastodon/mastodon/pull/19348), [Gargron](https://github.com/mastodon/mastodon/pull/19295), [Gargron](https://github.com/mastodon/mastodon/pull/19422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19414), [Gargron](https://github.com/mastodon/mastodon/pull/19319), [Gargron](https://github.com/mastodon/mastodon/pull/19345), [Gargron](https://github.com/mastodon/mastodon/pull/19310), [Gargron](https://github.com/mastodon/mastodon/pull/19301), [Gargron](https://github.com/mastodon/mastodon/pull/19423), [ykzts](https://github.com/mastodon/mastodon/pull/19471), [ykzts](https://github.com/mastodon/mastodon/pull/19333), [ykzts](https://github.com/mastodon/mastodon/pull/19337), [ykzts](https://github.com/mastodon/mastodon/pull/19272), [ykzts](https://github.com/mastodon/mastodon/pull/19468), [Gargron](https://github.com/mastodon/mastodon/pull/19466), [Gargron](https://github.com/mastodon/mastodon/pull/19457), [Gargron](https://github.com/mastodon/mastodon/pull/19426), [Gargron](https://github.com/mastodon/mastodon/pull/19427), [Gargron](https://github.com/mastodon/mastodon/pull/19421), [Gargron](https://github.com/mastodon/mastodon/pull/19417), [Gargron](https://github.com/mastodon/mastodon/pull/19413), [Gargron](https://github.com/mastodon/mastodon/pull/19397), [Gargron](https://github.com/mastodon/mastodon/pull/19387), [Gargron](https://github.com/mastodon/mastodon/pull/19396), [Gargron](https://github.com/mastodon/mastodon/pull/19385), [ykzts](https://github.com/mastodon/mastodon/pull/19334), [ykzts](https://github.com/mastodon/mastodon/pull/19329), [Gargron](https://github.com/mastodon/mastodon/pull/19324), [Gargron](https://github.com/mastodon/mastodon/pull/19318), [Gargron](https://github.com/mastodon/mastodon/pull/19316), [Gargron](https://github.com/mastodon/mastodon/pull/19263), [trwnh](https://github.com/mastodon/mastodon/pull/19305), [ykzts](https://github.com/mastodon/mastodon/pull/19273)) - **Change web UI to work for logged-out users** ([Gargron](https://github.com/mastodon/mastodon/pull/18961), [Gargron](https://github.com/mastodon/mastodon/pull/19250), [Gargron](https://github.com/mastodon/mastodon/pull/19294), [Gargron](https://github.com/mastodon/mastodon/pull/19306), [Gargron](https://github.com/mastodon/mastodon/pull/19315), [ykzts](https://github.com/mastodon/mastodon/pull/19322), [Gargron](https://github.com/mastodon/mastodon/pull/19412), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19437), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19415), [Gargron](https://github.com/mastodon/mastodon/pull/19348), [Gargron](https://github.com/mastodon/mastodon/pull/19295), [Gargron](https://github.com/mastodon/mastodon/pull/19422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19414), [Gargron](https://github.com/mastodon/mastodon/pull/19319), [Gargron](https://github.com/mastodon/mastodon/pull/19345), [Gargron](https://github.com/mastodon/mastodon/pull/19310), [Gargron](https://github.com/mastodon/mastodon/pull/19301), [Gargron](https://github.com/mastodon/mastodon/pull/19423), [ykzts](https://github.com/mastodon/mastodon/pull/19471), [ykzts](https://github.com/mastodon/mastodon/pull/19333), [ykzts](https://github.com/mastodon/mastodon/pull/19337), [ykzts](https://github.com/mastodon/mastodon/pull/19272), [ykzts](https://github.com/mastodon/mastodon/pull/19468), [Gargron](https://github.com/mastodon/mastodon/pull/19466), [Gargron](https://github.com/mastodon/mastodon/pull/19457), [Gargron](https://github.com/mastodon/mastodon/pull/19426), [Gargron](https://github.com/mastodon/mastodon/pull/19427), [Gargron](https://github.com/mastodon/mastodon/pull/19421), [Gargron](https://github.com/mastodon/mastodon/pull/19417), [Gargron](https://github.com/mastodon/mastodon/pull/19413), [Gargron](https://github.com/mastodon/mastodon/pull/19397), [Gargron](https://github.com/mastodon/mastodon/pull/19387), [Gargron](https://github.com/mastodon/mastodon/pull/19396), [Gargron](https://github.com/mastodon/mastodon/pull/19385), [ykzts](https://github.com/mastodon/mastodon/pull/19334), [ykzts](https://github.com/mastodon/mastodon/pull/19329), [Gargron](https://github.com/mastodon/mastodon/pull/19324), [Gargron](https://github.com/mastodon/mastodon/pull/19318), [Gargron](https://github.com/mastodon/mastodon/pull/19316), [Gargron](https://github.com/mastodon/mastodon/pull/19263), [trwnh](https://github.com/mastodon/mastodon/pull/19305), [ykzts](https://github.com/mastodon/mastodon/pull/19273), [Gargron](https://github.com/mastodon/mastodon/pull/19801), [Gargron](https://github.com/mastodon/mastodon/pull/19790), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19773), [Gargron](https://github.com/mastodon/mastodon/pull/19798), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19724), [Gargron](https://github.com/mastodon/mastodon/pull/19709), [Gargron](https://github.com/mastodon/mastodon/pull/19514), [Gargron](https://github.com/mastodon/mastodon/pull/19562))
- The web app can now be accessed without being logged in - The web app can now be accessed without being logged in
- No more `/web` prefix on web app paths - No more `/web` prefix on web app paths
- Profiles, posts, and other public pages now use the same interface for logged in and logged out users - Profiles, posts, and other public pages now use the same interface for logged in and logged out users
@ -77,15 +81,20 @@ Some of the features in this release have been funded through the [NGI0 Discover
- You can peek inside filtered posts anyway - You can peek inside filtered posts anyway
- Change path of privacy policy page from `/terms` to `/privacy-policy` ([Gargron](https://github.com/mastodon/mastodon/pull/19249)) - Change path of privacy policy page from `/terms` to `/privacy-policy` ([Gargron](https://github.com/mastodon/mastodon/pull/19249))
- Change how hashtags are normalized ([Gargron](https://github.com/mastodon/mastodon/pull/18795), [Gargron](https://github.com/mastodon/mastodon/pull/18863), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18854)) - Change how hashtags are normalized ([Gargron](https://github.com/mastodon/mastodon/pull/18795), [Gargron](https://github.com/mastodon/mastodon/pull/18863), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18854))
- Change public timelines to be filtered by current locale by default ([Gargron](https://github.com/mastodon/mastodon/pull/19291)) - Change public (but not hashtag) timelines to be filtered by current locale by default ([Gargron](https://github.com/mastodon/mastodon/pull/19291), [Gargron](https://github.com/mastodon/mastodon/pull/19563))
- Change settings area to be separated into categories in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19407)) - Change settings area to be separated into categories in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19407), [Gargron](https://github.com/mastodon/mastodon/pull/19533))
- Change "No accounts selected" errors to use the appropriate noun in admin UI ([prplecake](https://github.com/mastodon/mastodon/pull/19356)) - Change "No accounts selected" errors to use the appropriate noun in admin UI ([prplecake](https://github.com/mastodon/mastodon/pull/19356))
- Change e-mail domain blocks to match subdomains of blocked domains ([Gargron](https://github.com/mastodon/mastodon/pull/18979)) - Change e-mail domain blocks to match subdomains of blocked domains ([Gargron](https://github.com/mastodon/mastodon/pull/18979))
- Change custom emoji file size limit from 50 KB to 256 KB ([Gargron](https://github.com/mastodon/mastodon/pull/18788)) - Change custom emoji file size limit from 50 KB to 256 KB ([Gargron](https://github.com/mastodon/mastodon/pull/18788))
- Change "Allow trends without prior review" setting to also work for trending posts ([Gargron](https://github.com/mastodon/mastodon/pull/17977)) - Change "Allow trends without prior review" setting to also work for trending posts ([Gargron](https://github.com/mastodon/mastodon/pull/17977))
- Change admin announcements form to use single inputs for date and time in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18321))
- Change search API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18963), [Gargron](https://github.com/mastodon/mastodon/pull/19326)) - Change search API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18963), [Gargron](https://github.com/mastodon/mastodon/pull/19326))
- Change following and followers API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18964)) - Change following and followers API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18964))
- Change `AUTHORIZED_FETCH` to not block unauthenticated REST API access ([Gargron](https://github.com/mastodon/mastodon/pull/19803))
- Change Helm configuration ([deepy](https://github.com/mastodon/mastodon/pull/18997), [jgsmith](https://github.com/mastodon/mastodon/pull/18415), [deepy](https://github.com/mastodon/mastodon/pull/18941)) - Change Helm configuration ([deepy](https://github.com/mastodon/mastodon/pull/18997), [jgsmith](https://github.com/mastodon/mastodon/pull/18415), [deepy](https://github.com/mastodon/mastodon/pull/18941))
- Change mentions of blocked users to not be processed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19725))
- Change max. thumbnail dimensions to 640x360px (360p) ([Gargron](https://github.com/mastodon/mastodon/pull/19619))
- Change post-processing to be deferred only for large media types ([Gargron](https://github.com/mastodon/mastodon/pull/19617))
### Removed ### Removed
@ -98,6 +107,25 @@ Some of the features in this release have been funded through the [NGI0 Discover
### Fixed ### Fixed
- Fix featured tags not saving preferred casing ([Gargron](https://github.com/mastodon/mastodon/pull/19732))
- Fix language not being saved when editing status ([Gargron](https://github.com/mastodon/mastodon/pull/19543))
- Fix not being able to input featured tag with hash symbol ([Gargron](https://github.com/mastodon/mastodon/pull/19535))
- Fix user clean-up scheduler crash when an unconfirmed account has a moderation note ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19629))
- Fix being unable to withdraw follow request when confirmation modal is disabled in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19687))
- Fix inaccurate admin log entry for re-sending confirmation e-mails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19674))
- Fix edits not being immediately reflected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19673))
- Fix bookmark import stopping at the first failure ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19669))
- Fix account action type validation ([Gargron](https://github.com/mastodon/mastodon/pull/19476))
- Fix upload progress not communicating processing phase in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19530))
- Fix wrong host being used for custom.css when asset host configured ([Gargron](https://github.com/mastodon/mastodon/pull/19521))
- Fix account migration form ever using outdated account data ([Gargron](https://github.com/mastodon/mastodon/pull/18429))
- Fix error when uploading malformed CSV import ([Gargron](https://github.com/mastodon/mastodon/pull/19509))
- Fix avatars not using image tags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19488))
- Fix handling of duplicate and out-of-order notifications in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19693))
- Fix reblogs being discarded after the reblogged status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19731))
- Fix indexing scheduler trying to index when Elasticsearch is disabled ([Gargron](https://github.com/mastodon/mastodon/pull/19805))
- Fix n+1 queries when rendering initial state JSON ([Gargron](https://github.com/mastodon/mastodon/pull/19795))
- Fix n+1 query during status removal ([Gargron](https://github.com/mastodon/mastodon/pull/19753))
- Fix OCR not working due to Content Security Policy in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/18817)) - Fix OCR not working due to Content Security Policy in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/18817))
- Fix `nofollow` rel being removed in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19455)) - Fix `nofollow` rel being removed in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19455))
- Fix language dropdown causing zoom on mobile devices in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19428)) - Fix language dropdown causing zoom on mobile devices in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19428))

View file

@ -1,6 +1,6 @@
# Security Policy # Security Policy
If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you should submit the report through our [Bug Bounty Program][bug-bounty]. Alternatively, you can reach us at <hello@joinmastodon.org>. If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can reach us at <hello@joinmastodon.org>.
You should *not* report such issues on GitHub or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk. You should *not* report such issues on GitHub or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk.
@ -16,5 +16,3 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| 3.4.x | Yes | | 3.4.x | Yes |
| 3.3.x | No | | 3.3.x | No |
| < 3.3 | No | | < 3.3 | No |
[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail

View file

@ -17,7 +17,7 @@ module Admin
@user.resend_confirmation_instructions @user.resend_confirmation_instructions
log_action :confirm, @user log_action :resend, @user
flash[:notice] = I18n.t('admin.accounts.resend_confirmation.success') flash[:notice] = I18n.t('admin.accounts.resend_confirmation.success')
redirect_to admin_accounts_path redirect_to admin_accounts_path

View file

@ -79,7 +79,7 @@ class Api::V1::StatusesController < Api::BaseController
@status = Status.where(account: current_account).find(params[:id]) @status = Status.where(account: current_account).find(params[:id])
authorize @status, :destroy? authorize @status, :destroy?
@status.discard @status.discard_with_reblogs
StatusPin.find_by(status: @status)&.destroy StatusPin.find_by(status: @status)&.destroy
@status.account.statuses_count = @status.account.statuses_count - 1 @status.account.statuses_count = @status.account.statuses_count - 1
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true

View file

@ -3,7 +3,7 @@
class Api::V2::MediaController < Api::V1::MediaController class Api::V2::MediaController < Api::V1::MediaController
def create def create
@media_attachment = current_account.media_attachments.create!({ delay_processing: true }.merge(media_attachment_params)) @media_attachment = current_account.media_attachments.create!({ delay_processing: true }.merge(media_attachment_params))
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: 202 render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: @media_attachment.not_processed? ? 202 : 200
rescue Paperclip::Errors::NotIdentifiedByImageMagickError rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: file_type_error, status: 422 render json: file_type_error, status: 422
rescue Paperclip::Error rescue Paperclip::Error

View file

@ -23,7 +23,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
end end
def destroy def destroy
RemoveFeaturedTagWorker.perform_async(current_account.id, @featured_tag.id) RemoveFeaturedTagService.new.call(current_account, @featured_tag)
redirect_to settings_featured_tags_path redirect_to settings_featured_tags_path
end end

View file

@ -4,15 +4,19 @@ module Admin::ActionLogsHelper
def log_target(log) def log_target(log)
case log.target_type case log.target_type
when 'Account' when 'Account'
link_to log.human_identifier, admin_account_path(log.target_id) link_to (log.human_identifier.presence || I18n.t('admin.action_logs.deleted_account')), admin_account_path(log.target_id)
when 'User' when 'User'
link_to log.human_identifier, admin_account_path(log.route_param) if log.route_param.present?
link_to log.human_identifier, admin_account_path(log.route_param)
else
I18n.t('admin.action_logs.deleted_account')
end
when 'UserRole' when 'UserRole'
link_to log.human_identifier, admin_roles_path(log.target_id) link_to log.human_identifier, admin_roles_path(log.target_id)
when 'Report' when 'Report'
link_to "##{log.human_identifier}", admin_report_path(log.target_id) link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
link_to log.human_identifier, "https://#{log.human_identifier}" link_to log.human_identifier, "https://#{log.human_identifier.presence}"
when 'Status' when 'Status'
link_to log.human_identifier, log.permalink link_to log.human_identifier, log.permalink
when 'AccountWarning' when 'AccountWarning'
@ -22,9 +26,13 @@ module Admin::ActionLogsHelper
when 'IpBlock', 'Instance', 'CustomEmoji' when 'IpBlock', 'Instance', 'CustomEmoji'
log.human_identifier log.human_identifier
when 'CanonicalEmailBlock' when 'CanonicalEmailBlock'
content_tag(:samp, log.human_identifier[0...7], title: log.human_identifier) content_tag(:samp, (log.human_identifier.presence || '')[0...7], title: log.human_identifier)
when 'Appeal' when 'Appeal'
link_to log.human_identifier, disputes_strike_path(log.route_param) if log.route_param.present?
link_to log.human_identifier, disputes_strike_path(log.route_param.presence)
else
I18n.t('admin.action_logs.deleted_account')
end
end end
end end
end end

View file

@ -204,7 +204,7 @@ module ApplicationHelper
permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present? permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present?
state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility] state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility]
if user_signed_in? if user_signed_in? && current_user.functional?
state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {}) state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {})
state_params[:push_subscription] = current_account.user.web_push_subscription(current_session) state_params[:push_subscription] = current_account.user.web_push_subscription(current_session)
state_params[:current_account] = current_account state_params[:current_account] = current_account
@ -212,6 +212,11 @@ module ApplicationHelper
state_params[:admin] = Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) state_params[:admin] = Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, ''))
end end
if user_signed_in? && !current_user.functional?
state_params[:disabled_account] = current_account
state_params[:moved_to_account] = current_account.moved_to_account
end
if single_user_mode? if single_user_mode?
state_params[:owner] = Account.local.without_suspended.where('id > 0').first state_params[:owner] = Account.local.without_suspended.where('id > 0').first
end end

View file

@ -8,7 +8,7 @@ import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
import resizeImage from 'flavours/glitch/utils/resize_image'; import resizeImage from 'flavours/glitch/utils/resize_image';
import { showAlert, showAlertForError } from './alerts'; import { showAlert, showAlertForError } from './alerts';
import { useEmoji } from './emojis'; import { useEmoji } from './emojis';
import { importFetchedAccounts } from './importer'; import { importFetchedAccounts, importFetchedStatus } from './importer';
import { openModal } from './modal'; import { openModal } from './modal';
import { updateTimeline } from './timelines'; import { updateTimeline } from './timelines';
@ -223,6 +223,10 @@ export function submitCompose(routerHistory) {
} }
}; };
if (statusId) {
dispatch(importFetchedStatus({ ...response.data }));
}
if (statusId === null) { if (statusId === null) {
insertIfOnline('home'); insertIfOnline('home');
} }

View file

@ -27,6 +27,7 @@ export default @injectIntl
class Account extends ImmutablePureComponent { class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {
size: PropTypes.number,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
@ -41,6 +42,10 @@ class Account extends ImmutablePureComponent {
onActionClick: PropTypes.func, onActionClick: PropTypes.func,
}; };
static defaultProps = {
size: 36,
};
handleFollow = () => { handleFollow = () => {
this.props.onFollow(this.props.account); this.props.onFollow(this.props.account);
} }
@ -75,6 +80,7 @@ class Account extends ImmutablePureComponent {
actionIcon, actionIcon,
actionTitle, actionTitle,
defaultAction, defaultAction,
size,
} = this.props; } = this.props;
if (!account) { if (!account) {
@ -163,7 +169,7 @@ class Account extends ImmutablePureComponent {
<div className='account'> <div className='account'>
<div className='account__wrapper'> <div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}> <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div> <div className='account__avatar-wrapper'><Avatar account={account} size={size} /></div>
{mute_expires_at} {mute_expires_at}
<DisplayName account={account} /> <DisplayName account={account} />
</Permalink> </Permalink>

View file

@ -21,7 +21,12 @@ class NavigationPortal extends React.PureComponent {
render () { render () {
return ( return (
<Switch> <Switch>
<Route path='/@:acct/(tagged/:tagged?)?' component={AccountNavigation} /> <Route path='/@:acct' exact component={AccountNavigation} />
<Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
<Route path='/@:acct/followers' exact component={AccountNavigation} />
<Route path='/@:acct/following' exact component={AccountNavigation} />
<Route path='/@:acct/media' exact component={AccountNavigation} />
<Route component={DefaultNavigation} /> <Route component={DefaultNavigation} />
</Switch> </Switch>
); );

View file

@ -61,7 +61,7 @@ class ServerBanner extends React.PureComponent {
<div className='server-banner__meta__column'> <div className='server-banner__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'])} /> <Account id={server.getIn(['contact', 'account', 'id'])} size={36} />
</div> </div>
<div className='server-banner__meta__column'> <div className='server-banner__meta__column'>

View file

@ -33,6 +33,7 @@ store.dispatch(fetchCustomEmojis());
const createIdentityContext = state => ({ const createIdentityContext = state => ({
signedIn: !!state.meta.me, signedIn: !!state.meta.me,
accountId: state.meta.me, accountId: state.meta.me,
disabledAccountId: state.meta.disabled_account_id,
accessToken: state.meta.access_token, accessToken: state.meta.access_token,
permissions: state.role ? state.role.permissions : 0, permissions: state.role ? state.role.permissions : 0,
}); });
@ -47,6 +48,7 @@ export default class Mastodon extends React.PureComponent {
identity: PropTypes.shape({ identity: PropTypes.shape({
signedIn: PropTypes.bool.isRequired, signedIn: PropTypes.bool.isRequired,
accountId: PropTypes.string, accountId: PropTypes.string,
disabledAccountId: PropTypes.string,
accessToken: PropTypes.string, accessToken: PropTypes.string,
}).isRequired, }).isRequired,
}; };

View file

@ -125,7 +125,7 @@ class About extends React.PureComponent {
<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'])} /> <Account id={server.getIn(['contact', 'account', 'id'])} size={36} />
</div> </div>
<hr className='about__meta__divider' /> <hr className='about__meta__divider' />
@ -209,6 +209,11 @@ class About extends React.PureComponent {
</Section> </Section>
<LinkFooter /> <LinkFooter />
<div className='about__footer'>
<p><FormattedMessage id='about.fork_disclaimer' defaultMessage='Glitch-soc is free open source software forked from Mastodon.' /></p>
<p><FormattedMessage id='about.disclaimer' defaultMessage='Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.' /></p>
</div>
</div> </div>
<Helmet> <Helmet>

View file

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { revealAccount } from 'flavours/glitch/actions/accounts'; import { revealAccount } from 'flavours/glitch/actions/accounts';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Button from 'flavours/glitch/components/button'; import Button from 'flavours/glitch/components/button';
import { domain } from 'flavours/glitch/initial_state';
const mapDispatchToProps = (dispatch, { accountId }) => ({ const mapDispatchToProps = (dispatch, { accountId }) => ({
@ -26,7 +27,7 @@ class LimitedAccountHint extends React.PureComponent {
return ( return (
<div className='limited-account-hint'> <div className='limited-account-hint'>
<p><FormattedMessage id='limited_account_hint.title' defaultMessage='This profile has been hidden by the moderators of your server.' /></p> <p><FormattedMessage id='limited_account_hint.title' defaultMessage='This profile has been hidden by the moderators of {domain}.' values={{ domain }} /></p>
<Button onClick={reveal}><FormattedMessage id='limited_account_hint.action' defaultMessage='Show profile anyway' /></Button> <Button onClick={reveal}><FormattedMessage id='limited_account_hint.action' defaultMessage='Show profile anyway' /></Button>
</div> </div>
); );

View file

@ -61,6 +61,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))), onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
})); }));
} else {
dispatch(unfollowAccount(account.get('id')));
} }
} else { } else {
dispatch(followAccount(account.get('id'))); dispatch(followAccount(account.get('id')));

View file

@ -305,12 +305,12 @@ class ComposeForm extends ImmutablePureComponent {
const countText = this.getFulltextForCharacterCounting(); const countText = this.getFulltextForCharacterCounting();
return ( return (
<div className='composer'> <div className='compose-form'>
<WarningContainer /> <WarningContainer />
<ReplyIndicatorContainer /> <ReplyIndicatorContainer />
<div className={`composer--spoiler ${spoiler ? 'composer--spoiler--visible' : ''}`} ref={this.setRef}> <div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}>
<AutosuggestInput <AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)} placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={spoilerText} value={spoilerText}
@ -352,7 +352,7 @@ class ComposeForm extends ImmutablePureComponent {
</div> </div>
</AutosuggestTextarea> </AutosuggestTextarea>
<div className='composer--options-wrapper'> <div className='compose-form__buttons-wrapper'>
<OptionsContainer <OptionsContainer
advancedOptions={advancedOptions} advancedOptions={advancedOptions}
disabled={isSubmitting} disabled={isSubmitting}
@ -364,7 +364,7 @@ class ComposeForm extends ImmutablePureComponent {
sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)} sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)}
spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler}
/> />
<div className='compose--counter-wrapper'> <div className='character-counter__wrapper'>
<CharacterCounter text={countText} max={maxChars} /> <CharacterCounter text={countText} max={maxChars} />
</div> </div>
</div> </div>

View file

@ -16,7 +16,6 @@ import { assignHandlers } from 'flavours/glitch/utils/react_helpers';
export default class ComposerOptionsDropdown extends React.PureComponent { export default class ComposerOptionsDropdown extends React.PureComponent {
static propTypes = { static propTypes = {
active: PropTypes.bool,
disabled: PropTypes.bool, disabled: PropTypes.bool,
icon: PropTypes.string, icon: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({ items: PropTypes.arrayOf(PropTypes.shape({
@ -162,7 +161,6 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
// Rendering. // Rendering.
render () { render () {
const { const {
active,
disabled, disabled,
title, title,
icon, icon,
@ -174,35 +172,34 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
closeOnChange, closeOnChange,
} = this.props; } = this.props;
const { open, placement } = this.state; const { open, placement } = this.state;
const computedClass = classNames('composer--options--dropdown', {
active,
open,
top: placement === 'top',
});
// The result. const active = value && items.findIndex(({ name }) => name === value) === (placement === 'bottom' ? 0 : (items.length - 1));
return ( return (
<div <div
className={computedClass} className={classNames('privacy-dropdown', placement, { active: open })}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
> >
<IconButton <div className={classNames('privacy-dropdown__value', { active })}>
active={open || active} <IconButton
className='value' active={open}
disabled={disabled} className='privacy-dropdown__value-icon'
icon={icon} disabled={disabled}
inverted icon={icon}
onClick={this.handleToggle} inverted
onMouseDown={this.handleMouseDown} onClick={this.handleToggle}
onKeyDown={this.handleButtonKeyDown} onMouseDown={this.handleMouseDown}
onKeyPress={this.handleKeyPress} onKeyDown={this.handleButtonKeyDown}
size={18} onKeyPress={this.handleKeyPress}
style={{ size={18}
height: null, style={{
lineHeight: '27px', height: null,
}} lineHeight: '27px',
title={title} }}
/> title={title}
/>
</div>
<Overlay <Overlay
containerPadding={20} containerPadding={20}
placement={placement} placement={placement}

View file

@ -156,7 +156,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
const active = (name === (this.props.value || this.state.value)); const active = (name === (this.props.value || this.state.value));
const computedClass = classNames('composer--options--dropdown--content--item', { active }); const computedClass = classNames('privacy-dropdown__option', { active });
let contents = this.props.renderItemContents && this.props.renderItemContents(item, i); let contents = this.props.renderItemContents && this.props.renderItemContents(item, i);
@ -165,7 +165,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
<React.Fragment> <React.Fragment>
{icon && <Icon className='icon' fixedWidth id={icon} />} {icon && <Icon className='icon' fixedWidth id={icon} />}
<div className='content'> <div className='privacy-dropdown__option__content'>
<strong>{text}</strong> <strong>{text}</strong>
{meta} {meta}
</div> </div>
@ -218,7 +218,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
// size will be used to determine the coordinate of the menu by // size will be used to determine the coordinate of the menu by
// react-overlays // react-overlays
<div <div
className='composer--options--dropdown--content' className='privacy-dropdown__dropdown'
ref={this.handleRef} ref={this.handleRef}
role='listbox' role='listbox'
style={{ style={{

View file

@ -228,7 +228,7 @@ class ComposerOptions extends ImmutablePureComponent {
// The result. // The result.
return ( return (
<div className='composer--options'> <div className='compose-form__buttons'>
<input <input
accept={acceptContentTypes} accept={acceptContentTypes}
disabled={disabled || !allowMedia} disabled={disabled || !allowMedia}
@ -309,7 +309,6 @@ class ComposerOptions extends ImmutablePureComponent {
)} )}
<LanguageDropdown /> <LanguageDropdown />
<Dropdown <Dropdown
active={advancedOptions && advancedOptions.some(value => !!value)}
disabled={disabled || isEditing} disabled={disabled || isEditing}
icon='ellipsis-h' icon='ellipsis-h'
items={advancedOptions ? [ items={advancedOptions ? [

View file

@ -48,7 +48,7 @@ class Publisher extends ImmutablePureComponent {
const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props; const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props;
const diff = maxChars - length(countText || ''); const diff = maxChars - length(countText || '');
const computedClass = classNames('composer--publisher', { const computedClass = classNames('compose-form__publish', {
disabled: disabled, disabled: disabled,
over: diff < 0, over: diff < 0,
}); });
@ -72,22 +72,26 @@ class Publisher extends ImmutablePureComponent {
return ( return (
<div className={computedClass}> <div className={computedClass}>
{sideArm && !isEditing && sideArm !== 'none' ? ( {sideArm && !isEditing && sideArm !== 'none' ? (
<Button <div className='compose-form__publish-button-wrapper'>
className='side_arm' <Button
disabled={disabled} className='side_arm'
onClick={onSecondarySubmit} disabled={disabled}
style={{ padding: null }} onClick={onSecondarySubmit}
text={<Icon id={privacyIcons[sideArm]} />} style={{ padding: null }}
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`} text={<Icon id={privacyIcons[sideArm]} />}
/> title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`}
/>
</div>
) : null} ) : null}
<Button <div className='compose-form__publish-button-wrapper'>
className='primary' <Button
text={publishText} className='primary'
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${privacy}.short` })}`} text={publishText}
onClick={this.handleSubmit} title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${privacy}.short` })}`}
disabled={disabled} onClick={this.handleSubmit}
/> disabled={disabled}
/>
</div>
</div> </div>
); );
}; };

View file

@ -49,10 +49,10 @@ class ReplyIndicator extends ImmutablePureComponent {
// The result. // The result.
return ( return (
<article className='composer--reply'> <article className='reply-indicator'>
<header> <header className='reply-indicator__header'>
<IconButton <IconButton
className='cancel' className='reply-indicator__cancel'
icon='times' icon='times'
onClick={this.handleClick} onClick={this.handleClick}
title={intl.formatMessage(messages.cancel)} title={intl.formatMessage(messages.cancel)}
@ -66,7 +66,7 @@ class ReplyIndicator extends ImmutablePureComponent {
)} )}
</header> </header>
<div <div
className='content translate' className='reply-indicator__content translate'
dangerouslySetInnerHTML={{ __html: content || '' }} dangerouslySetInnerHTML={{ __html: content || '' }}
/> />
{attachments.size > 0 && ( {attachments.size > 0 && (

View file

@ -38,7 +38,7 @@ class TextareaIcons extends ImmutablePureComponent {
render () { render () {
const { advancedOptions, intl } = this.props; const { advancedOptions, intl } = this.props;
return ( return (
<div className='composer--textarea--icons'> <div className='compose-form__textarea-icons'>
{advancedOptions ? iconMap.map( {advancedOptions ? iconMap.map(
([key, icon, message]) => advancedOptions.get(key) ? ( ([key, icon, message]) => advancedOptions.get(key) ? (
<span <span

View file

@ -18,7 +18,7 @@ export default class Upload extends ImmutablePureComponent {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
onUndo: PropTypes.func.isRequired, onUndo: PropTypes.func.isRequired,
onOpenFocalPoint: PropTypes.func.isRequired, onOpenFocalPoint: PropTypes.func.isRequired,
isEditingStatus: PropTypes.func.isRequired, isEditingStatus: PropTypes.bool.isRequired,
}; };
handleUndoClick = e => { handleUndoClick = e => {
@ -39,17 +39,17 @@ export default class Upload extends ImmutablePureComponent {
const y = ((focusY / -2) + .5) * 100; const y = ((focusY / -2) + .5) * 100;
return ( return (
<div className='composer--upload_form--item' tabIndex='0' role='button'> <div className='compose-form__upload' tabIndex='0' role='button'>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12, }) }}> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12, }) }}>
{({ scale }) => ( {({ scale }) => (
<div style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}> <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
<div className='composer--upload_form--actions'> <div className='compose-form__upload__actions'>
<button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button> <button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
{!isEditingStatus && (<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)} {!isEditingStatus && (<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)}
</div> </div>
{(media.get('description') || '').length === 0 && ( {(media.get('description') || '').length === 0 && (
<div className='composer--upload_form--item__warning'> <div className='compose-form__upload__warning'>
<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button> <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
</div> </div>
)} )}

View file

@ -14,11 +14,11 @@ export default class UploadForm extends ImmutablePureComponent {
const { mediaIds } = this.props; const { mediaIds } = this.props;
return ( return (
<div className='composer--upload_form'> <div className='compose-form__upload-wrapper'>
<UploadProgressContainer /> <UploadProgressContainer />
{mediaIds.size > 0 && ( {mediaIds.size > 0 && (
<div className='content'> <div className='compose-form__uploads-wrapper'>
{mediaIds.map(id => ( {mediaIds.map(id => (
<UploadContainer id={id} key={id} /> <UploadContainer id={id} key={id} />
))} ))}

View file

@ -29,17 +29,18 @@ export default class UploadProgress extends React.PureComponent {
} }
return ( return (
<div className='composer--upload_form--progress'> <div className='upload-progress'>
<Icon id='upload' /> <div className='upload-progress__icon'>
<Icon id='upload' />
</div>
<div className='message'> <div className='upload-progress__message'>
{message} {message}
<div className='backdrop'> <div className='upload-progress__backdrop'>
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}> <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
{({ width }) => {({ width }) =>
(<div className='tracker' style={{ width: `${width}%` }} <div className='upload-progress__tracker' style={{ width: `${width}%` }} />
/>)
} }
</Motion> </Motion>
</div> </div>

View file

@ -15,7 +15,7 @@ export default class Warning extends React.PureComponent {
return ( return (
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => ( {({ opacity, scaleX, scaleY }) => (
<div className='composer--warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> <div className='compose-form__warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
{message} {message}
</div> </div>
)} )}

View file

@ -25,6 +25,7 @@ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
@ -45,10 +46,7 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow(account) { onFollow(account) {
if ( if (account.getIn(['relationship', 'following'])) {
account.getIn(['relationship', 'following']) ||
account.getIn(['relationship', 'requested'])
) {
if (unfollowModal) { if (unfollowModal) {
dispatch( dispatch(
openModal('CONFIRM', { openModal('CONFIRM', {
@ -66,6 +64,16 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} else { } else {
dispatch(unfollowAccount(account.get('id'))); dispatch(unfollowAccount(account.get('id')));
} }
} else if (account.getIn(['relationship', 'requested'])) {
if (unfollowModal) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
}));
} else {
dispatch(unfollowAccount(account.get('id')));
}
} else { } else {
dispatch(followAccount(account.get('id'))); dispatch(followAccount(account.get('id')));
} }

File diff suppressed because one or more lines are too long

View file

@ -64,7 +64,7 @@ const mapStateToProps = state => ({
showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']), showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
notifications: getNotifications(state), notifications: getNotifications(state),
localSettings: state.get('local_settings'), localSettings: state.get('local_settings'),
isLoading: state.getIn(['notifications', 'isLoading'], true), isLoading: state.getIn(['notifications', 'isLoading'], 0) > 0,
isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0, isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0,
hasMore: state.getIn(['notifications', 'hasMore']), hasMore: state.getIn(['notifications', 'hasMore']),
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,

View file

@ -0,0 +1,92 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { disabledAccountId, movedToAccountId, domain } from 'flavours/glitch/initial_state';
import { openModal } from 'flavours/glitch/actions/modal';
import { logOut } from 'flavours/glitch/utils/log_out';
const messages = defineMessages({
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
});
const mapStateToProps = (state) => ({
disabledAcct: state.getIn(['accounts', disabledAccountId, 'acct']),
movedToAcct: movedToAccountId ? state.getIn(['accounts', movedToAccountId, 'acct']) : undefined,
});
const mapDispatchToProps = (dispatch, { intl }) => ({
onLogout () {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
}));
},
});
export default @injectIntl
@connect(mapStateToProps, mapDispatchToProps)
class DisabledAccountBanner extends React.PureComponent {
static propTypes = {
disabledAcct: PropTypes.string.isRequired,
movedToAcct: PropTypes.string,
onLogout: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleLogOutClick = e => {
e.preventDefault();
e.stopPropagation();
this.props.onLogout();
return false;
}
render () {
const { disabledAcct, movedToAcct } = this.props;
const disabledAccountLink = (
<Link to={`/@${disabledAcct}`}>
{disabledAcct}@{domain}
</Link>
);
return (
<div className='sign-in-banner'>
<p>
{movedToAcct ? (
<FormattedMessage
id='moved_to_account_banner.text'
defaultMessage='Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.'
values={{
disabledAccount: disabledAccountLink,
movedToAccount: <Link to={`/@${movedToAcct}`}>{movedToAcct.includes('@') ? movedToAcct : `${movedToAcct}@${domain}`}</Link>,
}}
/>
) : (
<FormattedMessage
id='disabled_account_banner.text'
defaultMessage='Your account {disabledAccount} is currently disabled.'
values={{
disabledAccount: disabledAccountLink,
}}
/>
)}
</p>
<a href='/auth/edit' className='button button--block'>
<FormattedMessage id='disabled_account_banner.account_settings' defaultMessage='Account settings' />
</a>
<button type='button' className='button button--block button-tertiary' onClick={this.handleLogOutClick}>
<FormattedMessage id='confirmations.logout.confirm' defaultMessage='Log out' />
</button>
</div>
);
}
};

View file

@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { version, repository, source_url, profile_directory as profileDirectory } from 'flavours/glitch/initial_state'; import { domain, version, source_url, profile_directory as profileDirectory } from 'flavours/glitch/initial_state';
import { logOut } from 'flavours/glitch/utils/log_out'; import { logOut } from 'flavours/glitch/utils/log_out';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
import { PERMISSION_INVITE_USERS } from 'flavours/glitch/permissions'; import { PERMISSION_INVITE_USERS } from 'flavours/glitch/permissions';
@ -48,45 +48,42 @@ class LinkFooter extends React.PureComponent {
render () { render () {
const { signedIn, permissions } = this.context.identity; const { signedIn, permissions } = this.context.identity;
const items = [];
items.push(<a key='apps' href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Get the app' /></a>); const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
items.push(<Link key='about' to='/about'><FormattedMessage id='navigation_bar.info' defaultMessage='About' /></Link>); const canProfileDirectory = profileDirectory;
items.push(<a key='mastodon' href='https://joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.what_is_mastodon' defaultMessage='About Mastodon' /></a>);
items.push(<a key='docs' href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a>);
items.push(<Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></Link>);
items.push(<Link key='hotkeys' to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link>);
if (profileDirectory) {
items.push(<Link key='directory' to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Directory' /></Link>);
}
if (signedIn) {
if ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) {
items.push(<a key='invites' href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a>);
}
items.push(<a key='security' href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a>);
items.push(<a key='logout' href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a>);
}
return ( return (
<div className='getting-started__footer'> <div className='link-footer'>
<ul> <p>
{items.map((item, index, array) => ( <strong>{domain}</strong>:
<li>{item} { index === array.length - 1 ? null : ' · ' }</li> {' '}
))} <Link key='about' to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
</ul> {canInvite && (
<>
{' · '}
<a key='invites' href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
</>
)}
{canProfileDirectory && (
<>
{' · '}
<Link key='directory' to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
</>
)}
{' · '}
<Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
</p>
<p> <p>
<FormattedMessage <strong>Catstodon</strong>:
id='getting_started.open_source_notice' {' '}
defaultMessage='Catstodon is open source software, a friendly fork of {glitchsoc}, which in turn is a fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.' <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a>
values={{ {' · '}
github: <span><a href={source_url} rel='noopener noreferrer' target='_blank'>{repository}</a> (v{version})</span>, <Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link>
glitchsoc: <a href='https://github.com/glitch-soc/mastodon' rel='noopener noreferrer' target='_blank'>glitch-soc</a>, {' · '}
Mastodon: <a href='https://github.com/mastodon/mastodon' rel='noopener noreferrer' target='_blank'>Mastodon</a> }} <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
/> {' · '}
v{version}
</p> </p>
</div> </div>
); );

View file

@ -4,6 +4,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { timelinePreview, showTrends } from 'flavours/glitch/initial_state'; import { timelinePreview, showTrends } from 'flavours/glitch/initial_state';
import ColumnLink from 'flavours/glitch/features/ui/components/column_link'; import ColumnLink from 'flavours/glitch/features/ui/components/column_link';
import DisabledAccountBanner from './disabled_account_banner';
import FollowRequestsColumnLink from './follow_requests_column_link'; import FollowRequestsColumnLink from './follow_requests_column_link';
import ListPanel from './list_panel'; import ListPanel from './list_panel';
import NotificationsCounterIcon from './notifications_counter_icon'; import NotificationsCounterIcon from './notifications_counter_icon';
@ -42,7 +43,7 @@ class NavigationPanel extends React.Component {
render() { render() {
const { intl, onOpenSettings } = this.props; const { intl, onOpenSettings } = this.props;
const { signedIn } = this.context.identity; const { signedIn, disabledAccountId } = this.context.identity;
return ( return (
<div className='navigation-panel'> <div className='navigation-panel'>
@ -70,7 +71,7 @@ class NavigationPanel extends React.Component {
{!signedIn && ( {!signedIn && (
<div className='navigation-panel__sign-in-banner'> <div className='navigation-panel__sign-in-banner'>
<hr /> <hr />
<SignInBanner /> { disabledAccountId ? <DisabledAccountBanner /> : <SignInBanner /> }
</div> </div>
)} )}

View file

@ -54,6 +54,7 @@
* @property {boolean} crop_images * @property {boolean} crop_images
* @property {boolean=} delete_modal * @property {boolean=} delete_modal
* @property {boolean=} disable_swiping * @property {boolean=} disable_swiping
* @property {string=} disabled_account_id
* @property {boolean} display_media * @property {boolean} display_media
* @property {string} domain * @property {string} domain
* @property {boolean=} expand_spoilers * @property {boolean=} expand_spoilers
@ -61,6 +62,7 @@
* @property {string} locale * @property {string} locale
* @property {string | null} mascot * @property {string | null} mascot
* @property {string=} me * @property {string=} me
* @property {string=} moved_to_account_id
* @property {string=} owner * @property {string=} owner
* @property {boolean} profile_directory * @property {boolean} profile_directory
* @property {boolean} registrations_open * @property {boolean} registrations_open
@ -111,6 +113,7 @@ export const boostModal = getMeta('boost_modal');
export const cropImages = getMeta('crop_images'); export const cropImages = getMeta('crop_images');
export const deleteModal = getMeta('delete_modal'); export const deleteModal = getMeta('delete_modal');
export const disableSwiping = getMeta('disable_swiping'); export const disableSwiping = getMeta('disable_swiping');
export const disabledAccountId = getMeta('disabled_account_id');
export const displayMedia = getMeta('display_media'); export const displayMedia = getMeta('display_media');
export const domain = getMeta('domain'); export const domain = getMeta('domain');
export const expandSpoilers = getMeta('expand_spoilers'); export const expandSpoilers = getMeta('expand_spoilers');
@ -118,6 +121,7 @@ export const forceSingleColumn = !getMeta('advanced_layout');
export const limitedFederationMode = getMeta('limited_federation_mode'); export const limitedFederationMode = getMeta('limited_federation_mode');
export const mascot = getMeta('mascot'); export const mascot = getMeta('mascot');
export const me = getMeta('me'); export const me = getMeta('me');
export const movedToAccountId = getMeta('moved_to_account_id');
export const owner = getMeta('owner'); export const owner = getMeta('owner');
export const profile_directory = getMeta('profile_directory'); export const profile_directory = getMeta('profile_directory');
export const reduceMotion = getMeta('reduce_motion'); export const reduceMotion = getMeta('reduce_motion');

View file

@ -52,20 +52,26 @@ const initialState = ImmutableMap({
markNewForDelete: false, markNewForDelete: false,
}); });
const notificationToMap = (state, notification) => ImmutableMap({ const notificationToMap = (notification, markForDelete) => ImmutableMap({
id: notification.id, id: notification.id,
type: notification.type, type: notification.type,
account: notification.account.id, account: notification.account.id,
markedForDelete: state.get('markNewForDelete'), markedForDelete: markForDelete,
status: notification.status ? notification.status.id : null, status: notification.status ? notification.status.id : null,
report: notification.report ? fromJS(notification.report) : null, report: notification.report ? fromJS(notification.report) : null,
}); });
const normalizeNotification = (state, notification, usePendingItems) => { const normalizeNotification = (state, notification, usePendingItems) => {
const markNewForDelete = state.get('markNewForDelete');
const top = state.get('top'); const top = state.get('top');
// Under currently unknown conditions, the client may receive duplicates from the server
if (state.get('pendingItems').some((item) => item?.get('id') === notification.id) || state.get('items').some((item) => item?.get('id') === notification.id)) {
return state;
}
if (usePendingItems || !state.get('pendingItems').isEmpty()) { if (usePendingItems || !state.get('pendingItems').isEmpty()) {
return state.update('pendingItems', list => list.unshift(notificationToMap(state, notification))).update('unread', unread => unread + 1); return state.update('pendingItems', list => list.unshift(notificationToMap(notification, markNewForDelete))).update('unread', unread => unread + 1);
} }
if (shouldCountUnreadNotifications(state)) { if (shouldCountUnreadNotifications(state)) {
@ -79,32 +85,79 @@ const normalizeNotification = (state, notification, usePendingItems) => {
list = list.take(20); list = list.take(20);
} }
return list.unshift(notificationToMap(state, notification)); return list.unshift(notificationToMap(notification, markNewForDelete));
}); });
}; };
const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => { const expandNormalizedNotifications = (state, notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) => {
const lastReadId = state.get('lastReadId'); // This method is pretty tricky because:
let items = ImmutableList(); // - existing notifications might be out of order
// - the existing notifications may have gaps, most often explicitly noted with a `null` item
// - ideally, we don't want it to reorder existing items
// - `notifications` may include items that are already included
// - this function can be called either to fill in a gap, or load newer items
notifications.forEach((n, i) => { const markNewForDelete = state.get('markNewForDelete');
items = items.set(i, notificationToMap(state, n)); const lastReadId = state.get('lastReadId');
}); const newItems = ImmutableList(notifications.map((notification) => notificationToMap(notification, markNewForDelete)));
return state.withMutations(mutable => { return state.withMutations(mutable => {
if (!items.isEmpty()) { if (!newItems.isEmpty()) {
usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty()); usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty());
mutable.update(usePendingItems ? 'pendingItems' : 'items', list => { mutable.update(usePendingItems ? 'pendingItems' : 'items', oldItems => {
const lastIndex = 1 + list.findLastIndex( // If called to poll *new* notifications, we just need to add them on top without duplicates
item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')), if (isLoadingRecent) {
); const idsToCheck = oldItems.map(item => item?.get('id')).toSet();
const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id')));
return insertedItems.concat(oldItems);
}
const firstIndex = 1 + list.take(lastIndex).findLastIndex( // If called to expand more (presumably older than any known to the WebUI), we just have to
item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0, // add them to the bottom without duplicates
); if (isLoadingMore) {
const idsToCheck = oldItems.map(item => item?.get('id')).toSet();
const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id')));
return oldItems.concat(insertedItems);
}
return list.take(firstIndex).concat(items, list.skip(lastIndex)); // Now this gets tricky, as we don't necessarily know for sure where the gap to fill is,
// and some items in the timeline may not be properly ordered.
// However, we know that `newItems.last()` is the oldest item that was requested and that
// there is no “hole” between `newItems.last()` and `newItems.first()`.
// First, find the furthest (if properly sorted, oldest) item in the notifications that is
// newer than the oldest fetched one, as it's most likely that it delimits the gap.
// Start the gap *after* that item.
const lastIndex = oldItems.findLastIndex(item => item !== null && compareId(item.get('id'), newItems.last().get('id')) >= 0) + 1;
// Then, try to find the furthest (if properly sorted, oldest) item in the notifications that
// is newer than the most recent fetched one, as it delimits a section comprised of only
// items older or within `newItems` (or that were deleted from the server, so should be removed
// anyway).
// Stop the gap *after* that item.
const firstIndex = oldItems.take(lastIndex).findLastIndex(item => item !== null && compareId(item.get('id'), newItems.first().get('id')) > 0) + 1;
// At this point:
// - no `oldItems` after `firstIndex` is newer than any of the `newItems`
// - all `oldItems` after `lastIndex` are older than every of the `newItems`
// - it is possible for items in the replaced slice to be older than every `newItems`
// - it is possible for items before `firstIndex` to be in the `newItems` range
// Therefore:
// - to avoid losing items, items from the replaced slice that are older than `newItems`
// should be added in the back.
// - to avoid duplicates, `newItems` should be checked the first `firstIndex` items of
// `oldItems`
const idsToCheck = oldItems.take(firstIndex).map(item => item?.get('id')).toSet();
const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id')));
const olderItems = oldItems.slice(firstIndex, lastIndex).filter(item => item !== null && compareId(item.get('id'), newItems.last().get('id')) < 0);
return oldItems.take(firstIndex).concat(
insertedItems,
olderItems,
oldItems.skip(lastIndex),
);
}); });
} }
@ -115,7 +168,7 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
if (shouldCountUnreadNotifications(state)) { if (shouldCountUnreadNotifications(state)) {
mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), lastReadId) > 0)); mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), lastReadId) > 0));
} else { } else {
const mostRecent = items.find(item => item !== null); const mostRecent = newItems.find(item => item !== null);
if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) { if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) {
mutable.set('lastReadId', mostRecent.get('id')); mutable.set('lastReadId', mostRecent.get('id'));
} }
@ -264,7 +317,7 @@ export default function notifications(state = initialState, action) {
case NOTIFICATIONS_UPDATE: case NOTIFICATIONS_UPDATE:
return normalizeNotification(state, action.notification, action.usePendingItems); return normalizeNotification(state, action.notification, action.usePendingItems);
case NOTIFICATIONS_EXPAND_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS:
return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingRecent, action.usePendingItems); return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingMore, action.isLoadingRecent, action.usePendingItems);
case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_BLOCK_SUCCESS:
return filterNotifications(state, [action.relationship.id]); return filterNotifications(state, [action.relationship.id]);
case ACCOUNT_MUTE_SUCCESS: case ACCOUNT_MUTE_SUCCESS:

View file

@ -29,22 +29,22 @@ $emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange'
.hicolor-privacy-icons { .hicolor-privacy-icons {
.status__visibility-icon.fa-globe, .status__visibility-icon.fa-globe,
.composer--options--dropdown--content--item .fa-globe { .privacy-dropdown__option .fa-globe {
color: #1976d2; color: #1976d2;
} }
.status__visibility-icon.fa-unlock, .status__visibility-icon.fa-unlock,
.composer--options--dropdown--content--item .fa-unlock { .privacy-dropdown__option .fa-unlock {
color: #388e3c; color: #388e3c;
} }
.status__visibility-icon.fa-lock, .status__visibility-icon.fa-lock,
.composer--options--dropdown--content--item .fa-lock { .privacy-dropdown__option .fa-lock {
color: #ffa000; color: #ffa000;
} }
.status__visibility-icon.fa-envelope, .status__visibility-icon.fa-envelope,
.composer--options--dropdown--content--item .fa-envelope { .privacy-dropdown__option .fa-envelope {
color: #d32f2f; color: #d32f2f;
} }
} }

View file

@ -30,6 +30,34 @@
} }
} }
.link-footer {
flex: 0 0 auto;
padding: 10px;
padding-top: 20px;
z-index: 1;
font-size: 13px;
p {
color: $dark-text-color;
margin-bottom: 20px;
strong {
font-weight: 500;
}
a {
color: $dark-text-color;
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
}
}
.about { .about {
padding: 20px; padding: 20px;
@ -37,6 +65,14 @@
border-radius: 4px; border-radius: 4px;
} }
&__footer {
color: $dark-text-color;
text-align: center;
font-size: 15px;
line-height: 22px;
margin-top: 20px;
}
&__header { &__header {
margin-bottom: 30px; margin-bottom: 30px;
@ -157,7 +193,7 @@
} }
} }
.getting-started__footer { .link-footer {
padding: 0; padding: 0;
margin-top: 60px; margin-top: 60px;
text-align: center; text-align: center;

View file

@ -1,4 +1,4 @@
.composer { .compose-form {
padding: 10px; padding: 10px;
.emoji-picker-dropdown { .emoji-picker-dropdown {
@ -25,16 +25,16 @@
} }
} }
.no-reduce-motion .composer--spoiler { .no-reduce-motion .spoiler-input {
transition: height 0.4s ease, opacity 0.4s ease; transition: height 0.4s ease, opacity 0.4s ease;
} }
.composer--spoiler { .spoiler-input {
height: 0; height: 0;
transform-origin: bottom; transform-origin: bottom;
opacity: 0.0; opacity: 0.0;
&.composer--spoiler--visible { &.spoiler-input--visible {
height: 36px; height: 36px;
margin-bottom: 11px; margin-bottom: 11px;
opacity: 1.0; opacity: 1.0;
@ -64,7 +64,7 @@
} }
} }
.composer--warning { .compose-form__warning {
color: $inverted-text-color; color: $inverted-text-color;
margin-bottom: 15px; margin-bottom: 15px;
background: $ui-primary-color; background: $ui-primary-color;
@ -123,7 +123,7 @@
} }
} }
.composer--reply { .reply-indicator {
margin: 0 0 10px; margin: 0 0 10px;
border-radius: 4px; border-radius: 4px;
padding: 10px; padding: 10px;
@ -131,117 +131,117 @@
min-height: 23px; min-height: 23px;
overflow-y: auto; overflow-y: auto;
flex: 0 2 auto; flex: 0 2 auto;
}
& > header { .reply-indicator__header {
margin-bottom: 5px; margin-bottom: 5px;
overflow: hidden; overflow: hidden;
& > .account.small { color: $inverted-text-color; } & > .account.small { color: $inverted-text-color; }
}
& > .cancel { .reply-indicator__cancel {
float: right; float: right;
line-height: 24px; line-height: 24px;
}
.reply-indicator__content {
position: relative;
margin: 10px 0;
padding: 0 12px;
font-size: 14px;
line-height: 20px;
color: $inverted-text-color;
word-wrap: break-word;
font-weight: 400;
overflow: visible;
white-space: pre-wrap;
padding-top: 5px;
overflow: hidden;
p, pre, blockquote {
margin-bottom: 20px;
white-space: pre-wrap;
&:last-child {
margin-bottom: 0;
} }
} }
& > .content { h1, h2, h3, h4, h5 {
position: relative; margin-top: 20px;
margin: 10px 0; margin-bottom: 20px;
padding: 0 12px; }
font-size: 14px;
line-height: 20px; h1, h2 {
font-weight: 700;
font-size: 18px;
}
h2 {
font-size: 16px;
}
h3, h4, h5 {
font-weight: 500;
}
blockquote {
padding-left: 10px;
border-left: 3px solid $inverted-text-color;
color: $inverted-text-color; color: $inverted-text-color;
word-wrap: break-word; white-space: normal;
font-weight: 400;
overflow: visible;
white-space: pre-wrap;
padding-top: 5px;
overflow: hidden;
p, pre, blockquote { p:last-child {
margin-bottom: 20px; margin-bottom: 0;
white-space: pre-wrap;
&:last-child {
margin-bottom: 0;
}
} }
}
h1, h2, h3, h4, h5 { b, strong {
margin-top: 20px; font-weight: 700;
margin-bottom: 20px; }
em, i {
font-style: italic;
}
sub {
font-size: smaller;
vertical-align: sub;
}
sup {
font-size: smaller;
vertical-align: super;
}
ul, ol {
margin-left: 1em;
p {
margin: 0;
} }
}
h1, h2 { ul {
font-weight: 700; list-style-type: disc;
font-size: 18px; }
}
h2 { ol {
font-size: 16px; list-style-type: decimal;
} }
h3, h4, h5 { a {
font-weight: 500; color: $lighter-text-color;
} text-decoration: none;
blockquote { &:hover { text-decoration: underline }
padding-left: 10px;
border-left: 3px solid $inverted-text-color;
color: $inverted-text-color;
white-space: normal;
p:last-child { &.mention {
margin-bottom: 0; &:hover {
} text-decoration: none;
}
b, strong { span { text-decoration: underline }
font-weight: 700;
}
em, i {
font-style: italic;
}
sub {
font-size: smaller;
vertical-align: sub;
}
sup {
font-size: smaller;
vertical-align: super;
}
ul, ol {
margin-left: 1em;
p {
margin: 0;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
a {
color: $lighter-text-color;
text-decoration: none;
&:hover { text-decoration: underline }
&.mention {
&:hover {
text-decoration: none;
span { text-decoration: underline }
}
} }
} }
} }
@ -253,8 +253,12 @@
} }
} }
.compose-form__autosuggest-wrapper, .compose-form .compose-form__autosuggest-wrapper {
.autosuggest-input { position: relative;
}
.compose-form .autosuggest-textarea,
.compose-form .autosuggest-input {
position: relative; position: relative;
width: 100%; width: 100%;
@ -284,10 +288,6 @@
all: unset; all: unset;
} }
&:disabled {
background: $ui-secondary-color;
}
&:focus { &:focus {
outline: 0; outline: 0;
} }
@ -304,7 +304,7 @@
} }
} }
.composer--textarea--icons { .compose-form__textarea-icons {
display: block; display: block;
position: absolute; position: absolute;
top: 29px; top: 29px;
@ -401,25 +401,25 @@
} }
} }
.composer--upload_form { .compose-form__upload-wrapper {
overflow: hidden; overflow: hidden;
& > .content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
font-family: inherit;
padding: 5px;
overflow: hidden;
}
} }
.composer--upload_form--item { .compose-form__uploads-wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
font-family: inherit;
padding: 5px;
overflow: hidden;
}
.compose-form__upload {
flex: 1 1 0; flex: 1 1 0;
margin: 5px; margin: 5px;
min-width: 40%; min-width: 40%;
& > div { .compose-form__upload-thumbnail {
position: relative; position: relative;
border-radius: 4px; border-radius: 4px;
height: 140px; height: 140px;
@ -459,54 +459,54 @@
} }
} }
.composer--upload_form--actions { .compose-form__upload__actions {
background: linear-gradient(180deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent); background: linear-gradient(180deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
} }
.composer--upload_form--progress { .upload-progress {
display: flex; display: flex;
padding: 10px; padding: 10px;
color: $darker-text-color; color: $darker-text-color;
overflow: hidden; overflow: hidden;
& > .fa { .fa {
font-size: 34px; font-size: 34px;
margin-right: 10px; margin-right: 10px;
} }
& > .message { span {
flex: 1 1 auto; display: block;
font-size: 12px;
& > span { font-weight: 500;
display: block; text-transform: uppercase;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
& > .backdrop {
position: relative;
margin-top: 5px;
border-radius: 6px;
width: 100%;
height: 6px;
background: $ui-base-lighter-color;
& > .tracker {
position: absolute;
top: 0;
left: 0;
height: 6px;
border-radius: 6px;
background: $ui-highlight-color;
}
}
} }
} }
.upload-progress__message {
flex: 1 1 auto;
}
.upload-progress__backdrop {
position: relative;
margin-top: 5px;
border-radius: 6px;
width: 100%;
height: 6px;
background: $ui-base-lighter-color;
}
.upload-progress__tracker {
position: absolute;
top: 0;
left: 0;
height: 6px;
border-radius: 6px;
background: $ui-highlight-color;
}
.compose-form__modifiers { .compose-form__modifiers {
color: $inverted-text-color; color: $inverted-text-color;
font-family: inherit; font-family: inherit;
@ -514,7 +514,7 @@
background: $simple-background-color; background: $simple-background-color;
} }
.composer--options-wrapper { .compose-form__buttons-wrapper {
padding: 10px; padding: 10px;
background: darken($simple-background-color, 8%); background: darken($simple-background-color, 8%);
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
@ -524,7 +524,7 @@
flex: 0 0 auto; flex: 0 0 auto;
} }
.composer--options { .compose-form__buttons {
display: flex; display: flex;
flex: 0 0 auto; flex: 0 0 auto;
@ -551,30 +551,41 @@
} }
} }
.compose--counter-wrapper { .character-counter__wrapper {
align-self: center; align-self: center;
margin-right: 4px; margin-right: 4px;
} }
.composer--options--dropdown { .privacy-dropdown.active {
&.open { .privacy-dropdown__value {
& > .value { background: $simple-background-color;
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1); box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1);
color: $primary-text-color;
background: $ui-highlight-color; .icon-button {
transition: none; transition: none;
} }
&.top {
& > .value { &.active {
border-radius: 0 0 4px 4px; background: $ui-highlight-color;
box-shadow: 0 4px 4px rgba($base-shadow-color, 0.1);
.icon-button {
color: $primary-text-color;
} }
} }
} }
&.top .privacy-dropdown__value {
border-radius: 0 0 4px 4px;
}
.privacy-dropdown__dropdown {
display: block;
box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1);
}
} }
.composer--options--dropdown--content { .privacy-dropdown__dropdown {
position: absolute; position: absolute;
border-radius: 4px; border-radius: 4px;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
@ -583,14 +594,14 @@
transform-origin: 50% 0; transform-origin: 50% 0;
} }
.composer--options--dropdown--content--item { .privacy-dropdown__option {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 10px; padding: 10px;
color: $inverted-text-color; color: $inverted-text-color;
cursor: pointer; cursor: pointer;
& > .content { .privacy-dropdown__option__content {
flex: 1 1 auto; flex: 1 1 auto;
color: $lighter-text-color; color: $lighter-text-color;
@ -608,7 +619,7 @@
background: $ui-highlight-color; background: $ui-highlight-color;
color: $primary-text-color; color: $primary-text-color;
& > .content { .privacy-dropdown__option__content {
color: $primary-text-color; color: $primary-text-color;
strong { color: $primary-text-color } strong { color: $primary-text-color }
@ -618,31 +629,25 @@
&.active:hover { background: lighten($ui-highlight-color, 4%) } &.active:hover { background: lighten($ui-highlight-color, 4%) }
} }
.composer--publisher { .compose-form__publish {
padding-top: 10px; display: flex;
text-align: right;
white-space: nowrap;
overflow: hidden;
justify-content: flex-end; justify-content: flex-end;
min-width: 0;
flex: 0 0 auto; flex: 0 0 auto;
column-gap: 5px;
& > .primary { .compose-form__publish-button-wrapper {
display: inline-block; overflow: hidden;
margin: 0; padding-top: 10px;
padding: 7px 10px;
text-align: center;
}
& > .side_arm { button {
display: inline-block; padding: 7px 10px;
margin: 0 5px; text-align: center;
padding: 7px 0; }
width: 36px;
text-align: center;
}
&.over { & > .side_arm {
& > .count { color: $warning-red } width: 36px;
}
} }
} }

View file

@ -1044,43 +1044,6 @@
color: $dark-text-color; color: $dark-text-color;
} }
&__footer {
flex: 0 0 auto;
padding: 10px;
padding-top: 20px;
z-index: 1;
font-size: 13px;
ul {
margin-bottom: 10px;
}
ul li {
display: inline;
}
p {
color: $dark-text-color;
margin-bottom: 20px;
a {
color: $dark-text-color;
text-decoration: underline;
}
}
a {
text-decoration: none;
color: $darker-text-color;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
&__trends { &__trends {
flex: 0 1 auto; flex: 0 1 auto;
opacity: 1; opacity: 1;
@ -1775,7 +1738,7 @@ noscript {
@import 'domains'; @import 'domains';
@import 'status'; @import 'status';
@import 'modal'; @import 'modal';
@import 'composer'; @import 'compose_form';
@import 'columns'; @import 'columns';
@import 'regeneration_indicator'; @import 'regeneration_indicator';
@import 'directory'; @import 'directory';

View file

@ -1290,11 +1290,11 @@
} }
} }
.modal-root__container .composer--options--dropdown { .modal-root__container .privacy-dropdown {
flex-grow: 0; flex-grow: 0;
} }
.modal-root__container .composer--options--dropdown--content { .modal-root__container .privacy-dropdown__dropdown {
pointer-events: auto; pointer-events: auto;
z-index: 9999; z-index: 9999;
} }

View file

@ -4,6 +4,20 @@
p { p {
color: $darker-text-color; color: $darker-text-color;
margin-bottom: 20px; margin-bottom: 20px;
a {
color: $secondary-text-color;
text-decoration: none;
unicode-bidi: isolate;
&:hover {
text-decoration: underline;
.fa {
color: lighten($dark-text-color, 7%);
}
}
}
} }
.button { .button {

View file

@ -41,7 +41,7 @@
flex: 0 1 48px; flex: 0 1 48px;
} }
.composer { .compose-form {
flex: 1; flex: 1;
overflow-y: hidden; overflow-y: hidden;
display: flex; display: flex;
@ -59,10 +59,6 @@
.autosuggest-textarea__textarea { .autosuggest-textarea__textarea {
overflow-y: hidden; overflow-y: hidden;
} }
.compose-form__upload-thumbnail {
height: 80px;
}
} }
.navigation-panel { .navigation-panel {

View file

@ -37,7 +37,7 @@
} }
.compose-standalone { .compose-standalone {
.composer { .compose-form {
width: 400px; width: 400px;
margin: 0 auto; margin: 0 auto;
padding: 20px 0; padding: 20px 0;

View file

@ -1,36 +1,20 @@
// components.scss .compose-form {
.compose-form__modifiers {
.status__content a, .compose-form__upload {
.reply-indicator__content a { &-description {
color: lighten($ui-highlight-color, 12%); input {
text-decoration: underline; &::placeholder {
opacity: 1;
&.mention { }
text-decoration: none; }
} }
&.mention span {
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
} }
} }
&:hover,
&:focus,
&:active {
text-decoration: none;
}
&.status__content__spoiler-link {
color: $secondary-text-color;
text-decoration: none;
}
} }
.status__content a,
.link-footer a,
.reply-indicator__content a,
.status__content__read-more-button { .status__content__read-more-button {
text-decoration: underline; text-decoration: underline;
@ -39,31 +23,56 @@
&:active { &:active {
text-decoration: none; text-decoration: none;
} }
&.mention {
text-decoration: none;
span {
text-decoration: underline;
}
&:hover,
&:focus,
&:active {
span {
text-decoration: none;
}
}
}
} }
.getting-started__footer a { .status__content a {
text-decoration: underline; color: $highlight-text-color;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
} }
.nothing-here { .nothing-here {
color: $darker-text-color; color: $darker-text-color;
} }
.public-layout .public-account-header__tabs__tabs .counter.active::after { .compose-form__poll-wrapper .button.button-secondary,
border-bottom: 4px solid $ui-highlight-color; .compose-form .autosuggest-textarea__textarea::placeholder,
.compose-form .spoiler-input__input::placeholder,
.report-dialog-modal__textarea::placeholder,
.language-dropdown__dropdown__results__item__common-name,
.compose-form .icon-button {
color: $inverted-text-color;
} }
.composer { .text-icon-button.active {
.composer--spoiler input, color: $ui-highlight-color;
.compose-form__autosuggest-wrapper textarea { }
&::placeholder {
color: $inverted-text-color; .language-dropdown__dropdown__results__item.active {
} background: $ui-highlight-color;
font-weight: 500;
}
.link-button:disabled {
cursor: not-allowed;
&:hover,
&:focus,
&:active {
text-decoration: none !important;
} }
} }

View file

@ -14,8 +14,8 @@ $ui-highlight-color: $classic-highlight-color !default;
$darker-text-color: lighten($ui-primary-color, 20%) !default; $darker-text-color: lighten($ui-primary-color, 20%) !default;
$dark-text-color: lighten($ui-primary-color, 12%) !default; $dark-text-color: lighten($ui-primary-color, 12%) !default;
$secondary-text-color: lighten($ui-secondary-color, 6%) !default; $secondary-text-color: lighten($ui-secondary-color, 6%) !default;
$highlight-text-color: lighten($ui-highlight-color, 8%) !default; $highlight-text-color: lighten($ui-highlight-color, 10%) !default;
$action-button-color: #8d9ac2; $action-button-color: lighten($ui-base-color, 50%);
$inverted-text-color: $black !default; $inverted-text-color: $black !default;
$lighter-text-color: darken($ui-base-color,6%) !default; $lighter-text-color: darken($ui-base-color,6%) !default;

View file

@ -1,200 +1,250 @@
// Notes! // Notes!
// Sass color functions, "darken" and "lighten" are automatically replaced. // Sass color functions, "darken" and "lighten" are automatically replaced.
.glitch.local-settings { html {
background: $ui-base-color; scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
}
&__navigation { // Change the colors of button texts
background: darken($ui-base-color, 8%); .button {
color: $white;
&.button-alternative-2 {
color: $white;
}
}
.status-card__actions button,
.status-card__actions a {
color: rgba($white, 0.8);
&:hover,
&:active,
&:focus {
color: $white;
}
}
// Change default background colors of columns
.column > .scrollable,
.getting-started,
.column-inline-form,
.error-column,
.regeneration-indicator {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.column > .scrollable.about {
border-top: 1px solid lighten($ui-base-color, 8%);
}
.about__meta,
.about__section__title,
.interaction-modal {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
}
.rules-list li::before {
background: $ui-highlight-color;
}
.directory__card__img {
background: lighten($ui-base-color, 12%);
}
.filter-form {
background: $white;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.column-back-button,
.column-header {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
} }
&__navigation__item { &--slim-button {
background: darken($ui-base-color, 8%); top: -50px;
right: 0;
}
}
&:hover { .column-header__back-button,
.column-header__button,
.column-header__button.active,
.account__header__bar {
background: $white;
}
.column-header__button.active {
color: $ui-highlight-color;
&:hover,
&:active,
&:focus {
color: $ui-highlight-color;
background: $white;
}
}
.account__header__bar .avatar .account__avatar {
border-color: $white;
}
.getting-started__footer a {
color: $ui-secondary-color;
text-decoration: underline;
}
.confirmation-modal__secondary-button,
.confirmation-modal__cancel-button,
.mute-modal__cancel-button,
.block-modal__cancel-button {
color: lighten($ui-base-color, 26%);
&:hover,
&:focus,
&:active {
color: $primary-text-color;
}
}
.column-subheading {
background: darken($ui-base-color, 4%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.getting-started,
.scrollable {
.column-link {
background: $white;
border-bottom: 1px solid lighten($ui-base-color, 8%);
&:hover,
&:active,
&:focus {
background: $ui-base-color; background: $ui-base-color;
} }
} }
} }
.notification__dismiss-overlay { .getting-started .navigation-bar {
.wrappy { border-top: 1px solid lighten($ui-base-color, 8%);
box-shadow: unset; border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.ckbox { @media screen and (max-width: $no-gap-breakpoint) {
text-shadow: unset; border-top: 0;
} }
} }
.status.status-direct { .compose-form__autosuggest-wrapper,
background: darken($ui-base-color, 8%); .poll__option input[type="text"],
border-bottom-color: darken($ui-base-color, 12%); .compose-form .spoiler-input__input,
.compose-form__poll-wrapper select,
.search__input,
.setting-text,
.report-dialog-modal__textarea,
.audio-player {
border: 1px solid lighten($ui-base-color, 8%);
}
&.collapsed> .status__content:after { .report-dialog-modal .dialog-option .poll__input {
background: linear-gradient(rgba(darken($ui-base-color, 8%), 0), rgba(darken($ui-base-color, 8%), 1)); color: $white;
}
.search__input {
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
border-bottom: 0;
} }
} }
.focusable:focus.status.status-direct { .list-editor .search .search__input {
background: darken($ui-base-color, 4%); border-top: 0;
border-bottom: 0;
&.collapsed> .status__content:after {
background: linear-gradient(rgba(darken($ui-base-color, 4%), 0), rgba(darken($ui-base-color, 4%), 1));
}
} }
// Change columns' default background colors .compose-form__poll-wrapper select {
.column { background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>") no-repeat right 8px center / auto 16px;
> .scrollable {
background: darken($ui-base-color, 13%);
}
} }
.status.collapsed .status__content:after { .compose-form__poll-wrapper,
background: linear-gradient(rgba(darken($ui-base-color, 13%), 0), rgba(darken($ui-base-color, 13%), 1)); .compose-form__poll-wrapper .poll__footer {
border-top-color: lighten($ui-base-color, 8%);
} }
.drawer__inner { .notification__filter-bar {
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.compose-form .compose-form__buttons-wrapper {
background: $ui-base-color; background: $ui-base-color;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.drawer__header,
.drawer__inner {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
} }
.drawer__inner__mastodon { .drawer__inner__mastodon {
background: $ui-base-color url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color(darken($ui-base-color, 13%))}"/></svg>') no-repeat bottom / 100% auto !important; background: $white url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto;
.mastodon {
filter: contrast(75%) brightness(75%) !important;
}
} }
// Change the default appearance of the content warning button // Change the colors used in compose-form
.status__content { .compose-form {
.compose-form__modifiers {
.compose-form__upload__actions .icon-button {
color: lighten($white, 7%);
.status__content__spoiler-link { &:active,
&:focus,
background: lighten($ui-base-color, 30%); &:hover {
color: $white;
&:hover { }
background: lighten($ui-base-color, 35%);
text-decoration: none;
} }
} .compose-form__upload-description input {
color: lighten($white, 7%);
} &::placeholder {
color: lighten($white, 7%);
// Change the background colors of media and video spoilers }
.media-spoiler,
.video-player__spoiler,
.account-gallery__item a {
background: $ui-base-color;
}
// Change the colors used in the dropdown menu
.dropdown-menu {
background: $ui-base-color;
}
.dropdown-menu__arrow {
&.left {
border-left-color: $ui-base-color;
}
&.top {
border-top-color: $ui-base-color;
}
&.bottom {
border-bottom-color: $ui-base-color;
}
&.right {
border-right-color: $ui-base-color;
}
}
.dropdown-menu__item {
a,
button {
background: $ui-base-color;
color: $ui-secondary-color;
}
}
// Change the default color of several parts of the compose form
.composer {
.composer--spoiler input, .compose-form__autosuggest-wrapper textarea {
color: lighten($ui-base-color, 80%);
&:disabled { background: lighten($simple-background-color, 10%) }
&::placeholder {
color: lighten($ui-base-color, 70%);
} }
} }
.composer--options-wrapper { .compose-form__buttons-wrapper {
background: lighten($ui-base-color, 10%); background: darken($ui-base-color, 6%);
} }
.composer--options > hr { .autosuggest-textarea__suggestions {
display: none; background: darken($ui-base-color, 6%);
} }
.composer--options--dropdown--content--item { .autosuggest-textarea__suggestions__item {
color: $ui-primary-color; &:hover,
&:focus,
strong { &:active,
color: $ui-primary-color; &.selected {
background: lighten($ui-base-color, 4%);
} }
}
}
.composer--upload_form--actions .icon-button {
color: lighten($white, 7%);
&:active,
&:focus,
&:hover {
color: $white;
}
}
.language-dropdown__dropdown__results__item:hover,
.language-dropdown__dropdown__results__item:focus,
.language-dropdown__dropdown__results__item:active {
background-color: $ui-base-color;
}
.dropdown-menu__separator,
.dropdown-menu__item.edited-timestamp__history__item,
.dropdown-menu__container__header,
.compare-history-modal .report-modal__target,
.report-dialog-modal .poll__option.dialog-option {
border-bottom-color: lighten($ui-base-color, 12%);
}
.report-dialog-modal__container {
border-bottom-color: lighten($ui-base-color, 12%);
}
.status__content,
.reply-indicator__content {
a {
color: $highlight-text-color;
} }
} }
.emoji-mart-bar { .emoji-mart-bar {
border-color: darken($ui-base-color, 4%); border-color: lighten($ui-base-color, 4%);
&:first-child { &:first-child {
background: lighten($ui-base-color, 10%); background: darken($ui-base-color, 6%);
} }
} }
@ -203,35 +253,120 @@
border-color: $ui-base-color; border-color: $ui-base-color;
} }
.autosuggest-textarea__suggestions { // Change the background colors of statuses
background: lighten($ui-base-color, 10%) .focusable:focus {
background: $ui-base-color;
} }
.autosuggest-textarea__suggestions__item { .detailed-status,
&:hover, .detailed-status__action-bar {
&:focus, background: $white;
&:active, }
&.selected {
background: darken($ui-base-color, 4%); // Change the background colors of status__content__spoiler-link
.reply-indicator__content .status__content__spoiler-link,
.status__content .status__content__spoiler-link {
background: $ui-base-color;
&:hover {
background: lighten($ui-base-color, 4%);
} }
} }
.react-toggle-track { // Change the background colors of media and video spoilers
background: $ui-secondary-color; .media-spoiler,
.video-player__spoiler {
background: $ui-base-color;
} }
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { .privacy-dropdown.active .privacy-dropdown__value.active .icon-button {
background: lighten($ui-secondary-color, 10%); color: $white;
} }
.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { .account-gallery__item a {
background: darken($ui-highlight-color, 10%); background-color: $ui-base-color;
}
// Change the colors used in the dropdown menu
.dropdown-menu {
background: $white;
&__arrow {
&.left {
border-left-color: $white;
}
&.top {
border-top-color: $white;
}
&.bottom {
border-bottom-color: $white;
}
&.right {
border-right-color: $white;
}
}
&__item {
a,
button {
background: $white;
color: $darker-text-color;
}
}
}
// Change the text colors on inverted background
.privacy-dropdown__option.active,
.privacy-dropdown__option:hover,
.privacy-dropdown__option.active .privacy-dropdown__option__content,
.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
.privacy-dropdown__option:hover .privacy-dropdown__option__content,
.privacy-dropdown__option:hover .privacy-dropdown__option__content strong,
.dropdown-menu__item a:active,
.dropdown-menu__item a:focus,
.dropdown-menu__item a:hover,
.actions-modal ul li:not(:empty) a.active,
.actions-modal ul li:not(:empty) a.active button,
.actions-modal ul li:not(:empty) a:active,
.actions-modal ul li:not(:empty) a:active button,
.actions-modal ul li:not(:empty) a:focus,
.actions-modal ul li:not(:empty) a:focus button,
.actions-modal ul li:not(:empty) a:hover,
.actions-modal ul li:not(:empty) a:hover button,
.language-dropdown__dropdown__results__item.active,
.admin-wrapper .sidebar ul .simple-navigation-active-leaf a,
.simple_form .block-button,
.simple_form .button,
.simple_form button {
color: $white;
}
.language-dropdown__dropdown__results__item .language-dropdown__dropdown__results__item__common-name {
color: lighten($ui-base-color, 8%);
}
.language-dropdown__dropdown__results__item.active .language-dropdown__dropdown__results__item__common-name {
color: darken($ui-base-color, 12%);
}
.dropdown-menu__separator,
.dropdown-menu__item.edited-timestamp__history__item,
.dropdown-menu__container__header,
.compare-history-modal .report-modal__target,
.report-dialog-modal .poll__option.dialog-option {
border-bottom-color: lighten($ui-base-color, 4%);
}
.report-dialog-modal__container {
border-top-color: lighten($ui-base-color, 4%);
} }
// Change the background colors of modals // Change the background colors of modals
.actions-modal, .actions-modal,
.boost-modal, .boost-modal,
.favourite-modal,
.confirmation-modal, .confirmation-modal,
.mute-modal, .mute-modal,
.block-modal, .block-modal,
@ -243,12 +378,43 @@
.compare-history-modal, .compare-history-modal,
.report-modal__comment .setting-text__wrapper, .report-modal__comment .setting-text__wrapper,
.report-modal__comment .setting-text, .report-modal__comment .setting-text,
.report-dialog-modal__textarea { .announcements,
background: $white; .picture-in-picture__header,
.picture-in-picture__footer,
.reactions-bar__item {
background: $ui-base-color;
border: 1px solid lighten($ui-base-color, 8%); border: 1px solid lighten($ui-base-color, 8%);
} }
.report-dialog-modal .dialog-option .poll__input { .reactions-bar__item:hover,
.reactions-bar__item:focus,
.reactions-bar__item:active,
.language-dropdown__dropdown__results__item:hover,
.language-dropdown__dropdown__results__item:focus,
.language-dropdown__dropdown__results__item:active {
background-color: $ui-base-color;
}
.reactions-bar__item.active {
background-color: mix($white, $ui-highlight-color, 80%);
border-color: mix(lighten($ui-base-color, 8%), $ui-highlight-color, 80%);
}
.media-modal__overlay .picture-in-picture__footer {
border: 0;
}
.picture-in-picture__header {
border-bottom: 0;
}
.announcements,
.picture-in-picture__footer {
border-top: 0;
}
.icon-with-badge__badge {
border-color: $white;
color: $white; color: $white;
} }
@ -260,8 +426,43 @@
border-top-color: lighten($ui-base-color, 8%); border-top-color: lighten($ui-base-color, 8%);
} }
.column-header__collapsible-inner {
background: darken($ui-base-color, 4%);
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.dashboard__quick-access,
.focal-point__preview strong,
.admin-wrapper .content__heading__tabs a.selected {
color: $white;
}
.button.button-tertiary {
&:hover,
&:focus,
&:active {
color: $white;
}
}
.button.button-secondary {
border-color: $darker-text-color;
color: $darker-text-color;
&:hover,
&:focus,
&:active {
border-color: darken($darker-text-color, 8%);
color: darken($darker-text-color, 8%);
}
}
.flash-message.warning {
color: lighten($gold-star, 16%);
}
.boost-modal__action-bar, .boost-modal__action-bar,
.favourite-modal__action-bar,
.confirmation-modal__action-bar, .confirmation-modal__action-bar,
.mute-modal__action-bar, .mute-modal__action-bar,
.block-modal__action-bar, .block-modal__action-bar,
@ -279,33 +480,134 @@
} }
} }
.display-case__case {
background: $white;
}
.embed-modal .embed-modal__container .embed-modal__html {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
&:focus {
border-color: lighten($ui-base-color, 12%);
background: $white;
}
}
.react-toggle-track {
background: $ui-secondary-color;
}
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
background: darken($ui-secondary-color, 10%);
}
.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
background: lighten($ui-highlight-color, 10%);
}
// Change the default color used for the text in an empty column or on the error column // Change the default color used for the text in an empty column or on the error column
.empty-column-indicator, .empty-column-indicator,
.error-column { .error-column {
color: lighten($ui-base-color, 60%); color: $primary-text-color;
background: $white;
} }
// Change the default colors used on some parts of the profile pages // Change the default colors used on some parts of the profile pages
.activity-stream-tabs { .activity-stream-tabs {
background: $account-background-color; background: $account-background-color;
border-bottom-color: lighten($ui-base-color, 8%);
}
a { .nothing-here,
&.active { .page-header,
color: $ui-primary-color; .directory__tag > a,
} .directory__tag > div {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border-left: 0;
border-right: 0;
border-top: 0;
}
}
.simple_form {
input[type="text"],
input[type="number"],
input[type="email"],
input[type="password"],
textarea {
&:hover {
border-color: lighten($ui-base-color, 12%);
}
}
}
.picture-in-picture-placeholder {
background: $white;
border-color: lighten($ui-base-color, 8%);
color: lighten($ui-base-color, 8%);
}
.directory__tag > a {
&:hover,
&:active,
&:focus {
background: $ui-base-color;
} }
@media screen and (max-width: $no-gap-breakpoint) {
border: 0;
}
}
.directory__tag.active > a,
.directory__tag.active > div {
border-color: $ui-highlight-color;
&,
h4,
h4 small,
.fa,
.trends__item__current {
color: $white;
}
&:hover,
&:active,
&:focus {
background: $ui-highlight-color;
}
}
.batch-table {
&__toolbar,
&__row,
.nothing-here {
border-color: lighten($ui-base-color, 8%);
}
} }
.activity-stream { .activity-stream {
border: 1px solid lighten($ui-base-color, 8%);
&--under-tabs {
border-top: 0;
}
.entry { .entry {
background: $account-background-color; background: $account-background-color;
.detailed-status.light,
.more.light,
.status.light {
border-bottom-color: lighten($ui-base-color, 8%);
}
} }
.status.light { .status.light {
.status__content { .status__content {
color: $primary-text-color; color: $primary-text-color;
} }
@ -315,17 +617,14 @@
color: $primary-text-color; color: $primary-text-color;
} }
} }
} }
} }
.accounts-grid { .accounts-grid {
.account-grid-card { .account-grid-card {
.controls { .controls {
.icon-button { .icon-button {
color: $ui-secondary-color; color: $darker-text-color;
} }
} }
@ -336,13 +635,53 @@
} }
.username { .username {
color: $ui-secondary-color; color: $darker-text-color;
} }
.account__header__content { .account__header__content {
color: $primary-text-color; color: $primary-text-color;
} }
}
}
.simple_form {
.warning {
box-shadow: none;
background: rgba($error-red, 0.5);
text-shadow: none;
}
.recommended {
border-color: $ui-highlight-color;
color: $ui-highlight-color;
background-color: rgba($ui-highlight-color, 0.1);
}
}
.compose-form .compose-form__warning {
border-color: $ui-highlight-color;
background-color: rgba($ui-highlight-color, 0.1);
&,
a {
color: $ui-highlight-color;
}
}
.reply-indicator {
background: transparent;
border: 1px solid lighten($ui-base-color, 8%);
}
.dismissable-banner {
border-left: 1px solid lighten($ui-base-color, 8%);
border-right: 1px solid lighten($ui-base-color, 8%);
}
.status__content,
.reply-indicator__content {
a {
color: $highlight-text-color;
} }
} }
@ -354,6 +693,7 @@
} }
} }
.notification__filter-bar button.active::after,
.account__section-headline a.active::after { .account__section-headline a.active::after {
border-color: transparent transparent $white; border-color: transparent transparent $white;
} }
@ -364,7 +704,10 @@
.activity-stream, .activity-stream,
.nothing-here, .nothing-here,
.directory__tag > a, .directory__tag > a,
.directory__tag > div { .directory__tag > div,
.card > a,
.page-header,
.compose-form .compose-form__warning {
box-shadow: none; box-shadow: none;
} }
@ -372,3 +715,55 @@
border: 1px solid lighten($ui-base-color, 8%); border: 1px solid lighten($ui-base-color, 8%);
background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>") no-repeat right 8px center / auto 16px; background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>") no-repeat right 8px center / auto 16px;
} }
// Glitch-soc-specific changes
.glitch.local-settings {
background: $ui-base-color;
border: 1px solid lighten($ui-base-color, 8%);
}
.glitch.local-settings__navigation {
background: darken($ui-base-color, 8%);
}
.glitch.local-settings__navigation__item {
background: darken($ui-base-color, 8%);
border-bottom: 1px lighten($ui-base-color, 8%) solid;
&:hover {
background: $ui-base-color;
}
&.active {
background: $ui-highlight-color;
color: $white;
}
&.close, &.close:hover {
background: $error-value-color;
color: $primary-text-color;
}
}
.notification__dismiss-overlay {
.wrappy {
box-shadow: unset;
.ckbox {
text-shadow: unset;
}
}
}
.status.collapsed .status__content:after {
background: linear-gradient(rgba(darken($ui-base-color, 13%), 0), rgba(darken($ui-base-color, 13%), 1));
}
.drawer__inner__mastodon {
background: $white url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto !important;
.mastodon {
filter: contrast(75%) brightness(75%) !important;
}
}

View file

@ -7,11 +7,17 @@ $classic-primary-color: #9baec8;
$classic-secondary-color: #d9e1e8; $classic-secondary-color: #d9e1e8;
$classic-highlight-color: #6364ff; $classic-highlight-color: #6364ff;
// Differences
$success-green: lighten(#3c754d, 8%);
$base-overlay-background: $white !default;
$valid-value-color: $success-green !default;
$ui-base-color: $classic-secondary-color !default; $ui-base-color: $classic-secondary-color !default;
$ui-base-lighter-color: darken($ui-base-color, 57%); $ui-base-lighter-color: #b0c0cf;
$ui-highlight-color: $classic-highlight-color !default; $ui-primary-color: #9bcbed;
$ui-primary-color: $classic-primary-color !default;
$ui-secondary-color: $classic-base-color !default; $ui-secondary-color: $classic-base-color !default;
$ui-highlight-color: $classic-highlight-color !default;
$primary-text-color: $black !default; $primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default; $darker-text-color: $classic-base-color !default;
@ -19,17 +25,14 @@ $highlight-text-color: darken($ui-highlight-color, 8%) !default;
$dark-text-color: #444b5d; $dark-text-color: #444b5d;
$action-button-color: #606984; $action-button-color: #606984;
$success-green: lighten(#3c754d, 8%);
$base-overlay-background: $white !default;
$inverted-text-color: $black !default; $inverted-text-color: $black !default;
$lighter-text-color: $classic-base-color !default; $lighter-text-color: $classic-base-color !default;
$light-text-color: #444b5d; $light-text-color: #444b5d;
// Newly added colors
$account-background-color: $white !default; $account-background-color: $white !default;
//Invert darkened and lightened colors // Invert darkened and lightened colors
@function darken($color, $amount) { @function darken($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) + $amount); @return hsl(hue($color), saturation($color), lightness($color) + $amount);
} }

View file

@ -36,15 +36,11 @@ body.rtl {
margin-left: 5px; margin-left: 5px;
} }
.composer .compose--counter-wrapper { .compose-form .character-counter__wrapper {
margin-right: 0; margin-right: 0;
margin-left: 4px; margin-left: 4px;
} }
.composer--publisher {
text-align: left;
}
.boost-modal__status-time, .boost-modal__status-time,
.favourite-modal__status-time { .favourite-modal__status-time {
float: left; float: left;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 639 B

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 B

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 693 B

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 B

After

Width:  |  Height:  |  Size: 80 B

View file

@ -7,7 +7,7 @@ import { tagHistory } from 'mastodon/settings';
import resizeImage from 'mastodon/utils/resize_image'; import resizeImage from 'mastodon/utils/resize_image';
import { showAlert, showAlertForError } from './alerts'; import { showAlert, showAlertForError } from './alerts';
import { useEmoji } from './emojis'; import { useEmoji } from './emojis';
import { importFetchedAccounts } from './importer'; import { importFetchedAccounts, importFetchedStatus } from './importer';
import { openModal } from './modal'; import { openModal } from './modal';
import { updateTimeline } from './timelines'; import { updateTimeline } from './timelines';
@ -194,6 +194,10 @@ export function submitCompose(routerHistory) {
} }
}; };
if (statusId) {
dispatch(importFetchedStatus({ ...response.data }));
}
if (statusId === null && response.data.visibility !== 'direct') { if (statusId === null && response.data.visibility !== 'direct') {
insertIfOnline('home'); insertIfOnline('home');
} }

View file

@ -4,6 +4,7 @@ exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] =
<button <button
className="button button-secondary" className="button button-secondary"
onClick={[Function]} onClick={[Function]}
type="button"
/> />
`; `;
@ -11,6 +12,7 @@ exports[`<Button /> renders a button element 1`] = `
<button <button
className="button" className="button"
onClick={[Function]} onClick={[Function]}
type="button"
/> />
`; `;
@ -19,6 +21,7 @@ exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
className="button" className="button"
disabled={true} disabled={true}
onClick={[Function]} onClick={[Function]}
type="button"
/> />
`; `;
@ -26,6 +29,7 @@ exports[`<Button /> renders class="button--block" if props.block given 1`] = `
<button <button
className="button button--block" className="button button--block"
onClick={[Function]} onClick={[Function]}
type="button"
/> />
`; `;
@ -33,6 +37,7 @@ exports[`<Button /> renders the children 1`] = `
<button <button
className="button" className="button"
onClick={[Function]} onClick={[Function]}
type="button"
> >
<p> <p>
children children
@ -44,6 +49,7 @@ exports[`<Button /> renders the given text 1`] = `
<button <button
className="button" className="button"
onClick={[Function]} onClick={[Function]}
type="button"
> >
foo foo
</button> </button>
@ -53,6 +59,7 @@ exports[`<Button /> renders the props.text instead of children 1`] = `
<button <button
className="button" className="button"
onClick={[Function]} onClick={[Function]}
type="button"
> >
foo foo
</button> </button>

View file

@ -27,6 +27,7 @@ export default @injectIntl
class Account extends ImmutablePureComponent { class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {
size: PropTypes.number,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
@ -40,6 +41,10 @@ class Account extends ImmutablePureComponent {
onActionClick: PropTypes.func, onActionClick: PropTypes.func,
}; };
static defaultProps = {
size: 46,
};
handleFollow = () => { handleFollow = () => {
this.props.onFollow(this.props.account); this.props.onFollow(this.props.account);
} }
@ -65,7 +70,7 @@ class Account extends ImmutablePureComponent {
} }
render () { render () {
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction } = this.props; const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size } = this.props;
if (!account) { if (!account) {
return ( return (
@ -136,7 +141,7 @@ class Account extends ImmutablePureComponent {
<div className='account'> <div className='account'>
<div className='account__wrapper'> <div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}> <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={46} /></div> <div className='account__avatar-wrapper'><Avatar account={account} size={size} /></div>
{mute_expires_at} {mute_expires_at}
<DisplayName account={account} /> <DisplayName account={account} />
</Permalink> </Permalink>

View file

@ -54,7 +54,7 @@ export default class Avatar extends React.PureComponent {
return ( return (
<div className={classNames('account__avatar', { 'account__avatar-inline': inline })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={style}> <div className={classNames('account__avatar', { 'account__avatar-inline': inline })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={style}>
<img src={src} alt={account?.get('acct')} /> {src && <img src={src} alt={account?.get('acct')} />}
</div> </div>
); );
} }

View file

@ -6,6 +6,7 @@ export default class Button extends React.PureComponent {
static propTypes = { static propTypes = {
text: PropTypes.node, text: PropTypes.node,
type: PropTypes.string,
onClick: PropTypes.func, onClick: PropTypes.func,
disabled: PropTypes.bool, disabled: PropTypes.bool,
block: PropTypes.bool, block: PropTypes.bool,
@ -15,8 +16,12 @@ export default class Button extends React.PureComponent {
children: PropTypes.node, children: PropTypes.node,
}; };
static defaultProps = {
type: 'button',
};
handleClick = (e) => { handleClick = (e) => {
if (!this.props.disabled) { if (!this.props.disabled && this.props.onClick) {
this.props.onClick(e); this.props.onClick(e);
} }
} }
@ -42,6 +47,7 @@ export default class Button extends React.PureComponent {
onClick={this.handleClick} onClick={this.handleClick}
ref={this.setRef} ref={this.setRef}
title={this.props.title} title={this.props.title}
type={this.props.type}
> >
{this.props.text || this.props.children} {this.props.text || this.props.children}
</button> </button>

View file

@ -141,6 +141,7 @@ export default class IconButton extends React.PureComponent {
return ( return (
<button <button
type='button'
aria-label={title} aria-label={title}
aria-pressed={pressed} aria-pressed={pressed}
aria-expanded={expanded} aria-expanded={expanded}

View file

@ -18,7 +18,7 @@ export default class LoadMore extends React.PureComponent {
const { disabled, visible } = this.props; const { disabled, visible } = this.props;
return ( return (
<button className='load-more' disabled={disabled || !visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}> <button type='button' className='load-more' disabled={disabled || !visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' /> <FormattedMessage id='status.load_more' defaultMessage='Load more' />
</button> </button>
); );

View file

@ -21,7 +21,12 @@ class NavigationPortal extends React.PureComponent {
render () { render () {
return ( return (
<Switch> <Switch>
<Route path='/@:acct/(tagged/:tagged?)?' component={AccountNavigation} /> <Route path='/@:acct' exact component={AccountNavigation} />
<Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
<Route path='/@:acct/followers' exact component={AccountNavigation} />
<Route path='/@:acct/following' exact component={AccountNavigation} />
<Route path='/@:acct/media' exact component={AccountNavigation} />
<Route component={DefaultNavigation} /> <Route component={DefaultNavigation} />
</Switch> </Switch>
); );

View file

@ -61,7 +61,7 @@ class ServerBanner extends React.PureComponent {
<div className='server-banner__meta__column'> <div className='server-banner__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'])} /> <Account id={server.getIn(['contact', 'account', 'id'])} size={36} />
</div> </div>
<div className='server-banner__meta__column'> <div className='server-banner__meta__column'>

View file

@ -1,34 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
export default class SettingText extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
settingKey: PropTypes.array.isRequired,
label: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};
handleChange = (e) => {
this.props.onChange(this.props.settingKey, e.target.value);
}
render () {
const { settings, settingKey, label } = this.props;
return (
<label>
<span style={{ display: 'none' }}>{label}</span>
<input
className='setting-text'
value={settings.getIn(settingKey)}
onChange={this.handleChange}
placeholder={label}
/>
</label>
);
}
}

View file

@ -28,6 +28,7 @@ store.dispatch(fetchCustomEmojis());
const createIdentityContext = state => ({ const createIdentityContext = state => ({
signedIn: !!state.meta.me, signedIn: !!state.meta.me,
accountId: state.meta.me, accountId: state.meta.me,
disabledAccountId: state.meta.disabled_account_id,
accessToken: state.meta.access_token, accessToken: state.meta.access_token,
permissions: state.role ? state.role.permissions : 0, permissions: state.role ? state.role.permissions : 0,
}); });
@ -42,6 +43,7 @@ export default class Mastodon extends React.PureComponent {
identity: PropTypes.shape({ identity: PropTypes.shape({
signedIn: PropTypes.bool.isRequired, signedIn: PropTypes.bool.isRequired,
accountId: PropTypes.string, accountId: PropTypes.string,
disabledAccountId: PropTypes.string,
accessToken: PropTypes.string, accessToken: PropTypes.string,
}).isRequired, }).isRequired,
}; };

View file

@ -125,7 +125,7 @@ class About extends React.PureComponent {
<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'])} /> <Account id={server.getIn(['contact', 'account', 'id'])} size={36} />
</div> </div>
<hr className='about__meta__divider' /> <hr className='about__meta__divider' />
@ -209,6 +209,10 @@ class About extends React.PureComponent {
</Section> </Section>
<LinkFooter /> <LinkFooter />
<div className='about__footer'>
<p><FormattedMessage id='about.disclaimer' defaultMessage='Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.' /></p>
</div>
</div> </div>
<Helmet> <Helmet>

View file

@ -337,10 +337,10 @@ class Header extends ImmutablePureComponent {
</dl> </dl>
{fields.map((pair, i) => ( {fields.map((pair, i) => (
<dl key={i}> <dl key={i} className={classNames({ verified: pair.get('verified_at') })}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' /> <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
<dd className={`${pair.get('verified_at') ? 'verified' : ''} translate`} title={pair.get('value_plain')}> <dd className='translate' title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd> </dd>
</dl> </dl>

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