Compare commits

..

1 Commits
main ... th-new

Author SHA1 Message Date
kouhai dev 294b87a9a3 th: wip
ci/woodpecker/push/woodpecker Pipeline was successful Details
2024-02-01 10:24:43 -08:00
117 changed files with 12746 additions and 13004 deletions

View File

@ -19,7 +19,6 @@
.github
.gitignore
.woodpecker.yml
/*.md
build
chart
coverage

View File

@ -1,13 +1,9 @@
LOCAL_DOMAIN=localhost
ALTERNATE_DOMAINS=mastodon.internal
STREAMING_API_BASE_URL=https://streaming.mastodon.internal
DB_HOST=$PWD/data/postgres
DB_USER=mastodon
DB_NAME=mastodon_dev
REDIS_URL=unix://$PWD/data/redis/redis-dev.sock
TH_MENTION_SPAM_HEURISTIC_AUTO_LIMIT_ACTIVE=can-spam
TH_MENTION_SPAM_THRESHOLD=2
TH_STAFF_ACCOUNT=staff
TH_USE_INVITE_QUOTA=1

View File

@ -252,11 +252,6 @@ SMTP_FROM_ADDRESS=notifications@example.com
# Maximum allowed character count
MAX_TOOT_CHARS=500
# Maximum allowed hashtags to follow in a feed column
# Note that setting this value higher may cause significant
# database load
MAX_FEED_HASHTAGS=4
# Maximum number of pinned posts
MAX_PINNED_TOOTS=5

View File

@ -36,7 +36,7 @@ jobs:
tags: |
type=raw,value=edge
type=raw,value=nightly
type=raw,value=${{ needs.compute-suffix.outputs.prerelease }}
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit
build-image-streaming:
@ -57,5 +57,5 @@ jobs:
tags: |
type=raw,value=edge
type=raw,value=nightly
type=raw,value=${{ needs.compute-suffix.outputs.prerelease }}
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit

View File

@ -1,7 +1,6 @@
variables:
environment: &docker-environment
SERVER_IMAGE: gitea.treehouse.systems/treehouse/mastodon
STREAMING_IMAGE: gitea.treehouse.systems/treehouse/mastodon-streaming
NAME: gitea.treehouse.systems/treehouse/mastodon
DATE_COMMAND: export COMMIT_DATE=$(date -u -Idate -d @$(git show -s --format=%ct))
docker-step: &docker-step
image: docker:rc-git
@ -18,18 +17,41 @@ clone:
depth: 10
pipeline:
# build-base:
# <<: *docker-step
# commands:
# - docker version
# - docker image build -f Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . --target build-base -t $NAME:build-base
# build:
# <<: *docker-step
# commands:
# - docker version
# - docker image build -f Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . --target build -t $NAME:build
# output-base:
# <<: *docker-step
# commands:
# - docker version
# - docker image build -f Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . --target output-base -t $NAME:build
# the world is not yet ready for this step
# test:
# <<: *docker-step
# commands:
# - docker run --rm -e RAILS_ENV=test -e NODE_ENV=development $NAME:build-base sh -c 'bundle config set --local without development && bundle install && rake spec'
output:
<<: *docker-step
commands:
- eval $DATE_COMMAND
- export TAG=$${COMMIT_DATE}.$CI_COMMIT_SHA && echo $${TAG}
- docker image build -f Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . -t $SERVER_IMAGE:$${TAG}
- docker image build -f streaming/Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . -t $STREAMING_IMAGE:$${TAG}
- docker tag $SERVER_IMAGE:$${TAG} $SERVER_IMAGE:latest
- docker tag $STREAMING_IMAGE:$${TAG} $STREAMING_IMAGE:latest
- echo -n > tags.txt
- echo $${TAG} | tee -a tags.txt
- echo latest | tee -a tags.txt
- docker image build -f Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . -t $NAME:latest
- docker tag $NAME:latest $NAME:$TAG
# idk what's actually persisted between steps
# /shrug this works, so,???
- echo $${TAG} > tags.txt
- echo latest >> tags.txt
# maybe we can use tags someday,,,
# tag-tag:
@ -37,7 +59,7 @@ pipeline:
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock
# commands:
# - docker tag $SERVER_IMAGE:latest $SERVER_IMAGE:$CI_COMMIT_TAG
# - docker tag $NAME:latest $NAME:$CI_COMMIT_TAG
# when:
# event: tag
@ -45,10 +67,8 @@ pipeline:
<<: *docker-step
commands:
- echo $REGISTRY_SECRET | docker login -u $REGISTRY_USER --password-stdin gitea.treehouse.systems
- cat tags.txt | xargs -n 1 -I% echo docker image push $SERVER_IMAGE:%
- cat tags.txt | xargs -n 1 -I% docker image push $SERVER_IMAGE:%
- cat tags.txt | xargs -n 1 -I% echo docker image push $STREAMING_IMAGE:%
- cat tags.txt | xargs -n 1 -I% docker image push $STREAMING_IMAGE:%
- cat tags.txt | xargs -n 1 -I% echo docker image push $NAME:%
- cat tags.txt | xargs -n 1 -I% docker image push $NAME:%
when:
event: [push, tag]
branch: main

View File

@ -1,5 +1,3 @@
compressionLevel: mixed
enableGlobalCache: true
logFilters:

View File

@ -2,101 +2,6 @@
All notable changes to this project will be documented in this file.
## [4.2.7] - 2024-02-16
### Fixed
- Fix OmniAuth tests and edge cases in error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29207))
- Fix new installs by upgrading to the latest release of the `nsa` gem, instead of a no longer existing commit ([mjankowski](https://github.com/mastodon/mastodon/pull/29065))
### Security
- Fix insufficient checking of remote posts ([GHSA-jhrq-qvrm-qr36](https://github.com/mastodon/mastodon/security/advisories/GHSA-jhrq-qvrm-qr36))
## [4.2.6] - 2024-02-14
### Security
- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38))
In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution.
If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`.
If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`.
- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j))
- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187))
- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x))
In some rare cases, the streaming server was not notified of access tokens revocation on application deletion.
- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3))
Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address.
This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another.
However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider.
For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable.
In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account.
## [4.2.5] - 2024-02-01
### Security
- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw))
## [4.2.4] - 2024-01-24
### Fixed
- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823))
- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816))
- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788))
- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748))
- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476))
- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665))
- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558))
- Fix error when processing link preview with an array as `inLanguage` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28252))
- Fix unsupported time zone or locale preventing sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/28035))
- Fix "Hide these posts from home" list setting not refreshing when switching lists ([brianholley](https://github.com/mastodon/mastodon/pull/27763))
- Fix missing background behind dismissable banner in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/27479))
- Fix line wrapping of language selection button with long locale codes ([gunchleoc](https://github.com/mastodon/mastodon/pull/27100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27127))
- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482))
- Fix N+1s because of association preloaders not actually getting called ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28339))
- Fix empty column explainer getting cropped under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28337))
- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268))
- Fix call to inefficient `delete_matched` cache method in domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28367))
### Security
- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801))
## [4.2.3] - 2023-12-05
### Fixed
- Fix dependency on `json-canonicalization` version that has been made unavailable since last release
## [4.2.2] - 2023-12-04
### Changed
- Change dismissed banners to be stored server-side ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27055))
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
- Change single-column navigation notice to be displayed outside of the logo container ([renchap](https://github.com/mastodon/mastodon/pull/27462), [renchap](https://github.com/mastodon/mastodon/pull/27476))
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
- Change post language code to include country code when relevant ([gunchleoc](https://github.com/mastodon/mastodon/pull/27099), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27207))
### Fixed
- Fix upper border radius of onboarding columns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27890))
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
- Fix some posts from threads received out-of-order sometimes not being inserted into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27653))
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
- Fix error when trying to delete already-deleted file with OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27569))
- Fix batch attachment deletion when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27554))
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459))
- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442))
- Fix handling of `inLanguage` attribute in preview card processing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27423))
- Fix own posts being removed from home timeline when unfollowing a used hashtag ([kmycode](https://github.com/mastodon/mastodon/pull/27391))
- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584))
- Fix format-dependent redirects being cached regardless of requested format ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27634))
## [4.2.1] - 2023-10-10
### Added

View File

@ -1,14 +0,0 @@
# Divergences
## Major Features
- quote posting
- Treehouse::Automod (experimental feature flagged)
## Other Changes
- various build system changes
- a better dockerfile
- yarn v2 (a mistake, tbh)
- various dev env changes
- various css/style changes

View File

@ -26,8 +26,6 @@ ARG MASTODON_VERSION_PRERELEASE=""
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"]
ARG MASTODON_VERSION_METADATA=""
ARG SOURCE_TAG=""
# Allow Ruby on Rails to serve static files
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
ARG RAILS_SERVE_STATIC_FILES="true"
@ -191,7 +189,7 @@ RUN \
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
# Install Node packages
yarn workspaces focus --production @mastodon/mastodon
yarn workspaces focus --production @mastodon/mastodon;
# Create temporary assets build layer from build layer
FROM build as precompiler
@ -210,7 +208,9 @@ RUN \
# Use Ruby on Rails to create Mastodon assets
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile; \
# Cleanup temporary files
rm -fr /opt/mastodon/tmp
rm -fr /opt/mastodon/tmp; \
# TODO(kouhai): fork emoji-mart instead of patching
mv ./emoji_data/all.json ./node_modules/emoji-mart/data/all.json
# Prep final Mastodon Ruby layer
FROM ruby as mastodon

View File

@ -26,7 +26,7 @@ gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.18.0', require: false
gem 'bootsnap', '~> 1.17.0', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'chewy', '~> 7.3'
@ -63,7 +63,7 @@ gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.15'
gem 'nsa'
gem 'nsa', github: 'jhawthorn/nsa', ref: 'e020fcc3a54d993ab45b7194d89ab720296c111b'
gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14'
gem 'parslet'

View File

@ -7,6 +7,17 @@ GIT
hkdf (~> 0.2)
jwt (~> 2.0)
GIT
remote: https://github.com/jhawthorn/nsa.git
revision: e020fcc3a54d993ab45b7194d89ab720296c111b
ref: e020fcc3a54d993ab45b7194d89ab720296c111b
specs:
nsa (0.2.8)
activesupport (>= 4.2, < 7.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
GEM
remote: https://rubygems.org/
specs:
@ -144,9 +155,9 @@ GEM
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
blurhash (0.1.7)
bootsnap (1.18.3)
bootsnap (1.17.1)
msgpack (~> 1.2)
brakeman (6.1.2)
brakeman (6.1.1)
racc
browser (5.3.1)
brpoplpush-redis_script (0.1.3)
@ -156,11 +167,11 @@ GEM
bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
capybara (3.40.0)
capybara (3.39.2)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.11)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
@ -169,7 +180,7 @@ GEM
activesupport
cbor (0.5.9.6)
charlock_holmes (0.7.7)
chewy (7.5.1)
chewy (7.5.0)
activesupport (>= 5.2)
elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl
@ -182,8 +193,7 @@ GEM
cose (1.3.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
crack (0.4.6)
bigdecimal
crack (0.4.5)
rexml
crass (1.0.6)
css_parser (1.14.0)
@ -310,13 +320,13 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.56.0)
haml_lint (0.55.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
rubocop (>= 1.0)
sysexits (~> 1.1)
hashdiff (1.1.0)
hashdiff (1.0.1)
hashie (5.0.0)
hcaptcha (7.1.0)
json
@ -352,7 +362,7 @@ GEM
terminal-table (>= 1.5.1)
idn-ruby (0.1.5)
io-console (0.7.2)
irb (1.11.2)
irb (1.11.1)
rdoc
reline (>= 0.4.2)
jmespath (1.6.2)
@ -456,14 +466,9 @@ GEM
net-smtp (0.4.0.1)
net-protocol
nio4r (2.5.9)
nokogiri (1.16.2)
nokogiri (1.16.0)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nsa (0.3.0)
activesupport (>= 4.2, < 7.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.16.3)
bigdecimal (>= 3.0)
omniauth (2.1.1)
@ -506,8 +511,8 @@ GEM
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.5.5)
pghero (3.4.1)
pg (1.5.4)
pghero (3.4.0)
activerecord (>= 6)
posix-spawn (0.3.15)
premailer (1.21.0)
@ -708,7 +713,7 @@ GEM
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.33)
sidekiq-unique-jobs (7.1.31)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
redis (< 5.0)
@ -767,7 +772,7 @@ GEM
unf (~> 0.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2024.1)
tzinfo-data (1.2023.4)
tzinfo (>= 1.0.0)
unf (0.1.4)
unf_ext
@ -794,7 +799,7 @@ GEM
webfinger (1.2.0)
activesupport
httpclient (>= 2.4)
webmock (3.20.0)
webmock (3.19.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -825,7 +830,7 @@ DEPENDENCIES
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.18.0)
bootsnap (~> 1.17.0)
brakeman (~> 6.0)
browser
bundler-audit (~> 0.9)
@ -883,7 +888,7 @@ DEPENDENCIES
net-http (~> 0.4.0)
net-ldap (~> 0.18)
nokogiri (~> 1.15)
nsa
nsa!
oj (~> 3.14)
omniauth (~> 2.0)
omniauth-cas (~> 3.0.0.beta.1)

View File

@ -1,4 +1,5 @@
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
stream: env PORT=4000 NODE_ENV=development yarn workspace @mastodon/streaming start | npx pino-pretty
stream: env PORT=4000 yarn workspace @mastodon/streaming start
webpack: bin/webpack-dev-server
caddy: caddy run

View File

@ -7,7 +7,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def self.provides_callback_for(provider)
define_method provider do
@provider = provider
@user = User.find_for_omniauth(request.env['omniauth.auth'], current_user)
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
if @user.persisted?
record_login_activity
@ -17,9 +17,6 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
session["devise.#{provider}_data"] = request.env['omniauth.auth']
redirect_to new_user_registration_url
end
rescue ActiveRecord::RecordInvalid
flash[:alert] = I18n.t('devise.failure.omniauth_user_creation_failure') if is_navigational_format?
redirect_to new_user_session_url
end
end

View File

@ -174,19 +174,7 @@ module JsonLdHelper
build_request(uri, on_behalf_of, options: request_options).perform do |response|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
end
end
def valid_activitypub_content_type?(response)
return true if response.mime_type == 'application/activity+json'
# When the mime type is `application/ld+json`, we need to check the profile,
# but `http.rb` does not parse it for us.
return false unless response.mime_type == 'application/ld+json'
response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str|
str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams')
body_to_json(response.body_with_limit) if response.code == 200
end
end

View File

@ -301,16 +301,12 @@ export function submitCompose(routerHistory) {
insertIfOnline('direct');
}
// Upstream shows a "post published" message every time you post now. This is highly annoying.
// Instead, only display a message if a draft is saved.
if (statusId !== null) {
dispatch(showAlert({
message: messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}
dispatch(showAlert({
message: statusId === null ? messages.published : messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}).catch(function (error) {
dispatch(submitComposeFail(error));
});

View File

@ -10,7 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
import FormatQuoteIcon from '@/material-icons/400-24px/format_quote-fill.svg?react';
import FormatQuoteIcon from '@/material-icons/400-24px/format_quote.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';

View File

@ -14,7 +14,6 @@ import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import LinkIcon from '@/material-icons/400-24px/link.svg?react';
import MovieIcon from '@/material-icons/400-24px/movie.svg?react';
import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react';
import QuoteIcon from '@/material-icons/400-24px/format_quote-fill.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
@ -379,9 +378,10 @@ class StatusContent extends PureComponent {
<span className='quote-display-name'>
<Icon
fixedWidth
icon='quote-right'
id='quote-right'
aria-hidden='true'
key='icon-quote-right'
icon={QuoteIcon} />
key='icon-quote-right' />
<strong className='display-name__html'>
<a onClick={this.handleAccountClick} href={quoteStatus.getIn(['account', 'url'])} dangerouslySetInnerHTML={quoteStatusDisplayName} />
</strong>

View File

@ -15,7 +15,7 @@ export default class AutosuggestAccount extends ImmutablePureComponent {
return (
<div className='autosuggest-account' title={account.get('acct')}>
<div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div>
<div className='autosuggest-account-icon'><Avatar account={account} size={24} /></div>
<DisplayName account={account} inline />
</div>
);

View File

@ -71,6 +71,7 @@ class ComposeForm extends ImmutablePureComponent {
singleColumn: PropTypes.bool,
lang: PropTypes.string,
advancedOptions: ImmutablePropTypes.map,
layout: PropTypes.string,
media: ImmutablePropTypes.list,
sideArm: PropTypes.string,
sensitive: PropTypes.bool,
@ -259,6 +260,7 @@ class ComposeForm extends ImmutablePureComponent {
intl,
advancedOptions,
isSubmitting,
layout,
onChangeSpoilerness,
onPaste,
privacy,
@ -313,7 +315,7 @@ class ComposeForm extends ImmutablePureComponent {
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={!showSearch && !isMobile(window.innerWidth)}
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
lang={this.props.lang}
>
<TextareaIcons advancedOptions={advancedOptions} />

View File

@ -45,6 +45,8 @@ const notFoundFn = () => (
set='twitter'
size={32}
sheetSize={32}
sheetColumns={60}
sheetRows={60}
backgroundImageFn={backgroundImageFn}
/>
@ -103,12 +105,12 @@ class ModifierPickerMenu extends PureComponent {
return (
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
</div>
);
}
@ -143,7 +145,7 @@ class ModifierPicker extends PureComponent {
return (
<div className='emoji-picker-dropdown__modifiers'>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
</div>
);
@ -280,6 +282,8 @@ class EmojiPickerMenuImpl extends PureComponent {
perLine={8}
emojiSize={22}
sheetSize={32}
sheetColumns={60}
sheetRows={60}
custom={buildCustomEmojis(custom_emojis)}
color=''
emoji=''

View File

@ -38,8 +38,8 @@ export default class NavigationBar extends ImmutablePureComponent {
{ profileLink !== undefined && (
<a
className='edit'
href={profileLink}
className='navigation-bar__profile-edit'
><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
)}
</div>

View File

@ -76,7 +76,7 @@ class SearchResults extends ImmutablePureComponent {
return (
<div className='search-results'>
<div className='drawer--results'>
<header className='search-results__header'>
<Icon id='search' icon={SearchIcon} />
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />

View File

@ -71,6 +71,7 @@ const mapStateToProps = state => ({
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']),
advancedOptions: state.getIn(['compose', 'advanced_options']),
layout: state.getIn(['local_settings', 'layout']),
media: state.getIn(['compose', 'media_attachments']),
sideArm: sideArmPrivacy(state),
sensitive: state.getIn(['compose', 'sensitive']),

View File

@ -9,8 +9,6 @@ import { NonceProvider } from 'react-select';
import AsyncSelect from 'react-select/async';
import Toggle from 'react-toggle';
import { maxFeedHashtags } from 'flavours/glitch/initial_state';
import SettingToggle from '../../notifications/components/setting_toggle';
const messages = defineMessages({
@ -48,9 +46,9 @@ class ColumnSettings extends PureComponent {
onSelect = mode => value => {
const oldValue = this.tags(mode);
// Prevent changes that add more than the number of configured
// tags, but allow removing tags that were already added before
if ((value.length > maxFeedHashtags) && !(value < oldValue)) {
// Prevent changes that add more than 4 tags, but allow removing
// tags that were already added before
if ((value.length > 4) && !(value < oldValue)) {
return;
}

View File

@ -68,7 +68,7 @@ class NewListForm extends PureComponent {
<Button
disabled={disabled || !value}
text={title}
title={title}
onClick={this.handleClick}
/>
</form>

View File

@ -11,7 +11,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import QuoteIcon from '@/material-icons/400-24px/format_quote-fill.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
@ -260,7 +259,7 @@ class ActionBar extends PureComponent {
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={replyIcon} iconComponent={replyIconComponent} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='quote-right-icon' disabled={!publicStatus} title={intl.formatMessage(messages.quote)} icon='quote-right' iconComponent={QuoteIcon} onClick={this.handleQuoteClick} /></div>
<div className='detailed-status__button'><IconButton className='quote-right-icon' disabled={!publicStatus} title={intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>

View File

@ -9,10 +9,6 @@ import { connect } from 'react-redux';
import Atrament from 'atrament'; // the doodling library
import { debounce, mapValues } from 'lodash';
import ColorsIcon from '@/material-icons/400-24px/colors.svg?react';
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import UndoIcon from '@/material-icons/400-24px/undo.svg?react';
import { doodleSet, uploadCompose } from 'flavours/glitch/actions/compose';
import { Button } from 'flavours/glitch/components/button';
import { IconButton } from 'flavours/glitch/components/icon_button';
@ -588,10 +584,10 @@ class DoodleModal extends ImmutablePureComponent {
</div>
</div>
<div className='doodle-toolbar'>
<IconButton icon='pencil' iconComponent={EditIcon} title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted />
<IconButton icon='bath' iconComponent={ColorsIcon} title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted />
<IconButton icon='undo' iconComponent={UndoIcon} title='Undo' label='Undo' onClick={this.undo} size={18} inverted />
<IconButton icon='trash' iconComponent={DeleteIcon} title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted />
<IconButton icon='pencil' title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted />
<IconButton icon='bath' title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted />
<IconButton icon='undo' title='Undo' label='Undo' onClick={this.undo} size={18} inverted />
<IconButton icon='trash' title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted />
</div>
<div className='doodle-palette'>
{

View File

@ -602,7 +602,18 @@ class UI extends PureComponent {
const { draggingOver } = this.state;
const { children, isWide, location, dropdownMenuIsOpen, layout, moved } = this.props;
const className = classNames('ui', {
const columnsClass = layout => {
switch (layout) {
case 'single':
return 'single-column';
case 'multiple':
return 'multi-columns';
default:
return 'auto-columns';
}
};
const className = classNames('ui', columnsClass(layout), {
'wide': isWide,
'system-font': this.props.systemFontUi,
'hicolor-privacy-icons': this.props.hicolorPrivacyIcons,

View File

@ -67,7 +67,6 @@ export const hasMultiColumnPath = initialPath === '/'
* @property {InitialStateMeta} meta
* @property {object} local_settings
* @property {number} max_toot_chars
* @property {number} max_feed_hashtags
* @property {number} poll_limits
*/
@ -131,7 +130,6 @@ export const sso_redirect = getMeta('sso_redirect');
// Glitch-soc-specific settings
export const maxChars = (initialState && initialState.max_toot_chars) || 500;
export const maxFeedHashtags = (initialState && initialState.max_feed_hashtags) || 4;
export const favouriteModal = getMeta('favourite_modal');
export const pollLimits = (initialState && initialState.poll_limits);
export const defaultContentType = getMeta('default_content_type');

View File

@ -1,51 +1,4 @@
{
"about.fork_disclaimer": "Glitch-soc adalah perangkat lunak sumber terbuka yang merupakan fork dari Mastodon.",
"account.disclaimer_full": "Informasi di bawah ini mungkin tidak mencerminkan profil pengguna secara lengkap.",
"account.follows": "Mengikuti",
"account.joined": "Bergabung {date}",
"account.suspended_disclaimer_full": "Pengguna ini telah ditangguhkan oleh moderator.",
"account.view_full_profile": "Tampilkan profil lengkap",
"advanced_options.icon_title": "Opsi lanjutan",
"advanced_options.local-only.long": "Jangan mengunggah ke instance lain",
"advanced_options.local-only.short": "Hanya lokal",
"advanced_options.local-only.tooltip": "Postingan ini hanya untuk lokal",
"advanced_options.threaded_mode.long": "Secara otomatis membuka balasan pada postingan",
"advanced_options.threaded_mode.short": "Mode Utasan",
"advanced_options.threaded_mode.tooltip": "Mode utasan dinyalakan",
"boost_modal.missing_description": "Toot ini berisi beberapa media tanpa deskripsi",
"column.favourited_by": "Disukai oleh",
"column.heading": "Lainnya",
"column.reblogged_by": "Dibagikan oleh",
"column.subheading": "Opsi lain-lain",
"column_header.profile": "Profil",
"column_subheading.lists": "Daftar",
"column_subheading.navigation": "Penelusuran",
"community.column_settings.allow_local_only": "Tampilkan toot lokal saja",
"compose.attach": "Lampirkan...",
"compose.attach.doodle": "Gambar sesuatu",
"compose.attach.upload": "Unggah file",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Bahasa Markdown",
"compose.content-type.plain": "Teks biasa",
"compose_form.poll.multiple_choices": "Izinkan beberapa pilihan",
"compose_form.poll.single_choice": "Izinkan hanya satu pilihan",
"compose_form.spoiler": "Sembunyikan teks di balik peringatan",
"confirmation_modal.do_not_ask_again": "Jangan minta konfirmasi lagi",
"confirmations.deprecated_settings.confirm": "Gunakan preferensi Mastodon",
"confirmations.deprecated_settings.message": "Beberapa {app_settings} khusus perangkat Glitch-soc yang Anda gunakan telah digantikan oleh {preferences} Mastodon dan akan diganti:",
"confirmations.missing_media_description.confirm": "Tetap kirim",
"confirmations.missing_media_description.edit": "Sunting media",
"confirmations.missing_media_description.message": "Setidaknya satu lampiran media tidak memiliki deskripsi. Pertimbangkan untuk mendeskripsikan semua lampiran media untuk pengguna tunanetra sebelum mengirim toot Anda.",
"confirmations.unfilter.author": "Penulis",
"confirmations.unfilter.confirm": "Tampilkan",
"confirmations.unfilter.edit_filter": "Ubah saringan",
"content-type.change": "Jenis konten",
"direct.group_by_conversations": "Grupkan berdasarkan percakapan",
"endorsed_accounts_editor.endorsed_accounts": "Akun pilihan",
"favourite_modal.combo": "Anda dapat menekan {combo} untuk melewati ini lain kali",
"firehose.column_settings.allow_local_only": "Tampilkan postingan khusus lokal di \"Semua\"",
"home.column_settings.advanced": "Lanjutan",
"home.column_settings.filter_regex": "Saring dengan ekspresi reguler",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View File

@ -1,52 +1,4 @@
{
"account.follows": "Följer",
"account.joined": "Gick med {date}",
"account.suspended_disclaimer_full": "Denna användare har stängts av av en moderator.",
"account.view_full_profile": "Visa full profil",
"advanced_options.icon_title": "Avancerade inställningar",
"advanced_options.local-only.long": "Lägg inte ut på andra instanser",
"advanced_options.local-only.short": "Endast lokalt",
"advanced_options.local-only.tooltip": "Detta inlägg är endast tillgängligt lokalt",
"advanced_options.threaded_mode.long": "Öppnar automatiskt ett svar vid publicering",
"advanced_options.threaded_mode.short": "Tråd-läge",
"advanced_options.threaded_mode.tooltip": "Tråd-läge på",
"boost_modal.missing_description": "Denna toot innehåller viss media utan beskrivning",
"column.favourited_by": "Favoritmarkerad av",
"column.heading": "Övrigt",
"column.reblogged_by": "Boostad av",
"column.subheading": "Övriga val",
"column_header.profile": "Profil",
"column_subheading.lists": "Listor",
"column_subheading.navigation": "Navigering",
"community.column_settings.allow_local_only": "Visa endast lokala toots",
"compose.attach": "Bifoga...",
"compose.attach.doodle": "Rita något",
"compose.attach.upload": "Ladda upp en fil",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",
"compose.content-type.plain": "Klartext",
"compose_form.poll.multiple_choices": "Tillåt flera val",
"compose_form.poll.single_choice": "Tillåt ett val",
"compose_form.spoiler": "Göm text bakom varning",
"confirmation_modal.do_not_ask_again": "Fråga mig inte igen",
"confirmations.deprecated_settings.confirm": "Använd Mastodon-preferenser",
"confirmations.deprecated_settings.message": "Några av de glitch-soc-enhetsspecifika {app_settings} som du använder har ersatts av Mastodon-{preferences} och kommer att åsidosättas:",
"confirmations.missing_media_description.confirm": "Lägg ut ändå",
"confirmations.missing_media_description.edit": "Redigera media",
"confirmations.missing_media_description.message": "Minst en mediebilaga saknar beskrivning. Överväg att beskriva all media för synskadade innan du skickar din toot.",
"confirmations.unfilter.author": "Användare",
"confirmations.unfilter.confirm": "Visa",
"confirmations.unfilter.edit_filter": "Redigera filter",
"confirmations.unfilter.filters": "Matchande {count, plural, one {filter} other {filters}}",
"content-type.change": "Innehållstyp",
"direct.group_by_conversations": "Sortera efter konversation",
"endorsed_accounts_editor.endorsed_accounts": "Utvalda konton",
"favourite_modal.combo": "Du kan trycka på {combo} för att skippa detta nästa gång",
"firehose.column_settings.allow_local_only": "Visa endast lokala inlägg i \"Alla\"",
"home.column_settings.advanced": "Avancerat",
"home.column_settings.filter_regex": "Filtrera bort med reguljära uttryck",
"home.column_settings.show_direct": "Visa privata omnämningar",
"home.settings": "Kolumninställningar",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View File

@ -14,7 +14,9 @@ const initialState = ImmutableMap({
export default function meta(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
return state.merge(action.state.get('meta')).set('permissions', action.state.getIn(['role', 'permissions']));
return state.merge(action.state.get('meta'))
.set('permissions', action.state.getIn(['role', 'permissions']))
.set('layout', layoutFromWindow(action.state.getIn(['local_settings', 'layout'])));
case changeLayout.type:
return state.set('layout', action.payload.layout);
default:

View File

@ -10,6 +10,37 @@
background-size: $size $size;
}
@mixin single-column($media, $parent: '&') {
.auto-columns #{$parent} {
@media #{$media} {
@content;
}
}
.single-column #{$parent} {
@content;
}
}
@mixin limited-single-column($media, $parent: '&') {
.auto-columns #{$parent},
.single-column #{$parent} {
@media #{$media} {
@content;
}
}
}
@mixin multi-columns($media, $parent: '&') {
.auto-columns #{$parent} {
@media #{$media} {
@content;
}
}
.multi-columns #{$parent} {
@content;
}
}
@mixin fullwidth-gallery {
&.full-width {
margin-left: -14px;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,298 @@
.image {
position: relative;
overflow: hidden;
&__preview {
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
&.loaded &__preview {
display: none;
}
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
border: 0;
background: transparent;
opacity: 0;
}
&.loaded img {
opacity: 1;
}
}
.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;
.version {
white-space: nowrap;
}
strong {
font-weight: 500;
}
a {
color: $dark-text-color;
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
}
}
.about {
padding: 20px;
@media screen and (min-width: $no-gap-breakpoint) {
border-radius: 4px;
}
&__footer {
color: $dark-text-color;
text-align: center;
font-size: 15px;
line-height: 22px;
margin-top: 20px;
}
&__header {
margin-bottom: 30px;
&__hero {
width: 100%;
height: auto;
aspect-ratio: 1.9;
background: lighten($ui-base-color, 4%);
border-radius: 8px;
margin-bottom: 30px;
}
h1,
p {
text-align: center;
}
h1 {
font-size: 24px;
line-height: 1.5;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: $darker-text-color;
}
}
&__meta {
background: lighten($ui-base-color, 4%);
border-radius: 4px;
display: flex;
margin-bottom: 30px;
font-size: 15px;
&__column {
box-sizing: border-box;
width: 50%;
padding: 20px;
}
&__divider {
width: 0;
border: 0;
border-style: solid;
border-color: lighten($ui-base-color, 8%);
border-left-width: 1px;
min-height: calc(100% - 60px);
flex: 0 0 auto;
}
h4 {
font-size: 15px;
text-transform: uppercase;
color: $darker-text-color;
font-weight: 500;
margin-bottom: 20px;
}
@media screen and (width <= 600px) {
display: block;
h4 {
text-align: center;
}
&__column {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
&__divider {
min-height: 0;
width: 100%;
border-left-width: 0;
border-top-width: 1px;
}
}
.layout-multiple-columns & {
display: block;
h4 {
text-align: center;
}
&__column {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
&__divider {
min-height: 0;
width: 100%;
border-left-width: 0;
border-top-width: 1px;
}
}
}
&__mail {
color: $primary-text-color;
text-decoration: none;
font-weight: 500;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
.link-footer {
padding: 0;
margin-top: 60px;
text-align: center;
font-size: 15px;
line-height: 22px;
@media screen and (min-width: $no-gap-breakpoint) {
display: none;
}
}
.account {
padding: 0;
border: 0;
}
.account__avatar-wrapper {
margin-inline-start: 0;
}
.account__relationship {
display: none;
}
&__section {
margin-bottom: 10px;
&__title {
display: flex;
align-items: center;
gap: 6px;
font-size: 17px;
font-weight: 600;
line-height: 22px;
padding: 20px;
border-radius: 4px;
background: lighten($ui-base-color, 4%);
color: $highlight-text-color;
cursor: pointer;
}
&.active &__title {
border-radius: 4px 4px 0 0;
}
&__body {
border: 1px solid lighten($ui-base-color, 4%);
border-top: 0;
padding: 20px;
font-size: 15px;
line-height: 22px;
}
}
&__domain-blocks {
margin-top: 30px;
background: darken($ui-base-color, 4%);
border: 1px solid lighten($ui-base-color, 4%);
border-radius: 4px;
&__domain {
border-bottom: 1px solid lighten($ui-base-color, 4%);
padding: 10px;
font-size: 15px;
color: $darker-text-color;
&:nth-child(2n) {
background: darken($ui-base-color, 2%);
}
&:last-child {
border-bottom: 0;
}
&__header {
display: flex;
gap: 10px;
justify-content: space-between;
font-weight: 500;
margin-bottom: 4px;
}
h6 {
color: $secondary-text-color;
font-size: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
p {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}

View File

@ -0,0 +1,841 @@
.account {
padding: 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
color: inherit;
text-decoration: none;
.account__display-name {
flex: 1 1 auto;
display: flex;
align-items: center;
gap: 10px;
color: $darker-text-color;
overflow: hidden;
text-decoration: none;
font-size: 14px;
.display-name {
margin-bottom: 4px;
}
.display-name strong {
display: inline;
}
}
&--minimal {
.account__display-name {
.display-name {
margin-bottom: 0;
}
.display-name strong {
display: block;
}
}
}
&__note {
font-size: 14px;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
margin-top: 10px;
color: $darker-text-color;
&--missing {
color: $dark-text-color;
}
p {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
a {
color: inherit;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
}
}
.account__wrapper {
display: flex;
gap: 10px;
align-items: center;
}
.account__avatar-wrapper {
float: left;
}
.account__avatar {
@include avatar-radius;
display: block;
position: relative;
overflow: hidden;
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
&-inline {
display: inline-block;
vertical-align: middle;
margin-inline-end: 5px;
}
&-composite {
@include avatar-radius;
overflow: hidden;
position: relative;
& > div {
@include avatar-radius;
float: left;
position: relative;
box-sizing: border-box;
}
.account__avatar {
width: 100% !important;
height: 100% !important;
}
&__label {
display: block;
position: absolute;
top: 50%;
inset-inline-start: 50%;
transform: translate(-50%, -50%);
color: $primary-text-color;
text-shadow: 1px 1px 2px $base-shadow-color;
font-weight: 700;
font-size: 15px;
}
}
}
.account__avatar-overlay {
position: relative;
&-overlay {
position: absolute;
bottom: 0;
inset-inline-end: 0;
z-index: 1;
}
}
.account__relationship {
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.account__header__wrapper {
flex: 0 0 auto;
background: lighten($ui-base-color, 4%);
}
.account__disclaimer {
display: flex;
padding: 10px;
gap: 5px;
color: $dark-text-color;
align-items: center;
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
a {
font-weight: 500;
color: inherit;
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
}
.account__action-bar {
border-top: 1px solid lighten($ui-base-color, 8%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
line-height: 36px;
overflow: hidden;
flex: 0 0 auto;
display: flex;
}
.account__action-bar-links {
display: flex;
flex: 1 1 auto;
line-height: 18px;
text-align: center;
}
.account__action-bar__tab {
text-decoration: none;
overflow: hidden;
flex: 0 1 100%;
border-inline-start: 1px solid lighten($ui-base-color, 8%);
padding: 10px 0;
border-bottom: 4px solid transparent;
&:first-child {
border-inline-start: 0;
}
&.active {
border-bottom: 4px solid $ui-highlight-color;
}
& > span {
display: block;
text-transform: uppercase;
font-size: 11px;
color: $darker-text-color;
}
strong {
display: block;
font-size: 15px;
font-weight: 500;
color: $primary-text-color;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
abbr {
color: $highlight-text-color;
}
}
.account-authorize {
padding: 14px 10px;
.detailed-status__display-name {
display: block;
margin-bottom: 15px;
overflow: hidden;
}
}
.account-authorize__avatar {
float: left;
margin-inline-end: 10px;
}
.notification__report {
padding: 8px 10px;
padding-inline-start: 68px;
position: relative;
border-bottom: 1px solid lighten($ui-base-color, 8%);
min-height: 54px;
&__details {
display: flex;
justify-content: space-between;
align-items: center;
color: $darker-text-color;
font-size: 15px;
line-height: 22px;
strong {
font-weight: 500;
}
}
&__avatar {
position: absolute;
inset-inline-start: 10px;
top: 10px;
}
}
.notification__message {
margin-inline-start: 42px;
padding-top: 8px;
padding-inline-start: 26px;
cursor: default;
color: $darker-text-color;
font-size: 15px;
position: relative;
align-items: center;
.icon {
color: $highlight-text-color;
width: 18px;
height: 18px;
}
.icon-star {
color: $gold-star;
}
> span {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
.account--panel {
background: lighten($ui-base-color, 4%);
border-top: 1px solid lighten($ui-base-color, 8%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
display: flex;
flex-direction: row;
padding: 10px 0;
}
.account--panel__button,
.detailed-status__button {
flex: 1 1 auto;
text-align: center;
}
.relationship-tag {
color: $white;
margin-bottom: 4px;
display: block;
background-color: rgba($black, 0.45);
text-transform: uppercase;
font-size: 11px;
font-weight: 500;
padding: 4px;
border-radius: 4px;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
.account-gallery__container {
display: flex;
flex-wrap: wrap;
padding: 4px 2px;
}
.account-gallery__item {
border: 0;
box-sizing: border-box;
display: block;
position: relative;
border-radius: 4px;
overflow: hidden;
margin: 2px;
&__icons {
position: absolute;
top: 50%;
inset-inline-start: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
}
}
.notification__filter-bar,
.account__section-headline {
background: darken($ui-base-color, 4%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;
display: flex;
flex-shrink: 0;
button {
background: transparent;
border: 0;
margin: 0;
}
button,
a {
display: block;
flex: 1 1 auto;
color: $darker-text-color;
padding: 15px 0;
font-size: 14px;
font-weight: 500;
text-align: center;
text-decoration: none;
position: relative;
&.active {
color: $primary-text-color;
&::before {
display: block;
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 3px;
border-radius: 4px;
background: $highlight-text-color;
}
}
}
&.directory__section-headline {
background: darken($ui-base-color, 2%);
border-bottom-color: transparent;
a,
button {
&.active {
&::before {
display: none;
}
&::after {
border-color: transparent transparent darken($ui-base-color, 7%);
}
}
}
}
}
.account__moved-note {
padding: 14px 10px;
padding-bottom: 16px;
background: lighten($ui-base-color, 4%);
border-top: 1px solid lighten($ui-base-color, 8%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
&__message {
position: relative;
margin-inline-start: 58px;
color: $dark-text-color;
padding: 8px 0;
padding-top: 0;
padding-bottom: 4px;
font-size: 14px;
> span {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__icon-wrapper {
inset-inline-start: -26px;
position: absolute;
}
.detailed-status__display-avatar {
position: relative;
}
.detailed-status__display-name {
margin-bottom: 0;
}
}
.account__header__content {
color: $darker-text-color;
font-size: 14px;
font-weight: 400;
overflow: hidden;
word-break: normal;
word-wrap: break-word;
p {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
a {
color: inherit;
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
}
.account__header {
overflow: hidden;
&.inactive {
opacity: 0.5;
.account__header__image,
.account__avatar {
filter: grayscale(100%);
}
}
&__info {
position: absolute;
top: 10px;
inset-inline-start: 10px;
}
&__image {
overflow: hidden;
height: 145px;
position: relative;
background: darken($ui-base-color, 4%);
img {
object-fit: cover;
display: block;
width: 100%;
height: 100%;
margin: 0;
}
}
&__bar {
position: relative;
background: lighten($ui-base-color, 4%);
padding: 5px;
border-bottom: 1px solid lighten($ui-base-color, 12%);
.avatar {
display: block;
flex: 0 0 auto;
width: 94px;
.account__avatar {
background: darken($ui-base-color, 8%);
border: 2px solid lighten($ui-base-color, 4%);
}
}
}
&__badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
.account-role {
line-height: unset;
}
}
&__tabs {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 7px 10px;
margin-top: -55px;
gap: 8px;
overflow: hidden;
margin-inline-start: -2px; // aligns the pfp with content below
&__buttons {
display: flex;
align-items: center;
gap: 8px;
padding-top: 55px;
overflow: hidden;
.button {
flex-shrink: 1;
white-space: nowrap;
@media screen and (max-width: $no-gap-breakpoint) {
min-width: 0;
}
}
.icon-button {
border: 1px solid lighten($ui-base-color, 12%);
border-radius: 4px;
box-sizing: content-box;
padding: 5px;
.icon {
width: 24px;
height: 24px;
}
}
}
&__name {
padding: 5px 10px;
.emojione {
width: 22px;
height: 22px;
}
h1 {
font-size: 16px;
line-height: 24px;
color: $primary-text-color;
font-weight: 500;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
small {
display: block;
font-size: 14px;
color: $darker-text-color;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
span {
user-select: all;
}
.icon-lock {
height: 16px;
width: 16px;
position: relative;
top: 3px;
}
}
}
}
.spacer {
flex: 1 1 auto;
}
}
&__bio {
overflow: hidden;
margin: 0 -5px;
.account__header__content {
padding: 20px 15px;
padding-bottom: 5px;
color: $primary-text-color;
}
.account__header__joined {
font-size: 14px;
padding: 5px 15px;
color: $darker-text-color;
.columns-area--mobile & {
padding-inline-start: 20px;
padding-inline-end: 20px;
}
}
.account__header__fields {
margin: 0;
border-top: 1px solid lighten($ui-base-color, 12%);
a {
color: lighten($ui-highlight-color, 8%);
}
dl:first-child .verified {
border-radius: 0 4px 0 0;
}
.icon {
width: 18px;
height: 18px;
vertical-align: middle;
}
dd {
display: flex;
align-items: center;
gap: 4px;
}
.verified a {
color: $valid-value-color;
}
}
}
&__extra {
margin-top: 4px;
&__links {
font-size: 14px;
color: $darker-text-color;
padding: 10px 0;
a {
display: inline-block;
color: $darker-text-color;
text-decoration: none;
padding: 5px 10px;
font-weight: 500;
strong {
font-weight: 700;
color: $primary-text-color;
}
}
}
}
&__account-note {
margin: 0 -5px;
padding: 10px 15px;
display: flex;
flex-direction: column;
font-size: 14px;
font-weight: 400;
border-top: 1px solid lighten($ui-base-color, 12%);
border-bottom: 1px solid lighten($ui-base-color, 12%);
label {
display: block;
font-size: 12px;
font-weight: 500;
color: $darker-text-color;
text-transform: uppercase;
margin-bottom: 5px;
}
&__content {
white-space: pre-wrap;
padding: 10px 0;
}
strong {
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
textarea {
display: block;
box-sizing: border-box;
width: calc(100% + 20px);
color: $secondary-text-color;
background: $ui-base-color;
padding: 10px;
margin: 0 -10px;
font-family: inherit;
font-size: 14px;
resize: none;
border: 0;
outline: 0;
border-radius: 4px;
&::placeholder {
color: $dark-text-color;
opacity: 1;
}
}
}
}
.account__contents {
overflow: hidden;
}
.account__details {
display: flex;
flex-wrap: wrap;
column-gap: 1em;
}
.verified-badge {
display: inline-flex;
align-items: center;
color: $valid-value-color;
gap: 4px;
overflow: hidden;
white-space: nowrap;
> span {
overflow: hidden;
text-overflow: ellipsis;
}
a {
color: inherit;
font-weight: 500;
text-decoration: none;
}
.icon {
width: 16px;
height: 16px;
}
}
.moved-account-banner,
.follow-request-banner,
.account-memorial-banner {
padding: 20px;
background: lighten($ui-base-color, 4%);
display: flex;
align-items: center;
flex-direction: column;
&__message {
color: $darker-text-color;
padding: 8px 0;
padding-top: 0;
padding-bottom: 4px;
font-size: 14px;
font-weight: 500;
text-align: center;
margin-bottom: 16px;
}
&__action {
display: flex;
justify-content: space-between;
align-items: center;
gap: 15px;
width: 100%;
}
.detailed-status__display-name {
margin-bottom: 0;
}
}
.follow-request-banner .button {
width: 100%;
}
.account-memorial-banner__message {
margin-bottom: 0;
}

View File

@ -0,0 +1,233 @@
.announcements__item__content {
word-wrap: break-word;
overflow-y: auto;
.emojione {
width: 20px;
height: 20px;
margin: -3px 0 0;
}
p {
margin-bottom: 10px;
white-space: pre-wrap;
&:last-child {
margin-bottom: 0;
}
}
a {
color: $secondary-text-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
&.mention {
&:hover {
text-decoration: none;
span {
text-decoration: underline;
}
}
}
&.unhandled-link {
color: $highlight-text-color;
}
}
}
.announcements {
background: lighten($ui-base-color, 8%);
font-size: 13px;
display: flex;
align-items: flex-end;
&__mastodon {
width: 124px;
flex: 0 0 auto;
@media screen and (max-width: 124px + 300px) {
display: none;
}
}
&__container {
width: calc(100% - 124px);
flex: 0 0 auto;
position: relative;
@media screen and (max-width: 124px + 300px) {
width: 100%;
}
}
&__item {
box-sizing: border-box;
width: 100%;
padding: 15px;
position: relative;
font-size: 15px;
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
max-height: 50vh;
overflow: hidden;
display: flex;
flex-direction: column;
&__range {
display: block;
font-weight: 500;
margin-bottom: 10px;
padding-inline-end: 18px;
}
&__unread {
position: absolute;
top: 19px;
inset-inline-end: 19px;
display: block;
background: $highlight-text-color;
border-radius: 50%;
width: 0.625rem;
height: 0.625rem;
}
}
&__pagination {
padding: 15px;
color: $darker-text-color;
position: absolute;
bottom: 3px;
inset-inline-end: 0;
}
}
.layout-multiple-columns .announcements__mastodon {
display: none;
}
.layout-multiple-columns .announcements__container {
width: 100%;
}
.reactions-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-top: 15px;
margin-inline-start: -2px;
width: calc(100% - (90px - 33px));
&__item {
flex-shrink: 0;
background: lighten($ui-base-color, 12%);
border: 0;
border-radius: 3px;
margin: 2px;
cursor: pointer;
user-select: none;
padding: 0 6px;
display: flex;
align-items: center;
transition: all 100ms ease-in;
transition-property: background-color, color;
&__emoji {
display: block;
margin: 3px 0;
width: 16px;
height: 16px;
img {
display: block;
margin: 0;
width: 100%;
height: 100%;
min-width: auto;
min-height: auto;
vertical-align: bottom;
object-fit: contain;
}
}
&__count {
display: block;
min-width: 9px;
font-size: 13px;
font-weight: 500;
text-align: center;
margin-inline-start: 6px;
color: $darker-text-color;
}
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 16%);
transition: all 200ms ease-out;
transition-property: background-color, color;
&__count {
color: lighten($darker-text-color, 4%);
}
}
&.active {
transition: all 100ms ease-in;
transition-property: background-color, color;
background-color: mix(
lighten($ui-base-color, 12%),
$ui-highlight-color,
80%
);
.reactions-bar__item__count {
color: lighten($highlight-text-color, 8%);
}
}
}
.emoji-picker-dropdown {
margin: 2px;
}
&:hover .emoji-button {
opacity: 0.85;
}
.emoji-button {
color: $darker-text-color;
margin: 0;
font-size: 16px;
width: auto;
flex-shrink: 0;
padding: 0 6px;
height: 22px;
display: flex;
align-items: center;
opacity: 0.5;
transition: all 100ms ease-in;
transition-property: background-color, color;
&:hover,
&:active,
&:focus {
opacity: 1;
color: lighten($darker-text-color, 4%);
transition: all 200ms ease-out;
transition-property: background-color, color;
}
}
&--empty {
.emoji-button {
padding: 0;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,674 @@
.compose-form {
padding: 10px;
.emoji-picker-dropdown {
position: absolute;
top: 0;
inset-inline-end: 0;
::-webkit-scrollbar-track:hover,
::-webkit-scrollbar-track:active {
background-color: rgba($base-overlay-background, 0.3);
}
}
}
.character-counter {
cursor: default;
font-family: $font-sans-serif, sans-serif;
font-size: 14px;
font-weight: 600;
color: $lighter-text-color;
&.character-counter--over {
color: $warning-red;
}
}
.no-reduce-motion .spoiler-input {
transition:
height 0.4s ease,
opacity 0.4s ease;
}
.spoiler-input {
height: 0;
transform-origin: bottom;
opacity: 0;
&.spoiler-input--visible {
height: 36px;
margin-bottom: 11px;
opacity: 1;
}
input {
display: block;
box-sizing: border-box;
margin: 0;
border: 0;
border-radius: 4px;
padding: 10px;
width: 100%;
outline: 0;
color: $inverted-text-color;
background: $simple-background-color;
font-size: 14px;
font-family: inherit;
resize: vertical;
&::placeholder {
color: $dark-text-color;
}
&:focus {
outline: 0;
}
@include single-column('screen and (max-width: 630px)') {
font-size: 16px;
}
}
}
.compose-form__warning {
color: $inverted-text-color;
margin-bottom: 15px;
background: $ui-primary-color;
box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
padding: 8px 10px;
border-radius: 4px;
font-size: 13px;
font-weight: 400;
a {
color: $lighter-text-color;
font-weight: 500;
text-decoration: underline;
&:active,
&:focus,
&:hover {
text-decoration: none;
}
}
}
.compose-form__sensitive-button {
padding: 10px;
padding-top: 0;
font-size: 14px;
font-weight: 500;
&.active {
color: $highlight-text-color;
}
input[type='checkbox'] {
appearance: none;
display: inline-block;
position: relative;
border: 1px solid $ui-primary-color;
box-sizing: border-box;
width: 18px;
height: 18px;
flex: 0 0 auto;
margin-inline-start: 5px;
margin-inline-end: 10px;
top: -1px;
border-radius: 4px;
vertical-align: middle;
cursor: inherit;
&:checked {
border-color: $highlight-text-color;
background: $highlight-text-color
url("data:image/svg+xml;utf8,<svg width='18' height='18' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M4.5 8.5L8 12l6-6' stroke='white' stroke-width='1.5'/></svg>")
center center no-repeat;
}
}
}
.quote-indicator,
.reply-indicator {
margin: 0 0 10px;
border-radius: 4px;
padding: 10px;
background: $ui-primary-color;
min-height: 23px;
overflow-y: auto;
flex: 0 2 auto;
}
.quote-indicator__header,
.reply-indicator__header {
margin-bottom: 5px;
overflow: hidden;
}
.quote-indicator__cancel,
.reply-indicator__cancel {
float: right;
line-height: 24px;
}
.quote-indicator__display-name,
.reply-indicator__display-name {
color: $inverted-text-color;
display: block;
max-width: 100%;
line-height: 24px;
overflow: hidden;
text-decoration: none;
& > .display-name {
line-height: unset;
height: unset;
}
}
.quote-indicator__display-avatar,
.reply-indicator__display-avatar {
float: left;
margin-inline-end: 5px;
}
.quote-indicator__content,
.reply-indicator__content {
position: relative;
font-size: 14px;
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
overflow: hidden;
padding-top: 5px;
color: $inverted-text-color;
white-space: pre-wrap;
p,
pre {
margin-bottom: 20px;
white-space: pre-wrap;
&:last-child {
margin-bottom: 0;
}
}
a {
color: $lighter-text-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
&.mention {
&:hover {
text-decoration: none;
span {
text-decoration: underline;
}
}
}
}
.emojione {
width: 20px;
height: 20px;
margin: -5px 0 0;
}
}
.compose-form .compose-form__autosuggest-wrapper {
position: relative;
}
.compose-form .autosuggest-textarea,
.compose-form .autosuggest-input {
position: relative;
width: 100%;
label {
.autosuggest-textarea__textarea {
display: block;
box-sizing: border-box;
margin: 0;
border: 0;
border-radius: 4px 4px 0 0;
padding: 10px 32px 0 10px;
width: 100%;
min-height: 100px;
outline: 0;
color: $inverted-text-color;
background: $simple-background-color;
font-size: 14px;
font-family: inherit;
resize: none;
scrollbar-color: initial;
&::placeholder {
color: $dark-text-color;
}
&::-webkit-scrollbar {
all: unset;
}
&:focus {
outline: 0;
}
@include single-column('screen and (max-width: 630px)') {
font-size: 16px;
}
@include limited-single-column('screen and (max-width: 600px)') {
height: 100px !important; // prevent auto-resize textarea
resize: vertical;
}
}
}
}
.compose-form__textarea-icons {
display: block;
position: absolute;
top: 29px;
inset-inline-end: 5px;
bottom: 5px;
overflow: hidden;
& > .textarea_icon {
display: block;
margin-top: 2px;
margin-inline-start: 2px;
width: 24px;
height: 24px;
color: $lighter-text-color;
font-size: 18px;
line-height: 24px;
text-align: center;
opacity: 0.8;
}
}
.autosuggest-textarea__suggestions-wrapper {
position: relative;
height: 0;
}
.autosuggest-textarea__suggestions {
box-sizing: border-box;
display: none;
position: absolute;
top: 100%;
width: 100%;
z-index: 99;
box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
background: $ui-secondary-color;
border-radius: 0 0 4px 4px;
color: $inverted-text-color;
font-size: 14px;
padding: 6px;
}
.autosuggest-textarea__suggestions--visible {
display: block;
}
.autosuggest-textarea__suggestions__item {
padding: 10px;
cursor: pointer;
border-radius: 4px;
&:hover,
&:focus,
&:active,
&.selected {
background: darken($ui-secondary-color, 10%);
}
.autosuggest-account,
.autosuggest-emoji,
.autosuggest-hashtag {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
line-height: 18px;
font-size: 14px;
}
.autosuggest-hashtag {
justify-content: space-between;
&__name {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
strong {
font-weight: 500;
}
&__uses {
flex: 0 0 auto;
text-align: end;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.autosuggest-account-icon,
.autosuggest-emoji img {
margin-inline-end: 8px;
}
.autosuggest-account .display-name > span {
color: $lighter-text-color;
}
}
.compose-form__upload-wrapper {
overflow: hidden;
}
.compose-form__uploads-wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
font-family: inherit;
padding: 5px;
overflow: hidden;
}
.compose-form__upload {
flex: 1 1 0;
margin: 5px;
min-width: 40%;
.compose-form__upload-thumbnail {
position: relative;
border-radius: 4px;
height: 140px;
width: 100%;
background-color: $base-shadow-color;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
overflow: hidden;
& > .close {
mix-blend-mode: difference;
}
}
.icon-button {
flex: 0 1 auto;
color: $secondary-text-color;
font-size: 14px;
font-weight: 500;
padding: 10px;
font-family: inherit;
&:hover,
&:focus,
&:active {
color: lighten($secondary-text-color, 7%);
}
}
&__warning {
position: absolute;
z-index: 2;
bottom: 0;
inset-inline-start: 0;
inset-inline-end: 0;
box-sizing: border-box;
background: linear-gradient(
0deg,
rgba($base-shadow-color, 0.8) 0,
rgba($base-shadow-color, 0.35) 80%,
transparent
);
}
}
.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;
}
.upload-progress {
display: flex;
padding: 10px;
color: $darker-text-color;
overflow: hidden;
gap: 10px;
.icon {
width: 24px;
height: 24px;
}
span {
display: block;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
}
.upload-progress__message {
flex: 1 1 auto;
}
.upload-progress__backdrop {
position: relative;
margin-top: 5px;
border-radius: 6px;
width: 100%;
height: 6px;
background: darken($simple-background-color, 8%);
}
.upload-progress__tracker {
position: absolute;
top: 0;
inset-inline-start: 0;
height: 6px;
border-radius: 6px;
background: $ui-highlight-color;
}
.compose-form__modifiers {
color: $inverted-text-color;
font-family: inherit;
font-size: 14px;
background: $simple-background-color;
}
.compose-form__buttons-wrapper {
padding: 10px;
background: darken($simple-background-color, 8%);
border-radius: 0 0 4px 4px;
height: 27px;
display: flex;
justify-content: space-between;
flex: 0 0 auto;
}
.compose-form__buttons {
display: flex;
flex: 0 0 auto;
gap: 2px;
.icon-button {
height: 100%;
}
& .icon-button,
& .text-icon-button {
box-sizing: content-box;
padding: 0 3px;
}
}
.character-counter__wrapper {
align-self: center;
margin-inline-end: 4px;
}
.privacy-dropdown__dropdown {
border-radius: 4px;
box-shadow: var(--dropdown-shadow);
background: $simple-background-color;
overflow: hidden;
transform-origin: 50% 0;
}
.privacy-dropdown__option {
display: flex;
align-items: center;
padding: 10px;
color: $inverted-text-color;
cursor: pointer;
.privacy-dropdown__option__content {
flex: 1 1 auto;
color: $lighter-text-color;
&:not(:first-child) {
margin-inline-start: 10px;
}
strong {
display: block;
color: $inverted-text-color;
font-weight: 500;
}
}
&:hover,
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
.privacy-dropdown__option__content {
color: $primary-text-color;
strong {
color: $primary-text-color;
}
}
}
&.active:hover {
background: lighten($ui-highlight-color, 4%);
}
}
.compose-form__publish {
display: flex;
justify-content: flex-end;
min-width: 0;
flex: 0 0 auto;
column-gap: 5px;
.compose-form__publish-button-wrapper {
padding-top: 10px;
button {
padding: 7px 10px;
text-align: center;
& > span {
display: flex;
gap: 5px;
align-items: center;
}
}
.side_arm {
height: 100%;
}
}
}
.language-dropdown {
&__dropdown {
background: $simple-background-color;
box-shadow: var(--dropdown-shadow);
border-radius: 4px;
overflow: hidden;
z-index: 2;
&.top {
transform-origin: 50% 100%;
}
&.bottom {
transform-origin: 50% 0;
}
.emoji-mart-search {
padding-inline-end: 10px;
}
.emoji-mart-search-icon {
inset-inline-end: 10px + 5px;
}
.emoji-mart-scroll {
padding: 0 10px 10px;
}
&__results {
&__item {
cursor: pointer;
color: $inverted-text-color;
font-weight: 500;
padding: 10px;
border-radius: 4px;
display: flex;
gap: 6px;
align-items: center;
&:focus,
&:active,
&:hover {
background: $ui-secondary-color;
}
&__common-name {
color: $darker-text-color;
}
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
outline: 0;
.language-dropdown__dropdown__results__item__common-name {
color: $secondary-text-color;
}
&:hover {
background: lighten($ui-highlight-color, 4%);
}
}
}
}
}
}

View File

@ -0,0 +1,68 @@
.scrollable .account-card {
margin: 10px;
background: lighten($ui-base-color, 8%);
}
.scrollable .account-card__title__avatar {
img,
.account__avatar {
border-color: lighten($ui-base-color, 8%);
}
}
.scrollable .account-card__bio::after {
background: linear-gradient(
to left,
lighten($ui-base-color, 8%),
transparent
);
}
.filter-form {
background: $ui-base-color;
&__column {
padding: 10px 15px;
padding-bottom: 0;
}
.radio-button {
display: block;
}
}
.radio-button {
font-size: 14px;
position: relative;
display: inline-block;
padding: 6px 0;
line-height: 18px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
input[type='radio'],
input[type='checkbox'] {
display: none;
}
&__input {
display: inline-block;
position: relative;
border: 1px solid $ui-primary-color;
box-sizing: border-box;
width: 18px;
height: 18px;
flex: 0 0 auto;
margin-inline-end: 10px;
top: -1px;
border-radius: 50%;
vertical-align: middle;
&.checked {
border-color: lighten($ui-highlight-color, 4%);
background: lighten($ui-highlight-color, 4%);
}
}
}

View File

@ -0,0 +1,23 @@
.domain {
padding: 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
.domain__domain-name {
flex: 1 1 auto;
display: block;
color: $primary-text-color;
text-decoration: none;
font-size: 14px;
font-weight: 500;
}
}
.domain__wrapper {
display: flex;
}
.domain_buttons {
height: 18px;
padding: 10px;
white-space: nowrap;
}

View File

@ -0,0 +1,309 @@
.drawer {
width: 300px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow-y: hidden;
padding: 10px 5px;
flex: none;
&:first-child {
padding-inline-start: 10px;
}
&:last-child {
padding-inline-end: 10px;
}
@include single-column('screen and (max-width: 630px)') {
flex: auto;
}
@include limited-single-column('screen and (max-width: 630px)') {
&,
&:first-child,
&:last-child {
padding: 0;
}
}
.wide & {
min-width: 300px;
max-width: 400px;
flex: 1 1 200px;
}
@include single-column('screen and (max-width: 630px)') {
:root & {
// Overrides `.wide` for single-column view
flex: auto;
width: 100%;
min-width: 0;
max-width: none;
padding: 0;
}
}
.react-swipeable-view-container & {
height: 100%;
}
}
.drawer__tab {
display: flex;
flex: 1 1 auto;
padding: 13px 3px 11px;
color: $darker-text-color;
text-decoration: none;
text-align: center;
font-size: 16px;
align-items: center;
justify-content: center;
border-bottom: 2px solid transparent;
}
.drawer__header {
flex: none;
font-size: 16px;
background: $ui-base-color;
margin-bottom: 10px;
display: flex;
flex-direction: row;
border-radius: 4px;
overflow: hidden;
a:hover {
background: lighten($ui-base-color, 3%);
}
}
.search {
position: relative;
margin-bottom: 10px;
flex: none;
@include limited-single-column(
'screen and (max-width: #{$no-gap-breakpoint})'
) {
margin-bottom: 0;
}
@include single-column('screen and (max-width: 630px)') {
font-size: 16px;
}
}
.navigation-bar {
padding: 10px;
color: $darker-text-color;
display: flex;
align-items: center;
a {
color: inherit;
text-decoration: none;
}
.acct {
display: block;
color: $secondary-text-color;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.navigation-bar__actions {
position: relative;
.compose__action-bar .icon-button {
pointer-events: auto;
transform: scale(1, 1) translate(0, 0);
opacity: 1;
.icon {
width: 24px;
height: 24px;
}
}
}
}
.navigation-bar__profile {
display: flex;
flex-direction: column;
flex: 1 1 auto;
margin-inline-start: 8px;
}
.drawer--results {
overflow-x: hidden;
overflow-y: scroll;
}
.search-results__section {
border-bottom: 1px solid lighten($ui-base-color, 8%);
&:last-child {
border-bottom: 0;
}
&__header {
background: darken($ui-base-color, 4%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
padding: 15px;
font-weight: 500;
font-size: 14px;
color: $darker-text-color;
display: flex;
justify-content: space-between;
h3 {
display: flex;
align-items: center;
gap: 5px;
}
button {
color: $highlight-text-color;
padding: 0;
border: 0;
background: 0;
font: inherit;
&:hover,
&:active,
&:focus {
text-decoration: underline;
}
}
}
.account:last-child,
& > div:last-child .status {
border-bottom: 0;
}
& > .hashtag {
display: block;
padding: 10px;
color: $secondary-text-color;
text-decoration: none;
&:hover,
&:active,
&:focus {
color: lighten($secondary-text-color, 4%);
text-decoration: underline;
}
}
}
.drawer__pager {
box-sizing: border-box;
padding: 0;
flex-grow: 1;
position: relative;
overflow: hidden;
display: flex;
border-radius: 4px;
}
.drawer__inner {
position: absolute;
top: 0;
inset-inline-start: 0;
background: $ui-base-color;
box-sizing: border-box;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
overflow-y: auto;
width: 100%;
height: 100%;
&.darker {
background: $ui-base-color;
}
}
.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($ui-base-color)}"/></svg>')
no-repeat bottom / 100% auto;
flex: 1;
min-height: 47px;
display: none;
> img {
display: block;
object-fit: contain;
object-position: bottom left;
width: 85%;
height: 100%;
pointer-events: none;
user-select: none;
}
> .mastodon {
display: block;
width: 100%;
height: 100%;
border: 0;
cursor: inherit;
}
@media screen and (height >= 640px) {
display: block;
}
}
.pseudo-drawer {
background: lighten($ui-base-color, 13%);
font-size: 13px;
text-align: start;
}
.drawer__backdrop {
cursor: pointer;
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
height: 100%;
background: rgba($base-overlay-background, 0.5);
}
@for $i from 0 through 3 {
.mbstobon-#{$i} .drawer__inner__mastodon {
@if $i == 3 {
background:
url('~flavours/glitch/images/wave-drawer.png')
no-repeat
bottom /
100%
auto,
$ui-base-color;
} @else {
background:
url('~flavours/glitch/images/wave-drawer-glitched.png')
no-repeat
bottom /
100%
auto,
$ui-base-color;
}
& > .mastodon {
background: url('~flavours/glitch/images/mbstobon-ui-#{$i}.png')
no-repeat
left
bottom /
contain;
@if $i != 3 {
filter: contrast(50%) brightness(50%);
}
}
}
}

View File

@ -0,0 +1,106 @@
.emojione {
font-size: inherit;
vertical-align: middle;
object-fit: contain;
margin: -0.2ex 0.15em 0.2ex;
width: 16px;
height: 16px;
img {
width: auto;
}
}
.emoji-picker-dropdown__menu {
background: $simple-background-color;
position: relative;
box-shadow: var(--dropdown-shadow);
border-radius: 4px;
margin-top: 5px;
z-index: 2;
.emoji-mart-scroll {
transition: opacity 200ms ease;
}
&.selecting .emoji-mart-scroll {
opacity: 0.5;
}
}
.emoji-picker-dropdown__modifiers {
position: absolute;
top: 60px;
inset-inline-end: 11px;
cursor: pointer;
}
.emoji-picker-dropdown__modifiers__menu {
position: absolute;
z-index: 4;
top: -4px;
inset-inline-start: -8px;
background: $simple-background-color;
border-radius: 4px;
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
overflow: hidden;
button {
display: block;
cursor: pointer;
border: 0;
padding: 4px 8px;
background: transparent;
&:hover,
&:focus,
&:active {
background: rgba($ui-secondary-color, 0.4);
}
}
.emoji-mart-emoji {
height: 22px;
}
}
.emoji-mart-emoji {
span {
background-repeat: no-repeat;
}
}
.emoji-button {
display: block;
padding-top: 5px;
padding-bottom: 2px;
padding-inline-start: 2px;
padding-inline-end: 5px;
outline: 0;
cursor: pointer;
img {
filter: grayscale(100%);
opacity: 0.8;
display: block;
margin: 0;
width: 22px;
height: 22px;
}
&:hover,
&:active,
&:focus {
img {
opacity: 1;
filter: none;
border-radius: 100%;
}
}
&:focus-visible {
img {
outline: $ui-button-icon-focus-outline;
}
}
}

View File

@ -1,14 +1,14 @@
.emoji-mart {
font-size: 13px;
display: inline-block;
color: $inverted-text-color;
&,
* {
box-sizing: border-box;
line-height: 1.15;
}
font-size: 13px;
display: inline-block;
color: $inverted-text-color;
.emoji-mart-emoji {
padding: 6px;
}
@ -64,17 +64,17 @@
}
.emoji-mart-anchor-bar {
bottom: -1px;
bottom: 0;
}
}
.emoji-mart-anchor-bar {
position: absolute;
bottom: -5px;
bottom: -3px;
inset-inline-start: 0;
width: 100%;
height: 4px;
background-color: $highlight-text-color;
height: 3px;
background-color: darken($ui-highlight-color, 3%);
}
.emoji-mart-anchors {
@ -173,7 +173,7 @@
}
&:hover::before {
z-index: -1;
z-index: 0;
content: '';
position: absolute;
top: 0;

View File

@ -0,0 +1,143 @@
.account-card__header {
position: relative;
}
.explore__search-header {
background: darken($ui-base-color, 4%);
justify-content: center;
align-items: center;
padding: 15px;
.search {
width: 100%;
margin-bottom: 0;
}
.search__input {
border: 1px solid lighten($ui-base-color, 8%);
padding: 10px;
}
.search__popout {
border: 1px solid lighten($ui-base-color, 8%);
}
.search .icon {
top: 9px;
inset-inline-end: 10px;
color: $dark-text-color;
}
}
.explore__search-results {
flex: 1 1 auto;
display: flex;
flex-direction: column;
background: $ui-base-color;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.story {
display: flex;
align-items: center;
color: $primary-text-color;
text-decoration: none;
padding: 15px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
gap: 15px;
&:last-child {
border-bottom: 0;
}
&:hover,
&:active,
&:focus {
color: $highlight-text-color;
.story__details__publisher,
.story__details__shared {
color: $highlight-text-color;
}
}
&__details {
flex: 1 1 auto;
&__publisher {
color: $darker-text-color;
margin-bottom: 8px;
}
&__title {
font-size: 19px;
line-height: 24px;
font-weight: 500;
margin-bottom: 8px;
}
&__shared {
color: $darker-text-color;
}
strong {
font-weight: 500;
}
}
&__thumbnail {
flex: 0 0 auto;
position: relative;
width: 120px;
height: 120px;
.skeleton {
width: 100%;
height: 100%;
}
img {
border-radius: 8px;
display: block;
margin: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
&__preview {
border-radius: 8px;
display: block;
margin: 0;
width: 100%;
height: 100%;
object-fit: fill;
position: absolute;
top: 0;
inset-inline-start: 0;
z-index: 0;
&--hidden {
display: none;
}
}
}
&.expanded {
flex-direction: column;
.story__thumbnail {
order: 1;
width: 100%;
height: auto;
aspect-ratio: 1.91 / 1;
}
.story__details {
order: 2;
width: 100%;
flex: 0 0 auto;
}
}
}

View File

@ -0,0 +1,24 @@
@import 'misc';
@import 'accounts';
@import 'domains';
@import 'status';
@import 'modal';
@import 'compose_form';
@import 'columns';
@import 'regeneration_indicator';
@import 'directory';
@import 'search';
@import 'emoji';
@import 'doodle';
@import 'drawer';
@import 'media';
@import 'sensitive';
@import 'lists';
@import 'emoji_picker';
@import 'local_settings';
@import 'single_column';
@import 'announcements';
@import 'explore';
@import 'signed_out';
@import 'privacy_policy';
@import 'about';

View File

@ -0,0 +1,94 @@
.list-editor {
background: $ui-base-color;
flex-direction: column;
border-radius: 8px;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
width: 380px;
overflow: hidden;
@media screen and (width <= 420px) {
width: 90%;
}
h4 {
padding: 15px 0;
background: lighten($ui-base-color, 13%);
font-weight: 500;
font-size: 16px;
text-align: center;
border-radius: 8px 8px 0 0;
}
.drawer__pager {
height: 50vh;
}
.drawer__inner {
border-radius: 0 0 8px 8px;
&.backdrop {
width: calc(100% - 60px);
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
border-radius: 0 0 0 8px;
}
}
&__accounts {
overflow-y: auto;
}
.account__display-name {
&:hover strong {
text-decoration: none;
}
}
.account__avatar {
cursor: default;
}
.search {
margin-bottom: 0;
}
}
.list-adder {
background: $ui-base-color;
flex-direction: column;
border-radius: 8px;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
width: 380px;
overflow: hidden;
@media screen and (width <= 420px) {
width: 90%;
}
&__account {
background: lighten($ui-base-color, 13%);
}
&__lists {
background: lighten($ui-base-color, 13%);
height: 50vh;
border-radius: 0 0 8px 8px;
overflow-y: auto;
}
.list {
padding: 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.list__wrapper {
display: flex;
}
.list__display-name {
flex: 1 1 auto;
overflow: hidden;
text-decoration: none;
font-size: 16px;
padding: 10px;
}
}

View File

@ -0,0 +1,802 @@
.video-error-cover {
align-items: center;
background: $base-overlay-background;
color: $primary-text-color;
cursor: pointer;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
margin-top: 8px;
position: relative;
text-align: center;
z-index: 100;
}
.media-spoiler {
background: $base-overlay-background;
color: $darker-text-color;
border: 0;
width: 100%;
height: 100%;
&:hover,
&:active,
&:focus {
color: lighten($darker-text-color, 8%);
}
.status__content > & {
margin-top: 15px; // Add margin when used bare for NSFW video player
}
@include fullwidth-gallery;
}
.media-spoiler__warning {
display: block;
font-size: 14px;
}
.media-spoiler__trigger {
display: block;
font-size: 11px;
font-weight: 500;
}
.media-gallery__item__badges {
position: absolute;
bottom: 6px;
inset-inline-start: 6px;
display: flex;
gap: 2px;
}
.media-gallery__alt__label,
.media-gallery__gifv__label {
display: flex;
align-items: center;
justify-content: center;
color: $white;
background: rgba($black, 0.65);
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
z-index: 1;
pointer-events: none;
line-height: 18px;
.icon {
width: 15px;
height: 15px;
}
}
.media-gallery {
box-sizing: border-box;
margin-top: 8px;
overflow: hidden;
border-radius: 4px;
position: relative;
width: 100%;
min-height: 64px;
display: grid;
grid-template-columns: 50% 50%;
grid-template-rows: 50% 50%;
gap: 2px;
@include fullwidth-gallery;
}
.media-gallery__item {
border: 0;
box-sizing: border-box;
display: block;
position: relative;
border-radius: 4px;
overflow: hidden;
&--tall {
grid-row: span 2;
}
&--wide {
grid-column: span 2;
}
.full-width & {
border-radius: 0;
}
&.letterbox {
background: $base-shadow-color;
}
}
.media-gallery__item-thumbnail {
cursor: zoom-in;
display: block;
text-decoration: none;
color: $secondary-text-color;
position: relative;
z-index: 1;
&,
img {
height: 100%;
width: 100%;
object-fit: contain;
&:not(.letterbox) {
height: 100%;
object-fit: cover;
}
}
}
.media-gallery__preview {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
inset-inline-start: 0;
z-index: 0;
background: $base-overlay-background;
&--hidden {
display: none;
}
}
.media-gallery__gifv {
height: 100%;
overflow: hidden;
position: relative;
width: 100%;
display: flex;
justify-content: center;
}
.media-gallery__item-gifv-thumbnail {
cursor: zoom-in;
height: 100%;
width: 100%;
object-fit: contain;
user-select: none;
&:not(.letterbox) {
height: 100%;
object-fit: cover;
}
}
.media-gallery__item-thumbnail-label {
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
overflow: hidden;
position: absolute;
}
.video-modal__container {
max-width: 100vw;
max-height: 100vh;
}
.audio-modal__container {
width: 50vw;
}
.media-modal {
width: 100%;
height: 100%;
position: relative;
&__close,
&__zoom-button {
color: rgba($white, 0.7);
&:hover,
&:focus,
&:active {
color: $white;
background-color: rgba($white, 0.15);
}
&:focus {
background-color: rgba($white, 0.3);
}
}
}
.media-modal__closer {
position: absolute;
top: 0;
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
}
.media-modal__navigation {
position: absolute;
top: 0;
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
pointer-events: none;
transition: opacity 0.3s linear;
will-change: opacity;
* {
pointer-events: auto;
}
&.media-modal__navigation--hidden {
opacity: 0;
* {
pointer-events: none;
}
}
}
.media-modal__nav {
background: transparent;
box-sizing: border-box;
border: 0;
color: rgba($white, 0.7);
cursor: pointer;
display: flex;
align-items: center;
font-size: 24px;
height: 20vmax;
margin: auto 0;
padding: 30px 15px;
position: absolute;
top: 0;
bottom: 0;
&:hover,
&:focus,
&:active {
color: $white;
}
}
.media-modal__nav--left {
inset-inline-start: 0;
}
.media-modal__nav--right {
inset-inline-end: 0;
}
.media-modal__overlay {
max-width: 600px;
position: absolute;
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
margin: 0 auto;
.picture-in-picture__footer {
border-radius: 0;
background: transparent;
padding: 20px 0;
.icon-button {
color: $white;
&:hover,
&:focus,
&:active {
color: $white;
background-color: rgba($white, 0.15);
}
&:focus {
background-color: rgba($white, 0.3);
}
&.active {
color: $highlight-text-color;
&:hover,
&:focus,
&:active {
background: rgba($highlight-text-color, 0.15);
}
&:focus {
background: rgba($highlight-text-color, 0.3);
}
}
&.star-icon.active {
color: $gold-star;
&:hover,
&:focus,
&:active {
background: rgba($gold-star, 0.15);
}
&:focus {
background: rgba($gold-star, 0.3);
}
}
&.disabled {
color: $white;
background-color: transparent;
cursor: default;
opacity: 0.4;
}
}
}
}
.media-modal__pagination {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.media-modal__page-dot {
flex: 0 0 auto;
background-color: $white;
opacity: 0.4;
height: 6px;
width: 6px;
border-radius: 50%;
margin: 0 4px;
padding: 0;
border: 0;
font-size: 0;
transition: opacity 0.2s ease-in-out;
&.active {
opacity: 1;
}
}
.media-modal__close {
position: absolute;
inset-inline-end: 8px;
top: 8px;
z-index: 100;
}
.detailed,
.fullscreen {
.video-player__volume__current,
.video-player__volume::before {
bottom: 27px;
}
.video-player__volume__handle {
bottom: 23px;
}
}
.audio-player {
overflow: hidden;
box-sizing: border-box;
position: relative;
background: darken($ui-base-color, 8%);
border-radius: 4px;
padding-bottom: 44px;
width: 100%;
&.editable {
border-radius: 0;
height: 100%;
}
&.inactive {
audio,
.video-player__controls {
visibility: hidden;
}
}
.video-player__volume::before,
.video-player__seek::before {
background: currentColor;
opacity: 0.15;
}
.video-player__seek__buffer {
background: currentColor;
opacity: 0.2;
}
.video-player__buttons button,
.video-player__buttons a {
color: currentColor;
opacity: 0.75;
&:active,
&:hover,
&:focus {
color: currentColor;
opacity: 1;
}
}
.video-player__time-sep,
.video-player__time-total,
.video-player__time-current {
color: currentColor;
}
.video-player__seek::before,
.video-player__seek__buffer,
.video-player__seek__progress {
top: 0;
}
.video-player__seek__handle {
top: -4px;
}
.video-player__controls {
padding-top: 10px;
background: transparent;
}
}
.video-player {
overflow: hidden;
position: relative;
background: $base-shadow-color;
max-width: 100%;
border-radius: 4px;
box-sizing: border-box;
color: $white;
display: flex;
align-items: center;
&.editable {
border-radius: 0;
height: 100% !important;
}
&:focus {
outline: 0;
}
.detailed-status & {
width: 100%;
height: 100%;
}
@include fullwidth-gallery;
video {
display: block;
max-width: 100vw;
max-height: 80vh;
z-index: 1;
position: relative;
}
&.fullscreen {
width: 100% !important;
height: 100% !important;
margin: 0;
video {
max-width: 100% !important;
max-height: 100% !important;
width: 100% !important;
height: 100% !important;
outline: 0;
}
}
&.inline {
video {
object-fit: contain;
}
}
&__controls {
position: absolute;
direction: ltr;
z-index: 2;
bottom: 0;
inset-inline-start: 0;
inset-inline-end: 0;
box-sizing: border-box;
background: linear-gradient(
0deg,
rgba($base-shadow-color, 0.85) 0,
rgba($base-shadow-color, 0.45) 60%,
transparent
);
padding: 0 15px;
opacity: 0;
transition: opacity 0.1s ease;
&.active {
opacity: 1;
}
}
&.inactive {
video,
.video-player__controls {
visibility: hidden;
}
}
&__spoiler {
display: none;
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
height: 100%;
z-index: 4;
border: 0;
background: $base-shadow-color;
color: $darker-text-color;
transition: none;
pointer-events: none;
&.active {
display: block;
pointer-events: auto;
&:hover,
&:active,
&:focus {
color: lighten($darker-text-color, 7%);
}
}
&__title {
display: block;
font-size: 14px;
}
&__subtitle {
display: block;
font-size: 11px;
font-weight: 500;
}
}
&__buttons-bar {
display: flex;
justify-content: space-between;
padding-bottom: 8px;
margin: 0 -5px;
.video-player__download__icon {
color: inherit;
.fa,
&:active .fa,
&:hover .fa,
&:focus .fa {
color: inherit;
}
}
}
&__buttons {
display: flex;
flex: 0 1 auto;
min-width: 30px;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
gap: 5px;
.player-button {
display: inline-block;
outline: 0;
padding: 5px;
flex: 0 0 auto;
background: transparent;
border: 0;
color: rgba($white, 0.75);
&:active,
&:hover,
&:focus {
color: $white;
}
}
}
&__time {
display: inline;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
margin: 0 5px;
}
&__time-sep,
&__time-total,
&__time-current {
font-size: 14px;
font-weight: 500;
}
&__time-current {
color: $white;
}
&__time-sep {
display: inline-block;
margin: 0 6px;
}
&__time-sep,
&__time-total {
color: $white;
}
&__volume {
flex: 0 0 auto;
display: inline-flex;
cursor: pointer;
height: 24px;
position: relative;
overflow: hidden;
.no-reduce-motion & {
transition: all 100ms linear;
}
&.active {
overflow: visible;
width: 50px;
margin-inline-end: 16px;
}
&::before {
content: '';
width: 50px;
background: rgba($white, 0.35);
border-radius: 4px;
display: block;
position: absolute;
height: 4px;
inset-inline-start: 0;
top: 50%;
transform: translate(0, -50%);
}
&__current {
display: block;
position: absolute;
height: 4px;
border-radius: 4px;
inset-inline-start: 0;
top: 50%;
transform: translate(0, -50%);
background: lighten($ui-highlight-color, 8%);
}
&__handle {
position: absolute;
z-index: 3;
border-radius: 50%;
width: 12px;
height: 12px;
top: 50%;
inset-inline-start: 0;
margin-inline-start: -6px;
transform: translate(0, -50%);
background: lighten($ui-highlight-color, 8%);
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
opacity: 0;
.no-reduce-motion & {
transition: opacity 100ms linear;
}
}
&.active &__handle {
opacity: 1;
}
}
&__link {
padding: 2px 10px;
a {
text-decoration: none;
font-size: 14px;
font-weight: 500;
color: $white;
&:hover,
&:active,
&:focus {
text-decoration: underline;
}
}
}
&__seek {
cursor: pointer;
height: 24px;
position: relative;
&::before {
content: '';
width: 100%;
background: rgba($white, 0.35);
border-radius: 4px;
display: block;
position: absolute;
height: 4px;
top: 14px;
}
&__progress,
&__buffer {
display: block;
position: absolute;
height: 4px;
border-radius: 4px;
top: 14px;
background: lighten($ui-highlight-color, 8%);
}
&__buffer {
background: rgba($white, 0.2);
}
&__handle {
position: absolute;
z-index: 3;
opacity: 0;
border-radius: 50%;
width: 12px;
height: 12px;
top: 10px;
margin-inline-start: -6px;
background: lighten($ui-highlight-color, 8%);
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
.no-reduce-motion & {
transition: opacity 0.1s ease;
}
&.active {
opacity: 1;
}
}
&:hover {
.video-player__seek__handle {
opacity: 1;
}
}
}
&.detailed,
&.fullscreen {
.video-player__buttons {
.player-button {
padding-top: 10px;
padding-bottom: 10px;
}
}
}
}
.gifv {
video {
max-width: 100vw;
max-height: 80vh;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,209 @@
.privacy-policy {
background: $ui-base-color;
padding: 20px;
@media screen and (min-width: $no-gap-breakpoint) {
border-radius: 4px;
}
&__body {
margin-top: 20px;
}
}
.prose {
color: $secondary-text-color;
font-size: 15px;
line-height: 22px;
p,
ul,
ol {
margin-top: 1.25em;
margin-bottom: 1.25em;
}
img {
margin-top: 2em;
margin-bottom: 2em;
max-width: 100%;
}
video {
margin-top: 2em;
margin-bottom: 2em;
max-width: 100%;
}
figure {
margin-top: 2em;
margin-bottom: 2em;
figcaption {
font-size: 0.875em;
line-height: 1.4285714;
margin-top: 0.8571429em;
}
}
figure > * {
margin-top: 0;
margin-bottom: 0;
}
h1 {
font-size: 1.5em;
margin-top: 0;
margin-bottom: 1em;
line-height: 1.33;
}
h2 {
font-size: 1.25em;
margin-top: 1.6em;
margin-bottom: 0.6em;
line-height: 1.6;
}
h3,
h4,
h5,
h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
line-height: 1.5;
}
ol {
counter-reset: list-counter;
}
li {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
ol > li {
counter-increment: list-counter;
&::before {
content: counter(list-counter) '.';
position: absolute;
inset-inline-start: 0;
}
}
ul > li::before {
content: '';
position: absolute;
background-color: $darker-text-color;
border-radius: 50%;
width: 0.375em;
height: 0.375em;
top: 0.5em;
inset-inline-start: 0.25em;
}
ul > li,
ol > li {
position: relative;
padding-inline-start: 1.75em;
}
& > ul > li p {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
& > ul > li > *:first-child {
margin-top: 1.25em;
}
& > ul > li > *:last-child {
margin-bottom: 1.25em;
}
& > ol > li > *:first-child {
margin-top: 1.25em;
}
& > ol > li > *:last-child {
margin-bottom: 1.25em;
}
ul ul,
ul ol,
ol ul,
ol ol {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
h1,
h2,
h3,
h4,
h5,
h6,
strong,
b {
color: $primary-text-color;
font-weight: 700;
}
em,
i {
font-style: italic;
}
a {
color: $highlight-text-color;
text-decoration: underline;
&:focus,
&:hover,
&:active {
text-decoration: none;
}
}
code {
font-size: 0.875em;
background: darken($ui-base-color, 8%);
border-radius: 4px;
padding: 0.2em 0.3em;
}
hr {
border: 0;
border-top: 1px solid lighten($ui-base-color, 4%);
margin-top: 3em;
margin-bottom: 3em;
}
hr + * {
margin-top: 0;
}
h2 + * {
margin-top: 0;
}
h3 + * {
margin-top: 0;
}
h4 + *,
h5 + *,
h6 + * {
margin-top: 0;
}
& > :first-child {
margin-top: 0;
}
& > :last-child {
margin-bottom: 0;
}
}

View File

@ -0,0 +1,43 @@
.regeneration-indicator {
text-align: center;
font-size: 16px;
font-weight: 500;
color: $dark-text-color;
background: $ui-base-color;
cursor: default;
display: flex;
flex: 1 1 auto;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
&__figure {
&,
img {
display: block;
width: auto;
height: 160px;
margin: 0;
}
}
&--without-header {
padding-top: 20px + 48px;
}
&__label {
margin-top: 30px;
strong {
display: block;
margin-bottom: 10px;
color: $dark-text-color;
}
span {
font-size: 15px;
font-weight: 400;
}
}
}

View File

@ -0,0 +1,329 @@
.search {
margin-bottom: 10px;
position: relative;
&__popout {
box-sizing: border-box;
display: none;
position: absolute;
inset-inline-start: 0;
margin-top: -2px;
width: 100%;
background: $ui-base-color;
border-radius: 0 0 4px 4px;
box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
z-index: 99;
font-size: 13px;
padding: 15px 5px;
h4 {
text-transform: uppercase;
color: $dark-text-color;
font-weight: 500;
padding: 0 10px;
margin-bottom: 10px;
}
.icon-button {
padding: 0;
}
.icon {
width: 18px;
height: 18px;
}
&__menu {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
&__message {
color: $dark-text-color;
padding: 0 10px;
}
&__item {
display: block;
box-sizing: border-box;
width: 100%;
border: 0;
font: inherit;
background: transparent;
color: $darker-text-color;
padding: 10px;
cursor: pointer;
border-radius: 4px;
text-align: start;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
&--flex {
display: flex;
justify-content: space-between;
}
.icon-button {
transition: none;
}
&:hover,
&:focus,
&:active,
&.selected {
background: $ui-highlight-color;
color: $primary-text-color;
.icon-button {
color: $primary-text-color;
}
}
mark {
background: transparent;
font-weight: 700;
color: $primary-text-color;
}
span {
overflow: inherit;
text-overflow: inherit;
}
}
}
}
&.active {
.search__popout {
display: block;
}
}
}
.search__input {
@include search-input;
display: block;
padding: 15px;
padding-inline-end: 30px;
line-height: 18px;
font-size: 16px;
&::placeholder {
color: lighten($darker-text-color, 4%);
}
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
}
.search__icon {
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus {
outline: 0 !important;
}
.icon {
position: absolute;
top: 13px;
inset-inline-end: 10px;
display: inline-block;
opacity: 0;
transition: all 100ms linear;
transition-property: color, transform, opacity;
font-size: 18px;
width: 24px;
height: 24px;
color: $secondary-text-color;
cursor: default;
pointer-events: none;
&.active {
pointer-events: auto;
opacity: 0.3;
}
}
.icon-search {
transform: rotate(0deg);
&.active {
pointer-events: auto;
opacity: 0.3;
}
}
.icon-times-circle {
top: 17px;
transform: rotate(0deg);
color: $action-button-color;
cursor: pointer;
&.active {
transform: rotate(90deg);
opacity: 1;
}
&:hover {
color: lighten($action-button-color, 7%);
}
}
}
.search-results__header {
color: $dark-text-color;
background: lighten($ui-base-color, 2%);
padding: 15px;
font-weight: 500;
font-size: 16px;
cursor: default;
display: flex;
align-items: center;
gap: 5px;
}
.search-results__info {
padding: 20px;
color: $darker-text-color;
text-align: center;
}
.trends {
&__item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
gap: 15px;
&:last-child {
border-bottom: 0;
}
&__name {
flex: 1 1 auto;
color: $dark-text-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
strong {
font-weight: 500;
}
a {
color: $darker-text-color;
text-decoration: none;
font-size: 14px;
font-weight: 500;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover,
&:focus,
&:active {
span {
text-decoration: underline;
}
}
}
}
&__current {
flex: 0 0 auto;
font-size: 24px;
font-weight: 500;
text-align: end;
color: $secondary-text-color;
text-decoration: none;
}
&__sparkline {
flex: 0 0 auto;
width: 50px;
path:first-child {
fill: rgba($highlight-text-color, 0.25) !important;
fill-opacity: 1 !important;
}
path:last-child {
stroke: lighten($highlight-text-color, 6%) !important;
fill: none !important;
}
}
&--requires-review {
.trends__item__name {
color: $gold-star;
a {
color: $gold-star;
}
}
.trends__item__current {
color: $gold-star;
}
.trends__item__sparkline {
path:first-child {
fill: rgba($gold-star, 0.25) !important;
}
path:last-child {
stroke: lighten($gold-star, 6%) !important;
}
}
}
&--disabled {
.trends__item__name {
color: lighten($ui-base-color, 12%);
a {
color: lighten($ui-base-color, 12%);
}
}
.trends__item__current {
color: lighten($ui-base-color, 12%);
}
.trends__item__sparkline {
path:first-child {
fill: rgba(lighten($ui-base-color, 12%), 0.25) !important;
}
path:last-child {
stroke: lighten(lighten($ui-base-color, 12%), 6%) !important;
}
}
}
}
&--compact &__item {
padding: 10px;
padding-inline-end: 28px;
}
}

View File

@ -0,0 +1,26 @@
.sensitive-info {
display: flex;
flex-direction: row;
align-items: center;
position: absolute;
top: 4px;
inset-inline-start: 4px;
z-index: 100;
}
.sensitive-marker {
margin: 0 3px;
border-radius: 2px;
padding: 2px 6px;
color: rgba($primary-text-color, 0.8);
background: rgba($base-overlay-background, 0.5);
font-size: 12px;
line-height: 18px;
text-transform: uppercase;
opacity: 0.9;
transition: opacity 0.1s ease;
.media-gallery:hover & {
opacity: 1;
}
}

View File

@ -0,0 +1,106 @@
.sign-in-banner {
padding: 10px;
p {
color: $darker-text-color;
margin-bottom: 20px;
a {
color: $secondary-text-color;
text-decoration: none;
unicode-bidi: isolate;
&:hover {
text-decoration: underline;
}
}
}
.button {
margin-bottom: 10px;
}
}
.server-banner {
padding: 20px 0;
&__introduction {
color: $darker-text-color;
margin-bottom: 20px;
strong {
font-weight: 600;
}
a {
color: inherit;
text-decoration: underline;
&:hover,
&:active,
&:focus {
text-decoration: none;
}
}
}
&__hero {
display: block;
border-radius: 4px;
width: 100%;
height: auto;
margin-bottom: 20px;
aspect-ratio: 1.9;
border: 0;
background: $ui-base-color;
object-fit: cover;
}
&__description {
margin-bottom: 20px;
}
&__meta {
display: flex;
gap: 10px;
max-width: 100%;
&__column {
flex: 0 0 auto;
width: calc(50% - 5px);
overflow: hidden;
}
}
&__number {
font-weight: 600;
color: $primary-text-color;
font-size: 14px;
}
&__number-label {
color: $darker-text-color;
font-weight: 500;
font-size: 14px;
}
h4 {
text-transform: uppercase;
color: $darker-text-color;
margin-bottom: 10px;
font-weight: 600;
}
.account {
padding: 0;
border: 0;
}
.account__avatar-wrapper {
margin-inline-start: 0;
}
.spacer {
margin: 10px 0;
}
}

View File

@ -0,0 +1,319 @@
.compose-panel {
width: 285px;
margin-top: 10px;
display: flex;
flex-direction: column;
height: calc(100% - 10px);
overflow-y: hidden;
.hero-widget {
box-shadow: none;
&__text,
&__img,
&__img img {
border-radius: 0;
}
&__text {
padding: 15px;
color: $secondary-text-color;
strong {
font-weight: 700;
color: $primary-text-color;
}
}
}
.search__input {
line-height: 18px;
font-size: 16px;
padding: 15px;
padding-inline-end: 30px;
}
.search__icon .fa {
top: 15px;
}
.navigation-bar {
flex: 0 1 48px;
}
.compose-form {
flex: 1;
display: flex;
flex-direction: column;
min-height: 310px;
}
.compose-form__autosuggest-wrapper {
overflow-y: auto;
background-color: $white;
border-radius: 4px 4px 0 0;
flex: 0 1 auto;
}
.autosuggest-textarea__textarea {
overflow-y: hidden;
}
}
.navigation-panel {
margin-top: 10px;
margin-bottom: 10px;
height: calc(100% - 20px);
overflow-y: auto;
display: flex;
flex-direction: column;
& > a {
flex: 0 0 auto;
}
.logo {
height: 30px;
width: auto;
}
}
.navigation-panel,
.compose-panel {
hr {
flex: 0 0 auto;
border: 0;
background: transparent;
border-top: 1px solid lighten($ui-base-color, 4%);
margin: 10px 0;
}
.flex-spacer {
background: transparent;
}
}
.columns-area--mobile {
flex-direction: column;
width: 100%;
margin: 0 auto;
.column,
.drawer {
width: 100%;
height: 100%;
padding: 0;
}
.account-card {
margin-bottom: 0;
}
.filter-form {
display: flex;
flex-wrap: wrap;
}
.autosuggest-textarea__textarea {
font-size: 16px;
}
.search__input {
line-height: 18px;
font-size: 16px;
padding: 15px;
padding-inline-end: 30px;
}
.search__icon .fa {
top: 15px;
}
.scrollable {
overflow: visible;
@supports (display: grid) {
contain: content;
}
}
@media screen and (min-width: $no-gap-breakpoint) {
padding: 10px 0;
padding-top: 0;
}
.detailed-status {
padding: 15px;
.media-gallery,
.video-player,
.audio-player {
margin-top: 15px;
}
}
.account__header__bar {
padding: 5px 10px;
}
.navigation-bar,
.compose-form {
padding: 15px;
}
.compose-form .compose-form__publish .compose-form__publish-button-wrapper {
padding-top: 15px;
}
.notification__report {
padding: 15px;
padding-inline-start: (48px + 15px * 2);
min-height: 48px + 2px;
&__avatar {
inset-inline-start: 15px;
top: 17px;
}
}
.status {
padding: 15px;
min-height: 48px + 2px;
.media-gallery,
&__action-bar,
.video-player,
.audio-player {
margin-top: 10px;
}
}
.account {
padding: 15px 10px;
&__header__bio {
margin: 0 -10px;
}
}
.notification {
&__message {
padding-top: 15px;
}
.status {
padding-top: 8px;
}
.account {
padding-top: 8px;
}
}
}
@media screen and (min-width: $no-gap-breakpoint) {
.react-swipeable-view-container .columns-area--mobile {
height: calc(100% - 10px) !important;
}
.getting-started__wrapper {
margin-bottom: 10px;
}
.search-page .search {
display: none;
}
.navigation-panel__legal {
display: none;
}
}
@media screen and (max-width: $no-gap-breakpoint - 1px) {
$sidebar-width: 285px;
.columns-area__panels__main {
width: calc(100% - $sidebar-width);
}
.columns-area__panels {
min-height: calc(100vh - $ui-header-height);
}
.columns-area__panels__pane--navigational {
min-width: $sidebar-width;
.columns-area__panels__pane__inner {
width: $sidebar-width;
}
.navigation-panel {
margin: 0;
background: $ui-base-color;
border-inline-start: 1px solid lighten($ui-base-color, 8%);
height: 100vh;
}
.navigation-panel__sign-in-banner,
.navigation-panel__logo,
.navigation-panel__banner,
.getting-started__trends {
display: none;
}
.column-link__icon {
font-size: 18px;
}
}
.layout-single-column .ui__header {
display: flex;
background: $ui-base-color;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.column-header,
.column-back-button,
.scrollable,
.error-column {
border-radius: 0 !important;
}
}
@media screen and (max-width: $no-gap-breakpoint - 285px - 1px) {
$sidebar-width: 55px;
.columns-area__panels__main {
width: calc(100% - $sidebar-width);
}
.columns-area__panels__pane--navigational {
min-width: $sidebar-width;
.columns-area__panels__pane__inner {
width: $sidebar-width;
}
.column-link span {
display: none;
}
.list-panel {
display: none;
}
}
}
.explore__search-header {
display: none;
}
@media screen and (max-width: $no-gap-breakpoint - 1px) {
.columns-area__panels__pane--compositional {
display: none;
}
.explore__search-header {
display: flex;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -13,9 +13,8 @@
@import 'forms';
@import 'accounts';
@import 'statuses';
@import 'components';
@import 'components/index';
@import 'polls';
@import 'emoji_picker';
@import 'about';
@import 'tables';
@import 'admin';
@ -23,5 +22,3 @@
@import 'rtl';
@import 'dashboard';
@import 'rich_text';
@import 'glitch_local_settings';
@import 'glitch_doodle';

View File

@ -1,7 +1,5 @@
.status__quote,
.status__content__text,
.e-content,
.quote-indicator__content,
.reply-indicator__content {
pre,
blockquote {
@ -93,7 +91,6 @@
}
}
.quote-indicator__content,
.reply-indicator__content {
blockquote {
border-inline-start-color: $inverted-text-color;

View File

@ -44,6 +44,8 @@ const notFoundFn = () => (
set='twitter'
size={32}
sheetSize={32}
sheetColumns={60}
sheetRows={60}
backgroundImageFn={backgroundImageFn}
/>
@ -102,12 +104,12 @@ class ModifierPickerMenu extends PureComponent {
return (
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={1} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={2} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={3} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={4} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={5} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={6} backgroundImageFn={backgroundImageFn} /></button>
</div>
);
}
@ -142,7 +144,7 @@ class ModifierPicker extends PureComponent {
return (
<div className='emoji-picker-dropdown__modifiers'>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
</div>
);
@ -279,6 +281,8 @@ class EmojiPickerMenuImpl extends PureComponent {
perLine={8}
emojiSize={22}
sheetSize={32}
sheetColumns={60}
sheetRows={60}
custom={buildCustomEmojis(custom_emojis)}
color=''
emoji=''

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M346-140 100-386q-10-10-15-22t-5-25q0-13 5-25t15-22l230-229-106-106 62-65 400 400q10 10 14.5 22t4.5 25q0 13-4.5 25T686-386L440-140q-10 10-22 15t-25 5q-13 0-25-5t-22-15Zm47-506L179-432h428L393-646Zm399 526q-36 0-61-25.5T706-208q0-27 13.5-51t30.5-47l42-54 44 54q16 23 30 47t14 51q0 37-26 62.5T792-120Z"/></svg>

Before

Width:  |  Height:  |  Size: 405 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M346-140 100-386q-10-10-15-22t-5-25q0-13 5-25t15-22l230-229-106-106 62-65 400 400q10 10 14.5 22t4.5 25q0 13-4.5 25T686-386L440-140q-10 10-22 15t-25 5q-13 0-25-5t-22-15Zm47-506L179-432h428L393-646Zm399 526q-36 0-61-25.5T706-208q0-27 13.5-51t30.5-47l42-54 44 54q16 23 30 47t14 51q0 37-26 62.5T792-120Z"/></svg>

Before

Width:  |  Height:  |  Size: 405 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M280-200v-80h284q63 0 109.5-40T720-420q0-60-46.5-100T564-560H312l104 104-56 56-200-200 200-200 56 56-104 104h252q97 0 166.5 63T800-420q0 94-69.5 157T564-200H280Z"/></svg>

Before

Width:  |  Height:  |  Size: 267 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M280-200v-80h284q63 0 109.5-40T720-420q0-60-46.5-100T564-560H312l104 104-56 56-200-200 200-200 56 56-104 104h252q97 0 166.5 63T800-420q0 94-69.5 157T564-200H280Z"/></svg>

Before

Width:  |  Height:  |  Size: 267 B

View File

@ -86,7 +86,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@status = Status.create!(@params)
attach_tags(@status)
end
return if Treehouse::Automod.process_status!(@status)
resolve_thread(@status)
fetch_replies(@status)

View File

@ -4,34 +4,14 @@ module ApplicationExtension
extend ActiveSupport::Concern
included do
include Redisable
has_many :created_users, class_name: 'User', foreign_key: 'created_by_application_id', inverse_of: :created_by_application
validates :name, length: { maximum: 60 }
validates :website, url: true, length: { maximum: 2_000 }, if: :website?
validates :redirect_uri, length: { maximum: 2_000 }
# The relationship used between Applications and AccessTokens is using
# dependent: delete_all, which means the ActiveRecord callback in
# AccessTokenExtension is not run, so instead we manually announce to
# streaming that these tokens are being deleted.
before_destroy :push_to_streaming_api, prepend: true
end
def confirmation_redirect_uri
redirect_uri.lines.first.strip
end
def push_to_streaming_api
# TODO: #28793 Combine into a single topic
payload = Oj.dump(event: :kill)
access_tokens.in_batches do |tokens|
redis.pipelined do |pipeline|
tokens.ids.each do |id|
pipeline.publish("timeline:access_token:#{id}", payload)
end
end
end
end
end

View File

@ -261,28 +261,9 @@ class Request
begin
addresses = [IPAddr.new(host)]
rescue IPAddr::InvalidAddressError
begin
Resolv::DNS.open do |dns|
dns.timeouts = 5
addresses = dns.getaddresses(host)
addresses = addresses.filter { |addr| addr.is_a?(Resolv::IPv6) }.take(2) + addresses.filter { |addr| !addr.is_a?(Resolv::IPv6) }.take(2)
end
rescue ResolvError
end
if addresses.empty?
# try /etc/hosts file
# https://github.com/mastodon/mastodon/issues/9436
Resolv::Hosts.new.each_address(host) do |address|
case address
when Resolv::IPv4::Regex
addresses << Resolv::IPv4.create(address)
when Resolv::IPv6::Regex
addresses << Resolv::IPv6.create(address)
else
raise ResolvError.new("cannot interpret as address: #{address}")
end
end
Resolv::DNS.open do |dns|
dns.timeouts = 5
addresses = dns.getaddresses(host)
addresses = addresses.filter { |addr| addr.is_a?(Resolv::IPv6) }.take(2) + addresses.filter { |addr| !addr.is_a?(Resolv::IPv6) }.take(2)
end
end

View File

@ -25,15 +25,7 @@ module User::LdapAuthenticable
resource = joins(:account).find_by(accounts: { username: safe_username })
if resource.blank?
resource = new(
email: attributes[Devise.ldap_mail.to_sym].first,
agreement: true,
account_attributes: {
username: safe_username,
},
external: true,
confirmed_at: Time.now.utc
)
resource = new(email: attributes[Devise.ldap_mail.to_sym].first, agreement: true, account_attributes: { username: safe_username }, admin: false, external: true, confirmed_at: Time.now.utc)
resource.save!
end

View File

@ -19,18 +19,17 @@ module User::Omniauthable
end
class_methods do
def find_for_omniauth(auth, signed_in_resource = nil)
def find_for_oauth(auth, signed_in_resource = nil)
# EOLE-SSO Patch
auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array
identity = Identity.find_for_omniauth(auth)
identity = Identity.find_for_oauth(auth)
# If a signed_in_resource is provided it always overrides the existing user
# to prevent the identity being locked with accidentally created accounts.
# Note that this may leave zombie accounts (with no associated identity) which
# can be cleaned up at a later date.
user = signed_in_resource || identity.user
user ||= reattach_for_auth(auth)
user ||= create_for_auth(auth)
user ||= create_for_oauth(auth)
if identity.user.nil?
identity.user = user
@ -40,35 +39,19 @@ module User::Omniauthable
user
end
private
def reattach_for_auth(auth)
# If allowed, check if a user exists with the provided email address,
# and return it if they does not have an associated identity with the
# current authentication provider.
# This can be used to provide a choice of alternative auth providers
# or provide smooth gradual transition between multiple auth providers,
# but this is discouraged because any insecure provider will put *all*
# local users at risk, regardless of which provider they registered with.
return unless ENV['ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH'] == 'true'
email, email_is_verified = email_from_auth(auth)
return unless email_is_verified
user = User.find_by(email: email)
return if user.nil? || Identity.exists?(provider: auth.provider, user_id: user.id)
user
end
def create_for_auth(auth)
# Create a user for the given auth params. If no email was provided,
def create_for_oauth(auth)
# Check if the user exists with provided email. If no email was provided,
# we assign a temporary email and ask the user to verify it on
# the next step via Auth::SetupController.show
email, email_is_verified = email_from_auth(auth)
strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy
assume_verified = strategy&.security&.assume_email_is_verified
email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified
email = auth.info.verified_email || auth.info.email
user = User.find_by(email: email) if email_is_verified
return user unless user.nil?
user = User.new(user_params_from_auth(email, auth))
@ -83,14 +66,7 @@ module User::Omniauthable
user
end
def email_from_auth(auth)
strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy
assume_verified = strategy&.security&.assume_email_is_verified
email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified
email = auth.info.verified_email || auth.info.email
[email, email_is_verified]
end
private
def user_params_from_auth(email, auth)
{

View File

@ -32,6 +32,7 @@ module User::PamAuthenticable
self.email = "#{account.username}@#{find_pam_suffix}" if email.nil? && find_pam_suffix
self.confirmed_at = Time.now.utc
self.admin = false
self.account = account
self.external = true

View File

@ -17,7 +17,7 @@ class Identity < ApplicationRecord
validates :uid, presence: true, uniqueness: { scope: :provider }
validates :provider, presence: true
def self.find_for_omniauth(auth)
def self.find_for_oauth(auth)
find_or_create_by(uid: auth.uid, provider: auth.provider)
end
end

View File

@ -56,7 +56,7 @@ class Invite < ApplicationRecord
end
def created_by_moderator?
self.user.can?(:manage_invites)
self.user.moderator
end
def th_use_invite_quota?

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class TagFeed < PublicFeed
LIMIT_PER_MODE = (ENV['MAX_FEED_HASHTAGS'] || 4).to_i
LIMIT_PER_MODE = 4
# @param [Tag] tag
# @param [Account] account

View File

@ -51,8 +51,6 @@ class User < ApplicationRecord
last_sign_in_ip
skip_sign_in_token
filtered_languages
admin
moderator
)
include LanguagesHelper
@ -344,16 +342,6 @@ class User < ApplicationRecord
Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch|
batch.update_all(revoked_at: Time.now.utc)
Web::PushSubscription.where(access_token_id: batch).delete_all
# Revoke each access token for the Streaming API, since `update_all``
# doesn't trigger ActiveRecord Callbacks:
# TODO: #28793 Combine into a single topic
payload = Oj.dump(event: :kill)
redis.pipelined do |pipeline|
batch.ids.each do |id|
pipeline.publish("timeline:access_token:#{id}", payload)
end
end
end
end

View File

@ -5,7 +5,7 @@ class InitialStateSerializer < ActiveModel::Serializer
attributes :meta, :compose, :accounts,
:media_attachments, :settings,
:max_toot_chars, :max_feed_hashtags, :poll_limits,
:max_toot_chars, :poll_limits,
:languages
attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? }
@ -17,10 +17,6 @@ class InitialStateSerializer < ActiveModel::Serializer
StatusLengthValidator::MAX_CHARS
end
def max_feed_hashtags
TagFeed::LIMIT_PER_MODE
end
def poll_limits
{
max_options: PollValidator::MAX_OPTIONS,

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
require 'digest'
class ActivityPub::ProcessAccountService < BaseService
include JsonLdHelper
include DomainControlHelper
@ -92,9 +90,6 @@ class ActivityPub::ProcessAccountService < BaseService
set_immediate_protocol_attributes!
set_fetchable_key! unless @account.suspended? && @account.suspension_origin_local?
set_immediate_attributes! unless @account.suspended?
Treehouse::Automod.process_account!(@account)
set_fetchable_attributes! unless @options[:only_key] || @account.suspended?
@account.save_with_optional_media!

View File

@ -44,7 +44,7 @@ class FetchResourceService < BaseService
@response_code = response.code
return nil if response.code != 200
if valid_activitypub_content_type?(response)
if ['application/activity+json', 'application/ld+json'].include?(response.mime_type)
body = response.body_with_limit
json = body_to_json(body)

View File

@ -40,7 +40,6 @@ class ReportService < BaseService
end
def notify_staff!
return if @options[:th_skip_notify_staff]
return if @report.unresolved_siblings?
User.those_who_can(:manage_reports).includes(:account).find_each do |u|
@ -66,7 +65,6 @@ class ReportService < BaseService
end
def forward?
return false if @options[:th_skip_forward]
!@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
end

View File

@ -8,7 +8,9 @@
%td.email-inner-card-td.email-prose
%p= t @resource.approved? ? 'devise.mailer.confirmation_instructions.explanation' : 'devise.mailer.confirmation_instructions.explanation_when_pending', host: site_hostname
- if @resource.created_by_application
= render 'application/mailer/button', text: t('devise.mailer.confirmation_instructions.action_with_app', app: @resource.created_by_application.name), url: confirmation_url(@resource, confirmation_token: @token, redirect_to_app: 'true')
= render 'application/mailer/button', text: t('settings.account_settings'), url: edit_user_registration_url
= link_to confirmation_url(@resource, confirmation_token: @token, redirect_to_app: 'true') do
%span= t 'devise.mailer.confirmation_instructions.action_with_app', app: @resource.created_by_application.name
- else
= render 'application/mailer/button', text: t('devise.mailer.confirmation_instructions.action'), url: confirmation_url(@resource, confirmation_token: @token)
%p= t 'devise.mailer.confirmation_instructions.extra_html', terms_path: about_more_url, policy_path: privacy_policy_url

View File

@ -52,8 +52,6 @@ require_relative '../lib/active_record/batches'
require_relative '../lib/simple_navigation/item_extensions'
require_relative '../lib/http_extensions'
require_relative '../lib/treehouse/automod'
Dotenv::Railtie.load
Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true'
@ -109,15 +107,5 @@ module Mastodon
Devise::FailureApp.include AbstractController::Callbacks
Devise::FailureApp.include Localized
end
config.x.th_automod.automod_account_username = ENV['TH_STAFF_ACCOUNT']
config.x.th_automod.account_service_heuristic_auto_suspend_active = ENV.fetch('TH_ACCOUNT_SERVICE_HEURISTIC_AUTO_SUSPEND', '') == 'that-one-spammer'
config.x.th_automod.mention_spam_heuristic_auto_limit_active = ENV.fetch('TH_MENTION_SPAM_HEURISTIC_AUTO_LIMIT_ACTIVE', '') == 'can-spam'
config.x.th_automod.mention_spam_threshold =
begin
value = ENV.fetch('TH_MENTION_SPAM_THRESHOLD', '0').to_i
value == 0 ? Float::INFINITY : value
end
config.x.th_automod.min_account_age_threshold = ENV.fetch('TH_MIN_ACCOUNT_AGE_THRESHOLD', '2').to_i.days
end
end

View File

@ -36,7 +36,7 @@ Rails.application.config.content_security_policy do |p|
p.frame_ancestors :none
p.font_src :self, assets_host
p.img_src :self, :data, :blob, *media_hosts
p.style_src :self, :unsafe_inline, assets_host
p.style_src :self, assets_host
p.media_src :self, :data, *media_hosts
p.frame_src :self, :https
p.manifest_src :self, assets_host

View File

@ -21,14 +21,9 @@ Doorkeeper.configure do
user unless user&.otp_required_for_login?
end
# Doorkeeper provides some administrative interfaces for managing OAuth
# Applications, allowing creation, edit, and deletion of applications from the
# server. At present, these administrative routes are not integrated into
# Mastodon, and as such, we've disabled them by always return a 403 forbidden
# response for them. This does not affect the ability for users to manage
# their own OAuth Applications.
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
admin_authenticator do
head 403
current_user&.admin? || redirect_to(new_user_session_url)
end
# Authorization Code expiration time (default 10 minutes).

View File

@ -26,7 +26,6 @@ Sidekiq.configure_server do |config|
'queue' => 'scheduler',
},
}
SidekiqScheduler::Scheduler.instance.reload_schedule!
end
end

View File

@ -12,7 +12,6 @@ en:
last_attempt: You have one more attempt before your account is locked.
locked: Your account is locked.
not_found_in_database: Invalid %{authentication_keys} or password.
omniauth_user_creation_failure: Error creating an account for this identity.
pending: Your account is still under review.
timeout: Your session expired. Please login again to continue.
unauthenticated: You need to login or sign up before continuing.

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'sidekiq_unique_jobs/web' if ENV['ENABLE_SIDEKIQ_UNIQUE_JOBS_UI'] == true
require 'sidekiq_unique_jobs/web'
require 'sidekiq-scheduler/web'
class RedirectWithVary < ActionDispatch::Routing::PathRedirect

View File

@ -56,7 +56,7 @@ services:
web:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.7
image: ghcr.io/mastodon/mastodon:v4.2.0
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
@ -77,7 +77,7 @@ services:
streaming:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.7
image: ghcr.io/mastodon/mastodon:v4.2.0
restart: always
env_file: .env.production
command: node ./streaming
@ -95,7 +95,7 @@ services:
sidekiq:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.7
image: ghcr.io/mastodon/mastodon:v4.2.0
restart: always
env_file: .env.production
command: bundle exec sidekiq

1
emoji_data/all.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@ module Mastodon
end
def default_prerelease
'alpha.3'
'alpha.1'
end
def prerelease
@ -44,41 +44,33 @@ module Mastodon
end
def repository
@repository ||= ENV.fetch('GIT_REPOSITORY', false) || ENV.fetch('GITHUB_REPOSITORY', false) || 'treehouse/mastodon'
ENV.fetch('GIT_REPOSITORY', false) || ENV.fetch('GITHUB_REPOSITORY', false) || 'treehouse/mastodon'
end
def source_base_url
@source_base_url ||=
begin
base = ENV['GITHUB_REPOSITORY'] ? 'https://github.com' : 'https://gitea.treehouse.systems'
ENV.fetch('SOURCE_BASE_URL', "#{base}/#{repository}")
end
base = ENV['GITHUB_REPOSITORY'] ? 'https://github.com' : 'https://gitea.treehouse.systems'
ENV.fetch('SOURCE_BASE_URL', "#{base}/#{repository}")
end
# specify git tag or commit hash here
def source_tag
@source_tag ||=
begin
tag = ENV.fetch('SOURCE_TAG', nil)
return if tag.nil? || tag.empty?
tag
end
tag = ENV.fetch('SOURCE_TAG', nil)
return if tag.nil? || tag.empty?
tag
end
def source_url
@source_url ||=
begin
if source_tag && source_base_url =~ /gitea/
suffix = if !tag[/\H/]
"commit/#{source_tag}"
else
"branch/#{source_tag}"
end
"#{source_base_url}/#{suffix}"
else
source_base_url
end
end
tag = source_tag
if tag && source_base_url =~ /gitea/
suffix = if !tag[/\H/]
"commit/#{tag}"
else
"branch/#{tag}"
end
"#{source_base_url}/#{suffix}"
else
source_base_url
end
end
def user_agent

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
namespace :sidekiq_unique_jobs do
task delete_all_locks: :environment do
digests = SidekiqUniqueJobs::Digests.new
digests.delete_by_pattern('*', count: digests.count)
expiring_digests = SidekiqUniqueJobs::ExpiringDigests.new
expiring_digests.delete_by_pattern('*', count: expiring_digests.count)
end
end

View File

@ -1,151 +0,0 @@
module Treehouse
module Automod
COMMENT_HEADER = <<~EOS
Tracking Report - automatically created by TreehouseAutomod
EOS
WARNING_TEXT = <<~EOS
Tracking Infraction - automatically created by TreehouseAutomod
EOS
def self.silence_with_tracking_report!(account, status_ids: [], explanation: "")
account.save!
self.file_tracking_report!(account, status_ids: status_ids, type: 'silence') unless account.suspension_origin == "local"
end
def self.suspend_with_tracking_report!(account, status_ids: [], explanation: "")
account.save!
self.file_tracking_report!(account, status_ids: status_ids, type: 'suspend') unless account.suspension_origin == "local"
end
def self.file_tracking_report!(target_account, status_ids: [], explanation: "", type: 'suspend')
reporter = self.staff_account
return if reporter.nil?
# status_ids is broken because of validation
report = ReportService.new.call(
reporter,
target_account,
{
status_ids: status_ids,
comment: explanation.blank? ? COMMENT_HEADER : "#{COMMENT_HEADER}\n\n#{EXPLANATION}",
th_skip_notify_staff: true,
th_skip_forward: true,
}
)
report.save!
report.assign_to_self!(reporter)
account_action = Admin::AccountAction.new(
type: type,
report_id: report.id,
target_account: target_account,
current_account: reporter,
send_email_notification: false,
text: WARNING_TEXT,
)
account_action.save!
Admin::ActionLog.create(
account: reporter,
action: account_action,
target: target_account,
)
report.resolve!(reporter)
end
def self.staff_account
username = Rails.configuration.x.th_automod.automod_account_username
Account.find_local(username) unless username.blank?
end
def self.process_status!(status)
ActivityPubActivityCreateExt.process!(status)
end
def self.process_account!(account)
AccountServiceExt.process!(account)
end
module ActivityPubActivityCreateExt
EXPLANATION = <<~EOS
This account was automatically suspended by TreehouseAutomod, an unsupported feature.
Currently, the account-only heuristic should only automatically suspend accounts with one specific username and display name.
If this action is unexpected, please unset TH_MENTION_SPAM_HEURISTIC_AUTO_LIMIT_ACTIVE.
EOS
def self.is_spam?(status)
return false unless Rails.configuration.x.th_automod.mention_spam_heuristic_auto_limit_active
account = status.account
minimal_effort = account.note.blank? && account.avatar_remote_url.blank? && account.header_remote_url.blank?
return false if (account.local? ||
account.local_followers_count > 0 ||
!minimal_effort)
# minimal effort account, check mentions and account-known age
has_mention_spam = status.mentions.size >= Rails.configuration.x.th_automod.mention_spam_threshold
is_new_account = account.created_at > (Time.now - Rails.configuration.x.th_automod.min_account_age_threshold)
has_mention_spam && is_new_account
end
# check if the status should be considered spam
# @return true if the status was reported and the account was infracted
def self.process!(status)
return false unless self.is_spam?(status)
return true if status.account.silenced?
Automod.silence_with_tracking_report!(status.account, explanation: EXPLANATION)
true
end
end
module AccountServiceExt
# hardcoded for now
# md5 because they don't deserve more mentions
HEURISTIC_NAMES = {
"0116a9deace3289b7092e945ef5ca0a5" => Set["57d3d0b932cc9cd01be6b2f4e82c1a4a"],
}
# probably mathematically impossible to collide, but just in case...
HEURISTIC_MAX_LEN = 16
EXPLANATION = <<~EOS
This account was automatically suspended by TreehouseAutomod, an unsupported feature.
Currently, the account-only heuristic should only automatically suspend accounts with one specific username and display name.
If this action is unexpected, please unset TH_HEURISTIC_AUTO_SUSPEND.
EOS
# @return true if the account was infracted
def self.process!(account)
return false unless heuristic_auto_suspend?(account)
Automod.suspend_with_tracking_report!(account, explanation: EXPLANATION) unless account.suspension_origin == "local"
true
end
def self.matches_evil_hash?(account)
username_md5 = Digest::MD5.hexdigest(account.username)
display_name_md5 = Digest::MD5.hexdigest(account.display_name)
HEURISTIC_NAMES[username_md5].include?(display_name_md5)
end
def self.heuristic_auto_suspend?(account)
return false unless Rails.configuration.x.th_automod.account_service_heuristic_auto_suspend_active
return unless account.username.length < HEURISTIC_MAX_LEN && account.display_name.length < HEURISTIC_MAX_LEN
self.matches_evil_hash?(account)
end
end
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -8,7 +8,3 @@ Fabricator(:user) do
current_sign_in_at { Time.zone.now }
agreement true
end
Fabricator(:moderator_user, :from => :user) do
role { Fabricate(:moderator_role) }
end

View File

@ -5,11 +5,3 @@ Fabricator(:user_role) do
color ''
permissions 0
end
Fabricator(:moderator_role, :from => :user_role) do
name 'fake moderator'
permissions UserRole::Flags::DEFAULT |
UserRole::Flags::CATEGORIES[:moderation]
.map { |p| UserRole::FLAGS[p] }
.reduce(&:|)
end

View File

@ -56,15 +56,15 @@ describe JsonLdHelper do
describe '#fetch_resource' do
context 'when the second argument is false' do
it 'returns resource even if the retrieved ID and the given URI does not match' do
stub_request(:get, 'https://bob.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://alice.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://bob.test/').to_return body: '{"id": "https://alice.test/"}'
stub_request(:get, 'https://alice.test/').to_return body: '{"id": "https://alice.test/"}'
expect(fetch_resource('https://bob.test/', false)).to eq({ 'id' => 'https://alice.test/' })
end
it 'returns nil if the object identified by the given URI and the object identified by the retrieved ID does not match' do
stub_request(:get, 'https://mallory.test/').to_return(body: '{"id": "https://marvin.test/"}', headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://marvin.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://marvin.test/"}'
stub_request(:get, 'https://marvin.test/').to_return body: '{"id": "https://alice.test/"}'
expect(fetch_resource('https://mallory.test/', false)).to be_nil
end
@ -72,7 +72,7 @@ describe JsonLdHelper do
context 'when the second argument is true' do
it 'returns nil if the retrieved ID and the given URI does not match' do
stub_request(:get, 'https://mallory.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://alice.test/"}'
expect(fetch_resource('https://mallory.test/', true)).to be_nil
end
end
@ -80,12 +80,12 @@ describe JsonLdHelper do
describe '#fetch_resource_without_id_validation' do
it 'returns nil if the status code is not 200' do
stub_request(:get, 'https://host.test/').to_return(status: 400, body: '{}', headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://host.test/').to_return status: 400, body: '{}'
expect(fetch_resource_without_id_validation('https://host.test/')).to be_nil
end
it 'returns hash' do
stub_request(:get, 'https://host.test/').to_return(status: 200, body: '{}', headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://host.test/').to_return status: 200, body: '{}'
expect(fetch_resource_without_id_validation('https://host.test/')).to eq({})
end
end

View File

@ -35,7 +35,7 @@ RSpec.describe ActivityPub::Activity::Announce do
context 'when sender is followed by a local account' do
before do
Fabricate(:account).follow!(sender)
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
subject.perform
end
@ -120,7 +120,7 @@ RSpec.describe ActivityPub::Activity::Announce do
let(:object_json) { 'https://example.com/actor/hello-world' }
before do
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
end
context 'when the relay is enabled' do

View File

@ -1155,89 +1155,5 @@ RSpec.describe ActivityPub::Activity::Create do
expect(sender.statuses.count).to eq 0
end
end
context 'with automod active' do
subject { described_class.new(json, sender, delivery: true) }
let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', created_at: created_at) }
let(:created_at) { Time.now }
let(:min_account_age_threshold) { 1.day }
let(:recipient_a) { Fabricate(:account) }
let(:recipient_b) { Fabricate(:account) }
let(:staff_user) { Fabricate(:moderator_user) }
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
cc: ActivityPub::TagManager.instance.uri_for(recipient_a),
tag: recipients.map do |recipient|
{
type: 'Mention',
href: ActivityPub::TagManager.instance.uri_for(recipient),
}
end,
}
end
before do
allow(Rails.configuration.x.th_automod).to receive(:automod_account_username).and_return(staff_user.account.username)
allow(Rails.configuration.x.th_automod).to receive(:mention_spam_heuristic_auto_limit_active).and_return(true)
allow(Rails.configuration.x.th_automod).to receive(:mention_spam_threshold).and_return(2)
allow(Rails.configuration.x.th_automod).to receive(:min_account_age_threshold).and_return(min_account_age_threshold)
allow(subject).to receive(:distribute)
allow(sender).to receive(:silence!).and_call_original
subject.perform
end
shared_examples 'automod activates' do
it 'silences the sender' do
expect(sender).to have_received(:silence!)
expect(sender.silenced?).to be_truthy
end
it 'skips distribution' do
expect(subject).not_to have_received(:distribute)
end
it 'files a tracking report' do
expect(sender.previous_strikes_count).to be_truthy
end
end
shared_examples 'automod does not activate' do
it 'does not silence the sender' do
expect(sender.silenced?).to be_falsy
end
it 'does not file a tracking report' do
expect(sender.reports.empty?).to be_truthy
end
end
context 'and spammy message' do
let(:recipients) { [recipient_a, recipient_b] }
context 'and old account' do
let(:created_at) { Time.now - min_account_age_threshold - 1.hour }
include_examples 'automod does not activate'
end
context 'and new account' do
let(:created_at) { Time.now - min_account_age_threshold + 1.hour }
include_examples 'automod activates'
end
end
context 'and hammy message' do
let(:recipients) { [recipient_a] }
include_examples 'automod does not activate'
end
end
end
end

View File

@ -3,19 +3,19 @@
require 'rails_helper'
RSpec.describe Identity do
describe '.find_for_omniauth' do
describe '.find_for_oauth' do
let(:auth) { Fabricate(:identity, user: Fabricate(:user)) }
it 'calls .find_or_create_by' do
allow(described_class).to receive(:find_or_create_by)
described_class.find_for_omniauth(auth)
described_class.find_for_oauth(auth)
expect(described_class).to have_received(:find_or_create_by).with(uid: auth.uid, provider: auth.provider)
end
it 'returns an instance of Identity' do
expect(described_class.find_for_omniauth(auth)).to be_instance_of described_class
expect(described_class.find_for_oauth(auth)).to be_instance_of described_class
end
end
end

View File

@ -40,7 +40,7 @@ RSpec.describe Invite do
let(:max_uses) { 25 }
let(:expires_in) { 1.week.in_seconds }
let(:regular_user) { Fabricate(:user) }
let(:moderator_user) { Fabricate(:moderator_user) }
let(:moderator_user) { Fabricate(:user, moderator: true) }
let(:user) { regular_user }
let(:created_at) { Time.at(0) }
let(:expires_at) { Time.at(0) + expires_in }

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