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
develop
Jeremy Kescher 1 year ago
commit 03f50f2f00
No known key found for this signature in database
GPG Key ID: 48DFE4BB15BA5940

@ -2,7 +2,4 @@ blank_issues_enabled: false
contact_links:
- name: GitHub Discussions
url: https://github.com/mastodon/mastodon/discussions
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.
about: Please ask and answer questions here.

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

@ -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 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 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))
- Previously, you could only see trends in your current language
- 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 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 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 `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 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
@ -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 `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 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
- **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 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
- 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
@ -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
- 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 public timelines to be filtered by current locale by default ([Gargron](https://github.com/mastodon/mastodon/pull/19291))
- Change settings area to be separated into categories in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19407))
- 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), [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 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 "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 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 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
@ -98,6 +107,25 @@ Some of the features in this release have been funded through the [NGI0 Discover
### 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 `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))

@ -1,6 +1,6 @@
# 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.
@ -16,5 +16,3 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| 3.4.x | Yes |
| 3.3.x | No |
| < 3.3 | No |
[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail

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

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

@ -3,7 +3,7 @@
class Api::V2::MediaController < Api::V1::MediaController
def create
@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
render json: file_type_error, status: 422
rescue Paperclip::Error

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

@ -4,15 +4,19 @@ module Admin::ActionLogsHelper
def log_target(log)
case log.target_type
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'
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'
link_to log.human_identifier, admin_roles_path(log.target_id)
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'
link_to log.human_identifier, "https://#{log.human_identifier}"
link_to log.human_identifier, "https://#{log.human_identifier.presence}"
when 'Status'
link_to log.human_identifier, log.permalink
when 'AccountWarning'
@ -22,9 +26,13 @@ module Admin::ActionLogsHelper
when 'IpBlock', 'Instance', 'CustomEmoji'
log.human_identifier
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'
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

@ -204,7 +204,7 @@ module ApplicationHelper
permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present?
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[:push_subscription] = current_account.user.web_push_subscription(current_session)
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@/, ''))
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?
state_params[:owner] = Account.local.without_suspended.where('id > 0').first
end

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

@ -27,6 +27,7 @@ export default @injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
size: PropTypes.number,
account: ImmutablePropTypes.map,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
@ -41,6 +42,10 @@ class Account extends ImmutablePureComponent {
onActionClick: PropTypes.func,
};
static defaultProps = {
size: 36,
};
handleFollow = () => {
this.props.onFollow(this.props.account);
}
@ -75,6 +80,7 @@ class Account extends ImmutablePureComponent {
actionIcon,
actionTitle,
defaultAction,
size,
} = this.props;
if (!account) {
@ -163,7 +169,7 @@ class Account extends ImmutablePureComponent {
<div className='account'>
<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')}`}>
<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}
<DisplayName account={account} />
</Permalink>

@ -21,7 +21,12 @@ class NavigationPortal extends React.PureComponent {
render () {
return (
<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} />
</Switch>
);

@ -61,7 +61,7 @@ class ServerBanner extends React.PureComponent {
<div className='server-banner__meta__column'>
<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 className='server-banner__meta__column'>

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

@ -125,7 +125,7 @@ class About extends React.PureComponent {
<div className='about__meta__column'>
<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>
<hr className='about__meta__divider' />
@ -209,6 +209,11 @@ class About extends React.PureComponent {
</Section>
<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>
<Helmet>

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { revealAccount } from 'flavours/glitch/actions/accounts';
import { FormattedMessage } from 'react-intl';
import Button from 'flavours/glitch/components/button';
import { domain } from 'flavours/glitch/initial_state';
const mapDispatchToProps = (dispatch, { accountId }) => ({
@ -26,7 +27,7 @@ class LimitedAccountHint extends React.PureComponent {
return (
<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>
</div>
);

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

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

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

@ -156,7 +156,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
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);
@ -165,7 +165,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
<React.Fragment>
{icon && <Icon className='icon' fixedWidth id={icon} />}
<div className='content'>
<div className='privacy-dropdown__option__content'>
<strong>{text}</strong>
{meta}
</div>
@ -218,7 +218,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
// size will be used to determine the coordinate of the menu by
// react-overlays
<div
className='composer--options--dropdown--content'
className='privacy-dropdown__dropdown'
ref={this.handleRef}
role='listbox'
style={{

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

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

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

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

@ -18,7 +18,7 @@ export default class Upload extends ImmutablePureComponent {
media: ImmutablePropTypes.map.isRequired,
onUndo: PropTypes.func.isRequired,
onOpenFocalPoint: PropTypes.func.isRequired,
isEditingStatus: PropTypes.func.isRequired,
isEditingStatus: PropTypes.bool.isRequired,
};
handleUndoClick = e => {
@ -39,17 +39,17 @@ export default class Upload extends ImmutablePureComponent {
const y = ((focusY / -2) + .5) * 100;
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, }) }}>
{({ scale }) => (
<div style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
<div className='composer--upload_form--actions'>
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
<div className='compose-form__upload__actions'>
<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>)}
</div>
{(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>
</div>
)}

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

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

@ -15,7 +15,7 @@ export default class Warning extends React.PureComponent {
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 }) }}>
{({ 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}
</div>
)}

@ -25,6 +25,7 @@ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
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' },
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
@ -45,10 +46,7 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow(account) {
if (
account.getIn(['relationship', 'following']) ||
account.getIn(['relationship', 'requested'])
) {
if (account.getIn(['relationship', 'following'])) {
if (unfollowModal) {
dispatch(
openModal('CONFIRM', {
@ -66,6 +64,16 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} else {
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 {
dispatch(followAccount(account.get('id')));
}

File diff suppressed because one or more lines are too long

@ -64,7 +64,7 @@ const mapStateToProps = state => ({
showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
notifications: getNotifications(state),
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,
hasMore: state.getIn(['notifications', 'hasMore']),
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,

@ -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>
);
}
};

@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
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 { openModal } from 'flavours/glitch/actions/modal';
import { PERMISSION_INVITE_USERS } from 'flavours/glitch/permissions';
@ -48,45 +48,42 @@ class LinkFooter extends React.PureComponent {
render () {
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>);
items.push(<Link key='about' to='/about'><FormattedMessage id='navigation_bar.info' defaultMessage='About' /></Link>);
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>);
}
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
const canProfileDirectory = profileDirectory;
return (
<div className='getting-started__footer'>
<ul>
{items.map((item, index, array) => (
<li>{item} { index === array.length - 1 ? null : ' · ' }</li>
))}
</ul>
<div className='link-footer'>
<p>
<strong>{domain}</strong>:
{' '}
<Link key='about' to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
{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>
<FormattedMessage
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}.'
values={{
github: <span><a href={source_url} rel='noopener noreferrer' target='_blank'>{repository}</a> (v{version})</span>,
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> }}
/>
<strong>Catstodon</strong>:
{' '}
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a>
{' · '}
<Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link>
{' · '}
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
{' · '}
v{version}
</p>
</div>
);

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

@ -54,6 +54,7 @@
* @property {boolean} crop_images
* @property {boolean=} delete_modal
* @property {boolean=} disable_swiping
* @property {string=} disabled_account_id
* @property {boolean} display_media
* @property {string} domain
* @property {boolean=} expand_spoilers
@ -61,6 +62,7 @@
* @property {string} locale
* @property {string | null} mascot
* @property {string=} me
* @property {string=} moved_to_account_id
* @property {string=} owner
* @property {boolean} profile_directory
* @property {boolean} registrations_open
@ -111,6 +113,7 @@ export const boostModal = getMeta('boost_modal');
export const cropImages = getMeta('crop_images');
export const deleteModal = getMeta('delete_modal');
export const disableSwiping = getMeta('disable_swiping');
export const disabledAccountId = getMeta('disabled_account_id');
export const displayMedia = getMeta('display_media');
export const domain = getMeta('domain');
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 mascot = getMeta('mascot');
export const me = getMeta('me');
export const movedToAccountId = getMeta('moved_to_account_id');
export const owner = getMeta('owner');
export const profile_directory = getMeta('profile_directory');
export const reduceMotion = getMeta('reduce_motion');

@ -52,20 +52,26 @@ const initialState = ImmutableMap({
markNewForDelete: false,
});
const notificationToMap = (state, notification) => ImmutableMap({
const notificationToMap = (notification, markForDelete) => ImmutableMap({
id: notification.id,
type: notification.type,
account: notification.account.id,
markedForDelete: state.get('markNewForDelete'),
markedForDelete: markForDelete,
status: notification.status ? notification.status.id : null,
report: notification.report ? fromJS(notification.report) : null,
});
const normalizeNotification = (state, notification, usePendingItems) => {
const markNewForDelete = state.get('markNewForDelete');
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()) {
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)) {
@ -79,32 +85,79 @@ const normalizeNotification = (state, notification, usePendingItems) => {
list = list.take(20);
}
return list.unshift(notificationToMap(state, notification));
return list.unshift(notificationToMap(notification, markNewForDelete));
});
};
const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => {
const lastReadId = state.get('lastReadId');
let items = ImmutableList();
const expandNormalizedNotifications = (state, notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) => {
// This method is pretty tricky because:
// - 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) => {
items = items.set(i, notificationToMap(state, n));
});
const markNewForDelete = state.get('markNewForDelete');
const lastReadId = state.get('lastReadId');
const newItems = ImmutableList(notifications.map((notification) => notificationToMap(notification, markNewForDelete)));
return state.withMutations(mutable => {
if (!items.isEmpty()) {
if (!newItems.isEmpty()) {
usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty());
mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
const lastIndex = 1 + list.findLastIndex(
item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')),
);
const firstIndex = 1 + list.take(lastIndex).findLastIndex(
item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0,
mutable.update(usePendingItems ? 'pendingItems' : 'items', oldItems => {
// If called to poll *new* notifications, we just need to add them on top without duplicates
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);
}
// If called to expand more (presumably older than any known to the WebUI), we just have to
// 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);
}
// 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),
);
return list.take(firstIndex).concat(items, list.skip(lastIndex));
});
}
@ -115,7 +168,7 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
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));
} else {
const mostRecent = items.find(item => item !== null);
const mostRecent = newItems.find(item => item !== null);
if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) {
mutable.set('lastReadId', mostRecent.get('id'));
}
@ -264,7 +317,7 @@ export default function notifications(state = initialState, action) {
case NOTIFICATIONS_UPDATE:
return normalizeNotification(state, action.notification, action.usePendingItems);
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:
return filterNotifications(state, [action.relationship.id]);
case ACCOUNT_MUTE_SUCCESS:

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

@ -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 {
padding: 20px;
@ -37,6 +65,14 @@
border-radius: 4px;
}
&__footer {
color: $dark-text-color;
text-align: center;
font-size: 15px;
line-height: 22px;
margin-top: 20px;
}
&__header {
margin-bottom: 30px;
@ -157,7 +193,7 @@
}
}
.getting-started__footer {
.link-footer {
padding: 0;
margin-top: 60px;
text-align: center;

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

@ -1044,43 +1044,6 @@
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 {
flex: 0 1 auto;
opacity: 1;
@ -1775,7 +1738,7 @@ noscript {
@import 'domains';
@import 'status';
@import 'modal';
@import 'composer';
@import 'compose_form';
@import 'columns';
@import 'regeneration_indicator';
@import 'directory';

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

@ -4,6 +4,20 @@
p {
color: $darker-text-color;
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 {

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

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

@ -1,69 +1,78 @@
// components.scss
.compose-form {
.compose-form__modifiers {
.compose-form__upload {
&-description {
input {
&::placeholder {
opacity: 1;
}
}
}
}
}
}
.status__content a,
.reply-indicator__content a {
color: lighten($ui-highlight-color, 12%);
.link-footer a,
.reply-indicator__content a,
.status__content__read-more-button {
text-decoration: underline;
&.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;
&.mention {
text-decoration: none;
}
}
.status__content__read-more-button {
text-decoration: underline;
span {
text-decoration: underline;
}
&:hover,
&:focus,
&:active {
text-decoration: none;
&:hover,
&:focus,
&:active {
span {
text-decoration: none;
}
}
}
}
.getting-started__footer a {
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
.status__content a {
color: $highlight-text-color;
}
.nothing-here {
color: $darker-text-color;
}
.public-layout .public-account-header__tabs__tabs .counter.active::after {
border-bottom: 4px solid $ui-highlight-color;
.compose-form__poll-wrapper .button.button-secondary,
.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 {
.composer--spoiler input,
.compose-form__autosuggest-wrapper textarea {
&::placeholder {
color: $inverted-text-color;
}
.text-icon-button.active {
color: $ui-highlight-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;
}
}

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

@ -1,237 +1,372 @@
// Notes!
// Sass color functions, "darken" and "lighten" are automatically replaced.
.glitch.local-settings {
background: $ui-base-color;
html {
scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
}
&__navigation {
background: darken($ui-base-color, 8%);
// Change the colors of button texts
.button {
color: $white;
&.button-alternative-2 {
color: $white;
}
}
&__navigation__item {
background: darken($ui-base-color, 8%);
.status-card__actions button,
.status-card__actions a {
color: rgba($white, 0.8);
&:hover {
background: $ui-base-color;
}
&:hover,
&:active,
&:focus {
color: $white;
}
}
.notification__dismiss-overlay {
.wrappy {
box-shadow: unset;
}
// 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;
}
.ckbox {
text-shadow: unset;
}
.column > .scrollable.about {
border-top: 1px solid lighten($ui-base-color, 8%);
}
.status.status-direct {
background: darken($ui-base-color, 8%);
border-bottom-color: darken($ui-base-color, 12%);
.about__meta,
.about__section__title,
.interaction-modal {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
}
&.collapsed> .status__content:after {
background: linear-gradient(rgba(darken($ui-base-color, 8%), 0), rgba(darken($ui-base-color, 8%), 1));
}
.rules-list li::before {
background: $ui-highlight-color;
}
.focusable:focus.status.status-direct {
background: darken($ui-base-color, 4%);
.directory__card__img {
background: lighten($ui-base-color, 12%);
}
&.collapsed> .status__content:after {
background: linear-gradient(rgba(darken($ui-base-color, 4%), 0), rgba(darken($ui-base-color, 4%), 1));
.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;
}
&--slim-button {
top: -50px;
right: 0;
}
}
.column-header__back-button,
.column-header__button,
.column-header__button.active,
.account__header__bar {
background: $white;
}
// Change columns' default background colors
.column {
> .scrollable {
background: darken($ui-base-color, 13%);
.column-header__button.active {
color: $ui-highlight-color;
&:hover,
&:active,
&:focus {
color: $ui-highlight-color;
background: $white;
}
}
.status.collapsed .status__content:after {
background: linear-gradient(rgba(darken($ui-base-color, 13%), 0), rgba(darken($ui-base-color, 13%), 1));
.account__header__bar .avatar .account__avatar {
border-color: $white;
}
.drawer__inner {
background: $ui-base-color;
.getting-started__footer a {
color: $ui-secondary-color;
text-decoration: underline;
}
.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;
.confirmation-modal__secondary-button,
.confirmation-modal__cancel-button,
.mute-modal__cancel-button,
.block-modal__cancel-button {
color: lighten($ui-base-color, 26%);
.mastodon {
filter: contrast(75%) brightness(75%) !important;
&:hover,
&:focus,
&:active {
color: $primary-text-color;
}
}
// Change the default appearance of the content warning button
.status__content {
.status__content__spoiler-link {
.column-subheading {
background: darken($ui-base-color, 4%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
background: lighten($ui-base-color, 30%);
.getting-started,
.scrollable {
.column-link {
background: $white;
border-bottom: 1px solid lighten($ui-base-color, 8%);
&:hover {
background: lighten($ui-base-color, 35%);
text-decoration: none;
&:hover,
&:active,
&:focus {
background: $ui-base-color;
}
}
}
.getting-started .navigation-bar {
border-top: 1px solid lighten($ui-base-color, 8%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
}
}
.compose-form__autosuggest-wrapper,
.poll__option input[type="text"],
.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%);
}
// Change the background colors of media and video spoilers
.media-spoiler,
.video-player__spoiler,
.account-gallery__item a {
background: $ui-base-color;
.report-dialog-modal .dialog-option .poll__input {
color: $white;
}
// Change the colors used in the dropdown menu
.dropdown-menu {
background: $ui-base-color;
.search__input {
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
border-bottom: 0;
}
}
.dropdown-menu__arrow {
.list-editor .search .search__input {
border-top: 0;
border-bottom: 0;
}
&.left {
border-left-color: $ui-base-color;
}
.compose-form__poll-wrapper select {
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;
}
&.top {
border-top-color: $ui-base-color;
}
.compose-form__poll-wrapper,
.compose-form__poll-wrapper .poll__footer {
border-top-color: lighten($ui-base-color, 8%);
}
&.bottom {
border-bottom-color: $ui-base-color;
}
.notification__filter-bar {
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
&.right {
border-right-color: $ui-base-color;
}
.compose-form .compose-form__buttons-wrapper {
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%);
}
.dropdown-menu__item {
a,
button {
background: $ui-base-color;
color: $ui-secondary-color;
}
.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;
}
// Change the default color of several parts of the compose form
.composer {
// Change the colors used in compose-form
.compose-form {
.compose-form__modifiers {
.compose-form__upload__actions .icon-button {
color: lighten($white, 7%);
.composer--spoiler input, .compose-form__autosuggest-wrapper textarea {
color: lighten($ui-base-color, 80%);
&:active,
&:focus,
&:hover {
color: $white;
}
}
&:disabled { background: lighten($simple-background-color, 10%) }
.compose-form__upload-description input {
color: lighten($white, 7%);
&::placeholder {
color: lighten($ui-base-color, 70%);
&::placeholder {
color: lighten($white, 7%);
}
}
}
.composer--options-wrapper {
background: lighten($ui-base-color, 10%);
.compose-form__buttons-wrapper {
background: darken($ui-base-color, 6%);
}
.composer--options > hr {
display: none;
.autosuggest-textarea__suggestions {
background: darken($ui-base-color, 6%);
}
.composer--options--dropdown--content--item {
color: $ui-primary-color;
strong {
color: $ui-primary-color;
.autosuggest-textarea__suggestions__item {
&:hover,
&:focus,
&:active,
&.selected {
background: lighten($ui-base-color, 4%);
}
}
}
.emoji-mart-bar {
border-color: lighten($ui-base-color, 4%);
&:first-child {
background: darken($ui-base-color, 6%);
}
}
.emoji-mart-search input {
background: rgba($ui-base-color, 0.3);
border-color: $ui-base-color;
}
// Change the background colors of statuses
.focusable:focus {
background: $ui-base-color;
}
.composer--upload_form--actions .icon-button {
color: lighten($white, 7%);
.detailed-status,
.detailed-status__action-bar {
background: $white;
}
// 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;
&:active,
&:focus,
&:hover {
color: $white;
background: lighten($ui-base-color, 4%);
}
}
.language-dropdown__dropdown__results__item:hover,
.language-dropdown__dropdown__results__item:focus,
.language-dropdown__dropdown__results__item:active {
background-color: $ui-base-color;
// Change the background colors of media and video spoilers
.media-spoiler,
.video-player__spoiler {
background: $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%);
.privacy-dropdown.active .privacy-dropdown__value.active .icon-button {
color: $white;
}
.report-dialog-modal__container {
border-bottom-color: lighten($ui-base-color, 12%);
.account-gallery__item a {
background-color: $ui-base-color;
}
.status__content,
.reply-indicator__content {
a {
color: $highlight-text-color;
}
}
// Change the colors used in the dropdown menu
.dropdown-menu {
background: $white;
.emoji-mart-bar {
border-color: darken($ui-base-color, 4%);
&__arrow {
&.left {
border-left-color: $white;
}
&:first-child {
background: lighten($ui-base-color, 10%);
&.top {
border-top-color: $white;
}
&.bottom {
border-bottom-color: $white;
}
&.right {
border-right-color: $white;
}
}
}
.emoji-mart-search input {
background: rgba($ui-base-color, 0.3);
border-color: $ui-base-color;
&__item {
a,
button {
background: $white;
color: $darker-text-color;
}
}
}
.autosuggest-textarea__suggestions {
background: lighten($ui-base-color, 10%)
// 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;
}
.autosuggest-textarea__suggestions__item {
&:hover,
&:focus,
&:active,
&.selected {
background: darken($ui-base-color, 4%);
}
.language-dropdown__dropdown__results__item .language-dropdown__dropdown__results__item__common-name {
color: lighten($ui-base-color, 8%);
}
.react-toggle-track {
background: $ui-secondary-color;
.language-dropdown__dropdown__results__item.active .language-dropdown__dropdown__results__item__common-name {
color: darken($ui-base-color, 12%);
}
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
background: lighten($ui-secondary-color, 10%);
.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%);
}
.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
background: darken($ui-highlight-color, 10%);
.report-dialog-modal__container {
border-top-color: lighten($ui-base-color, 4%);
}
// Change the background colors of modals
.actions-modal,
.boost-modal,
.favourite-modal,
.confirmation-modal,
.mute-modal,
.block-modal,
@ -243,12 +378,43 @@
.compare-history-modal,
.report-modal__comment .setting-text__wrapper,
.report-modal__comment .setting-text,
.report-dialog-modal__textarea {
background: $white;
.announcements,
.picture-in-picture__header,
.picture-in-picture__footer,
.reactions-bar__item {
background: $ui-base-color;
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;
}
@ -260,8 +426,43 @@
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,
.favourite-modal__action-bar,
.confirmation-modal__action-bar,
.mute-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
.empty-column-indicator,
.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
.activity-stream-tabs {
background: $account-background-color;
border-bottom-color: lighten($ui-base-color, 8%);
}
a {
&.active {
color: $ui-primary-color;
}
.nothing-here,
.page-header,
.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 {
border: 1px solid lighten($ui-base-color, 8%);
&--under-tabs {
border-top: 0;
}
.entry {
background: $account-background-color;
.detailed-status.light,
.more.light,
.status.light {
border-bottom-color: lighten($ui-base-color, 8%);
}
}
.status.light {
.status__content {
color: $primary-text-color;
}
@ -315,17 +617,14 @@
color: $primary-text-color;
}
}
}
}
.accounts-grid {
.account-grid-card {
.controls {
.icon-button {
color: $ui-secondary-color;
color: $darker-text-color;
}
}
@ -336,13 +635,53 @@
}
.username {
color: $ui-secondary-color;
color: $darker-text-color;
}
.account__header__content {
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 {
border-color: transparent transparent $white;
}
@ -364,7 +704,10 @@
.activity-stream,
.nothing-here,
.directory__tag > a,
.directory__tag > div {
.directory__tag > div,
.card > a,
.page-header,
.compose-form .compose-form__warning {
box-shadow: none;
}
@ -372,3 +715,55 @@
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;
}
// 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;
}
}

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

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

@ -7,7 +7,7 @@ import { tagHistory } from 'mastodon/settings';
import resizeImage from 'mastodon/utils/resize_image';
import { showAlert, showAlertForError } from './alerts';
import { useEmoji } from './emojis';
import { importFetchedAccounts } from './importer';
import { importFetchedAccounts, importFetchedStatus } from './importer';
import { openModal } from './modal';
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') {
insertIfOnline('home');
}

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

@ -27,6 +27,7 @@ export default @injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
size: PropTypes.number,
account: ImmutablePropTypes.map,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
@ -40,6 +41,10 @@ class Account extends ImmutablePureComponent {
onActionClick: PropTypes.func,
};
static defaultProps = {
size: 46,
};
handleFollow = () => {
this.props.onFollow(this.props.account);
}
@ -65,7 +70,7 @@ class Account extends ImmutablePureComponent {
}
render () {
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction } = this.props;
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size } = this.props;
if (!account) {
return (
@ -136,7 +141,7 @@ class Account extends ImmutablePureComponent {
<div className='account'>
<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')}`}>
<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}
<DisplayName account={account} />
</Permalink>

@ -54,7 +54,7 @@ export default class Avatar extends React.PureComponent {
return (
<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>
);
}

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

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

@ -18,7 +18,7 @@ export default class LoadMore extends React.PureComponent {
const { disabled, visible } = this.props;
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' />
</button>
);

@ -21,7 +21,12 @@ class NavigationPortal extends React.PureComponent {
render () {
return (
<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} />
</Switch>
);

@ -61,7 +61,7 @@ class ServerBanner extends React.PureComponent {
<div className='server-banner__meta__column'>
<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 className='server-banner__meta__column'>

@ -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>
);
}
}

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

@ -125,7 +125,7 @@ class About extends React.PureComponent {
<div className='about__meta__column'>
<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>
<hr className='about__meta__divider' />
@ -209,6 +209,10 @@ class About extends React.PureComponent {
</Section>
<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>
<Helmet>

@ -337,10 +337,10 @@ class Header extends ImmutablePureComponent {
</dl>
{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' />
<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') }} />
</dd>
</dl>

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

Loading…
Cancel
Save