1
0
Fork 0

adds patched-files

main
Dominic Reich 1 year ago
parent 66937ddb69
commit aa60c6bc5f
Signed by: dominic
GPG Key ID: BC9D6AE1A3BE169A

@ -0,0 +1,19 @@
# patched-files
This folder contains some modified files (not patches) that I extracted
from my dead instance. I was very happy with the recent color changed I made
so you might want to have a look at the `oe7drt-blue` and `oe7drt-greeny`
styles.
## additional files/changes
I've also played a bit with some other files. Those were primarily the
`robots.txt` which I added a few paths to be ignored (haha! funny.
fucking bots...) by web spiders/crawlers.
I also increased the post character limit to 2000, aswell as the poll
options to 12 and those options max-chars to 180.
One patch changes the <abbr title="Content Security Policy">CSP</abbr> to
allow the use of Google Fonts (if you use the material theme (not in this
repo)).

@ -0,0 +1,301 @@
import React from 'react';
import CharacterCounter from './character_counter';
import Button from '../../../components/button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import AutosuggestInput from '../../../components/autosuggest_input';
import PollButtonContainer from '../containers/poll_button_container';
import UploadButtonContainer from '../containers/upload_button_container';
import { defineMessages, injectIntl } from 'react-intl';
import SpoilerButtonContainer from '../containers/spoiler_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import PollFormContainer from '../containers/poll_form_container';
import UploadFormContainer from '../containers/upload_form_container';
import WarningContainer from '../containers/warning_container';
import LanguageDropdown from '../containers/language_dropdown_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { countableText } from '../util/counter';
import Icon from 'mastodon/components/icon';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' },
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
});
export default @injectIntl
class ComposeForm extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
intl: PropTypes.object.isRequired,
text: PropTypes.string.isRequired,
suggestions: ImmutablePropTypes.list,
spoiler: PropTypes.bool,
privacy: PropTypes.string,
spoilerText: PropTypes.string,
focusDate: PropTypes.instanceOf(Date),
caretPosition: PropTypes.number,
preselectDate: PropTypes.instanceOf(Date),
isSubmitting: PropTypes.bool,
isChangingUpload: PropTypes.bool,
isEditing: PropTypes.bool,
isUploading: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onClearSuggestions: PropTypes.func.isRequired,
onFetchSuggestions: PropTypes.func.isRequired,
onSuggestionSelected: PropTypes.func.isRequired,
onChangeSpoilerText: PropTypes.func.isRequired,
onPaste: PropTypes.func.isRequired,
onPickEmoji: PropTypes.func.isRequired,
autoFocus: PropTypes.bool,
anyMedia: PropTypes.bool,
isInReply: PropTypes.bool,
singleColumn: PropTypes.bool,
lang: PropTypes.string,
};
static defaultProps = {
autoFocus: false,
};
handleChange = (e) => {
this.props.onChange(e.target.value);
};
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.handleSubmit();
}
};
getFulltextForCharacterCounting = () => {
return [this.props.spoiler? this.props.spoilerText: '', countableText(this.props.text)].join('');
};
canSubmit = () => {
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
const fulltext = this.getFulltextForCharacterCounting();
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 2000 || (isOnlyWhitespace && !anyMedia));
};
handleSubmit = (e) => {
if (this.props.text !== this.autosuggestTextarea.textarea.value) {
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// Update the state to match the current text
this.props.onChange(this.autosuggestTextarea.textarea.value);
}
if (!this.canSubmit()) {
return;
}
this.props.onSubmit(this.context.router ? this.context.router.history : null);
if (e) {
e.preventDefault();
}
};
onSuggestionsClearRequested = () => {
this.props.onClearSuggestions();
};
onSuggestionsFetchRequested = (token) => {
this.props.onFetchSuggestions(token);
};
onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
};
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
};
handleChangeSpoilerText = (e) => {
this.props.onChangeSpoilerText(e.target.value);
};
handleFocus = () => {
if (this.composeForm && !this.props.singleColumn) {
const { left, right } = this.composeForm.getBoundingClientRect();
if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
this.composeForm.scrollIntoView();
}
}
};
componentDidMount () {
this._updateFocusAndSelection({ });
}
componentDidUpdate (prevProps) {
this._updateFocusAndSelection(prevProps);
}
_updateFocusAndSelection = (prevProps) => {
// This statement does several things:
// - If we're beginning a reply, and,
// - Replying to zero or one users, places the cursor at the end of the textbox.
// - Replying to more than one user, selects any usernames past the first;
// this provides a convenient shortcut to drop everyone else from the conversation.
if (this.props.focusDate && this.props.focusDate !== prevProps.focusDate) {
let selectionEnd, selectionStart;
if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply) {
selectionEnd = this.props.text.length;
selectionStart = this.props.text.search(/\s/) + 1;
} else if (typeof this.props.caretPosition === 'number') {
selectionStart = this.props.caretPosition;
selectionEnd = this.props.caretPosition;
} else {
selectionEnd = this.props.text.length;
selectionStart = selectionEnd;
}
// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => {
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
this.autosuggestTextarea.textarea.focus();
}).catch(console.error);
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
this.autosuggestTextarea.textarea.focus();
} else if (this.props.spoiler !== prevProps.spoiler) {
if (this.props.spoiler) {
this.spoilerText.input.focus();
} else if (prevProps.spoiler) {
this.autosuggestTextarea.textarea.focus();
}
}
};
setAutosuggestTextarea = (c) => {
this.autosuggestTextarea = c;
};
setSpoilerText = (c) => {
this.spoilerText = c;
};
setRef = c => {
this.composeForm = c;
};
handleEmojiPick = (data) => {
const { text } = this.props;
const position = this.autosuggestTextarea.textarea.selectionStart;
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
this.props.onPickEmoji(position, data, needsSpace);
};
render () {
const { intl, onPaste, autoFocus } = this.props;
const disabled = this.props.isSubmitting;
let publishText = '';
if (this.props.isEditing) {
publishText = intl.formatMessage(messages.saveChanges);
} else if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
} else {
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
}
return (
<form className='compose-form' onSubmit={this.handleSubmit}>
<WarningContainer />
<ReplyIndicatorContainer />
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
<AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDown}
disabled={!this.props.spoiler}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSpoilerSuggestionSelected}
searchTokens={[':']}
id='cw-spoiler-input'
className='spoiler-input__input'
lang={this.props.lang}
spellCheck
/>
</div>
<AutosuggestTextarea
ref={this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={autoFocus}
lang={this.props.lang}
>
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
<div className='compose-form__modifiers'>
<UploadFormContainer />
<PollFormContainer />
</div>
</AutosuggestTextarea>
<div className='compose-form__buttons-wrapper'>
<div className='compose-form__buttons'>
<UploadButtonContainer />
<PollButtonContainer />
<PrivacyDropdownContainer disabled={this.props.isEditing} />
<SpoilerButtonContainer />
<LanguageDropdown />
</div>
<div className='character-counter__wrapper'>
<CharacterCounter max={2000} text={this.getFulltextForCharacterCounting()} />
</div>
</div>
<div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'>
<Button
type='submit'
text={publishText}
disabled={!this.canSubmit()}
block
/>
</div>
</div>
</form>
);
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,11 @@
@import 'coffee-dark/variables';
@import 'application';
@import 'coffee-dark/diff';
//@import 'boost';
//@import 'mods/display_browserfont';
@import 'mods/display_breakname';
@import 'mods/display_fullname';
@import 'mods/display_emojizoom';
//@import 'mods/display_circleavatar';
@import 'mods/layout_1600px';
@import 'mods/layout_widercolumns';

@ -0,0 +1,77 @@
// components.scss
.compose-form {
.compose-form__modifiers {
.compose-form__upload {
&-description {
input {
&::placeholder {
opacity: 1;
}
}
}
}
}
}
.rich-formatting a,
.rich-formatting p a,
.rich-formatting li a,
.landing-page__short-description p a,
.status__content a,
.reply-indicator__content a {
color: lighten($ui-highlight-color, 12%);
text-decoration: none;
&.mention {
text-decoration: none;
}
&.mention span {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
&:hover,
&:focus,
&:active {
text-decoration: none;
}
&.status__content__spoiler-link {
color: $secondary-text-color;
text-decoration: none;
}
}
.status__content__read-more-button {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
.getting-started__footer a {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
.nothing-here {
color: $darker-text-color;
}
.public-layout .public-account-header__tabs__tabs .counter.active::after {
border-bottom: 4px solid $ui-highlight-color;
}

@ -0,0 +1,56 @@
// Commonly used web colors
$black: #000000; // Black
$white: #ffffff; // White
$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
$red-bookmark: $warning-red;
// Values from the classic Mastodon UI
$classic-base-color: #282c37; // Midnight Express
$classic-primary-color: #9baec8; // Echo Blue
$classic-secondary-color: #d9e1e8; // Pattens Blue
$classic-highlight-color: #e7b01c; // Summer Sky
// Variables for defaults in UI
$base-shadow-color: $black !default;
$base-overlay-background: $black !default;
$base-border-color: $white !default;
$simple-background-color: $white !default;
$valid-value-color: $success-green !default;
$error-value-color: $error-red !default;
// Tell UI to use selected colors
$ui-base-color: $classic-base-color !default; // Darkest
$ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default;
// Variables for texts
$primary-text-color: $white !default;
$darker-text-color: $ui-primary-color !default;
$dark-text-color: $ui-base-lighter-color !default;
$secondary-text-color: $ui-secondary-color !default;
$highlight-text-color: $ui-highlight-color !default;
$action-button-color: $ui-base-lighter-color !default;
// For texts on inverted backgrounds
$inverted-text-color: $ui-base-color !default;
$lighter-text-color: $ui-base-lighter-color !default;
$light-text-color: $ui-primary-color !default;
// Language codes that uses CJK fonts
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
// Variables for components
$media-modal-media-max-width: 100%;
// put margins on top and bottom of image to avoid the screen covered by image.
$media-modal-media-max-height: 80%;
$no-gap-breakpoint: 415px;
$font-sans-serif: 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default;
$font-monospace: 'mastodon-font-monospace' !default;

@ -0,0 +1,6 @@
@import 'coffee-light/variables';
@import 'application';
@import 'coffee-light/diff';
//@import 'boost';
//@import 'mods/display_fullname';
@import 'mods/display_circleavatar';

@ -0,0 +1,776 @@
// Notes!
// Sass color functions, "darken" and "lighten" are automatically replaced.
html {
scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
}
// Change the colors of button texts
.button {
color: $white;
&.button-alternative-2 {
color: $white;
}
}
.status-card__actions button,
.status-card__actions a {
color: rgba($white, 0.8);
&:hover,
&:active,
&:focus {
color: $white;
}
}
// Change default background colors of columns
.column > .scrollable,
.getting-started,
.column-inline-form,
.error-column,
.regeneration-indicator {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.directory__card__img {
background: lighten($ui-base-color, 12%);
}
.filter-form,
.directory__card__bar {
background: $white;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.scrollable .directory__list {
width: calc(100% + 2px);
margin-left: -1px;
margin-right: -1px;
}
.directory__card,
.table-of-contents {
border: 1px solid lighten($ui-base-color, 8%);
}
.column-back-button,
.column-header {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
}
&--slim-button {
top: -50px;
right: 0;
}
}
.column-header__back-button,
.column-header__button,
.column-header__button.active,
.account__header__bar,
.directory__card__extra {
background: $white;
}
.column-header__button.active {
color: $ui-highlight-color;
&:hover,
&:active,
&:focus {
color: $ui-highlight-color;
background: $white;
}
}
.account__header__bar .avatar .account__avatar {
border-color: $white;
}
.getting-started__footer a {
color: $ui-secondary-color;
text-decoration: underline;
}
.confirmation-modal__secondary-button,
.confirmation-modal__cancel-button,
.mute-modal__cancel-button,
.block-modal__cancel-button {
color: lighten($ui-base-color, 26%);
&:hover,
&:focus,
&:active {
color: $primary-text-color;
}
}
.column-subheading {
background: darken($ui-base-color, 4%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.getting-started,
.scrollable {
.column-link {
background: $white;
border-bottom: 1px solid lighten($ui-base-color, 8%);
&:hover,
&:active,
&:focus {
background: $ui-base-color;
}
}
}
.getting-started .navigation-bar {
border-top: 1px solid lighten($ui-base-color, 8%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
}
}
.compose-form__autosuggest-wrapper,
.poll__option input[type="text"],
.compose-form .spoiler-input__input,
.compose-form__poll-wrapper select,
.search__input,
.setting-text,
.box-widget input[type="text"],
.box-widget input[type="email"],
.box-widget input[type="password"],
.box-widget textarea,
.statuses-grid .detailed-status,
.audio-player {
border: 1px solid lighten($ui-base-color, 8%);
}
.search__input {
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
border-bottom: 0;
}
}
.list-editor .search .search__input {
border-top: 0;
border-bottom: 0;
}
.compose-form__poll-wrapper select {
background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>") no-repeat right 8px center / auto 16px;
}
.compose-form__poll-wrapper,
.compose-form__poll-wrapper .poll__footer {
border-top-color: lighten($ui-base-color, 8%);
}
.notification__filter-bar {
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.compose-form .compose-form__buttons-wrapper {
background: $ui-base-color;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.drawer__header,
.drawer__inner {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
}
.drawer__inner__mastodon {
background: $white url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto;
}
// Change the colors used in compose-form
.compose-form {
.compose-form__modifiers {
.compose-form__upload__actions .icon-button {
color: lighten($white, 7%);
&:active,
&:focus,
&:hover {
color: $white;
}
}
.compose-form__upload-description input {
color: lighten($white, 7%);
&::placeholder {
color: lighten($white, 7%);
}
}
}
.compose-form__buttons-wrapper {
background: darken($ui-base-color, 6%);
}
.autosuggest-textarea__suggestions {
background: darken($ui-base-color, 6%);
}
.autosuggest-textarea__suggestions__item {
&:hover,
&:focus,
&:active,
&.selected {
background: lighten($ui-base-color, 4%);
}
}
}
.emoji-mart-bar {
border-color: lighten($ui-base-color, 4%);
&:first-child {
background: darken($ui-base-color, 6%);
}
}
.emoji-mart-search input {
background: rgba($ui-base-color, 0.3);
border-color: $ui-base-color;
}
// Change the background colors of statuses
.focusable:focus {
background: $ui-base-color;
}
.status.status-direct {
background: lighten($ui-base-color, 4%);
}
.focusable:focus .status.status-direct {
background: lighten($ui-base-color, 8%);
}
.detailed-status,
.detailed-status__action-bar {
background: $white;
}
// Change the background colors of status__content__spoiler-link
.reply-indicator__content .status__content__spoiler-link,
.status__content .status__content__spoiler-link {
background: $ui-base-color;
&:hover {
background: lighten($ui-base-color, 4%);
}
}
// Change the background colors of media and video spoilers
.media-spoiler,
.video-player__spoiler {
background: $ui-base-color;
}
.privacy-dropdown.active .privacy-dropdown__value.active .icon-button {
color: $white;
}
.account-gallery__item a {
background-color: $ui-base-color;
}
// Change the colors used in the dropdown menu
.dropdown-menu {
background: $white;
&__arrow {
&.left {
border-left-color: $white;
}
&.top {
border-top-color: $white;
}
&.bottom {
border-bottom-color: $white;
}
&.right {
border-right-color: $white;
}
}
&__item {
a {
background: $white;
color: $darker-text-color;
}
}
}
// Change the text colors on inverted background
.privacy-dropdown__option.active,
.privacy-dropdown__option:hover,
.privacy-dropdown__option.active .privacy-dropdown__option__content,
.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
.privacy-dropdown__option:hover .privacy-dropdown__option__content,
.privacy-dropdown__option:hover .privacy-dropdown__option__content strong,
.dropdown-menu__item a:active,
.dropdown-menu__item a:focus,
.dropdown-menu__item a:hover,
.actions-modal ul li:not(:empty) a.active,
.actions-modal ul li:not(:empty) a.active button,
.actions-modal ul li:not(:empty) a:active,
.actions-modal ul li:not(:empty) a:active button,
.actions-modal ul li:not(:empty) a:focus,
.actions-modal ul li:not(:empty) a:focus button,
.actions-modal ul li:not(:empty) a:hover,
.actions-modal ul li:not(:empty) a:hover button,
.admin-wrapper .sidebar ul .simple-navigation-active-leaf a,
.simple_form .block-button,
.simple_form .button,
.simple_form button {
color: $white;
}
.dropdown-menu__separator {
border-bottom-color: lighten($ui-base-color, 4%);
}
// Change the background colors of modals
.actions-modal,
.boost-modal,
.confirmation-modal,
.mute-modal,
.block-modal,
.report-modal,
.embed-modal,
.error-modal,
.onboarding-modal,
.report-modal__comment .setting-text__wrapper,
.report-modal__comment .setting-text {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
}
.report-modal__comment {
border-right-color: lighten($ui-base-color, 8%);
}
.report-modal__container {
border-top-color: lighten($ui-base-color, 8%);
}
.column-header__collapsible-inner {
background: darken($ui-base-color, 4%);
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.focal-point__preview strong {
color: $white;
}
.boost-modal__action-bar,
.confirmation-modal__action-bar,
.mute-modal__action-bar,
.block-modal__action-bar,
.onboarding-modal__paginator,
.error-modal__footer {
background: darken($ui-base-color, 6%);
.onboarding-modal__nav,
.error-modal__nav {
&:hover,
&:focus,
&:active {
background-color: darken($ui-base-color, 12%);
}
}
}
.display-case__case {
background: $white;
}
.embed-modal .embed-modal__container .embed-modal__html {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
&:focus {
border-color: lighten($ui-base-color, 12%);
background: $white;
}
}
.react-toggle-track {
background: $ui-secondary-color;
}
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
background: darken($ui-secondary-color, 10%);
}
.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
background: lighten($ui-highlight-color, 10%);
}
// Change the default color used for the text in an empty column or on the error column
.empty-column-indicator,
.error-column {
color: $primary-text-color;
background: $white;
}
.tabs-bar {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
border-bottom: 0;
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
}
&__link {
padding-bottom: 14px;
border-bottom-width: 1px;
border-bottom-color: lighten($ui-base-color, 8%);
&:hover,
&:active,
&:focus {
background: $ui-base-color;
}
&.active {
&:hover,
&:active,
&:focus {
background: transparent;
border-bottom-color: $ui-highlight-color;
}
}
}
}
// Change the default colors used on some parts of the profile pages
.activity-stream-tabs {
background: $account-background-color;
border-bottom-color: lighten($ui-base-color, 8%);
}
.box-widget,
.nothing-here,
.page-header,
.directory__tag > a,
.directory__tag > div,
.landing-page__call-to-action,
.contact-widget,
.landing .hero-widget__text,
.landing-page__information.contact-widget {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border-left: 0;
border-right: 0;
border-top: 0;
}
}
.landing .hero-widget__text {
border-top: 0;
border-bottom: 0;
}
.simple_form {
input[type=text],
input[type=number],
input[type=email],
input[type=password],
textarea {
&:hover {
border-color: lighten($ui-base-color, 12%);
}
}
}
.landing .hero-widget__footer {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
@media screen and (max-width: $no-gap-breakpoint) {
border: 0;
}
}
.brand__tagline {
color: $ui-secondary-color;
}
.directory__tag > a {
&:hover,
&:active,
&:focus {
background: $ui-base-color;
}
@media screen and (max-width: $no-gap-breakpoint) {
border: 0;
}
}
.directory__tag.active > a,
.directory__tag.active > div {
border-color: $ui-highlight-color;
&,
h4,
h4 small,
.fa,
.trends__item__current {
color: $white;
}
&:hover,
&:active,
&:focus {
background: $ui-highlight-color;
}
}
.batch-table {
&__toolbar,
&__row,
.nothing-here {
border-color: lighten($ui-base-color, 8%);
}
}
.activity-stream {
border: 1px solid lighten($ui-base-color, 8%);
&--under-tabs {
border-top: 0;
}
.entry {
background: $account-background-color;
.detailed-status.light,
.more.light,
.status.light {
border-bottom-color: lighten($ui-base-color, 8%);
}
}
.status.light {
.status__content {
color: $primary-text-color;
}
.display-name {
strong {
color: $primary-text-color;
}
}
}
}
.accounts-grid {
.account-grid-card {
.controls {
.icon-button {
color: $darker-text-color;
}
}
.name {
a {
color: $primary-text-color;
}
}
.username {
color: $darker-text-color;
}
.account__header__content {
color: $primary-text-color;
}
}
}
.simple_form,
.table-form {
.warning {
box-shadow: none;
background: rgba($error-red, 0.5);
text-shadow: none;
}
.recommended {
border-color: $ui-highlight-color;
color: $ui-highlight-color;
background-color: rgba($ui-highlight-color, 0.1);
}
}
.compose-form .compose-form__warning {
border-color: $ui-highlight-color;
background-color: rgba($ui-highlight-color, 0.1);
&,
a {
color: $ui-highlight-color;
}
}
.status__content,
.reply-indicator__content {
a {
color: $highlight-text-color;
}
}
.button.logo-button {
color: $white;
svg {
fill: $white;
}
}
.public-layout {
.account__section-headline {
border: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
}
}
.header,
.public-account-header,
.public-account-bio {
box-shadow: none;
}
.public-account-bio,
.hero-widget__text {
background: $account-background-color;
border: 1px solid lighten($ui-base-color, 8%);
}
.header {
background: $ui-base-color;
border: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border: 0;
}
.brand {
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 4%);
}
}
}
.public-account-header {
&__image {
background: lighten($ui-base-color, 12%);
&::after {
box-shadow: none;
}
}
&__bar {
&::before {
background: $account-background-color;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.avatar img {
border-color: $account-background-color;
}
@media screen and (max-width: $no-columns-breakpoint) {
background: $account-background-color;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
}
&__tabs {
&__name {
h1,
h1 small {
color: $white;
@media screen and (max-width: $no-columns-breakpoint) {
color: $primary-text-color;
}
}
}
}
&__extra {
.public-account-bio {
border: 0;
}
.public-account-bio .account__header__fields {
border-color: lighten($ui-base-color, 8%);
}
}
}
}
.notification__filter-bar button.active::after,
.account__section-headline a.active::after {
border-color: transparent transparent $white;
}
.hero-widget,
.box-widget,
.contact-widget,
.landing-page__information.contact-widget,
.moved-account-widget,
.memoriam-widget,
.activity-stream,
.nothing-here,
.directory__tag > a,
.directory__tag > div,
.card > a,
.page-header,
.compose-form .compose-form__warning {
box-shadow: none;
}
.audio-player .video-player__controls button,
.audio-player .video-player__time-sep,
.audio-player .video-player__time-current,
.audio-player .video-player__time-total {
color: $primary-text-color;
}

@ -0,0 +1,41 @@
// Dependent colors
$black: #000000;
$white: #ffffff;
$classic-base-color: #282c37;
$classic-primary-color: #9baec8;
$classic-secondary-color: #d9e1e8;
$classic-highlight-color: #e7b01c;
// Differences
$success-green: lighten(#3c754d, 8%);
$base-overlay-background: $white !default;
$valid-value-color: $success-green !default;
$ui-base-color: $classic-secondary-color !default;
$ui-base-lighter-color: #b0c0cf;
$ui-primary-color: #9bcbed;
$ui-secondary-color: $classic-base-color !default;
$ui-highlight-color: #e7b01c;
$primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default;
$dark-text-color: #444b5d;
$action-button-color: #606984;
$inverted-text-color: $black !default;
$lighter-text-color: $classic-base-color !default;
$light-text-color: #444b5d;
//Newly added colors
$account-background-color: $white !default;
//Invert darkened and lightened colors
@function darken($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) + $amount);
}
@function lighten($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) - $amount);
}

@ -0,0 +1,3 @@
@import 'dark-red/variables';
@import 'application';
@import 'dark-red/diff';

@ -0,0 +1,77 @@
// components.scss
.compose-form {
.compose-form__modifiers {
.compose-form__upload {
&-description {
input {
&::placeholder {
opacity: 1;
}
}
}
}
}
}
.rich-formatting a,
.rich-formatting p a,
.rich-formatting li a,
.landing-page__short-description p a,
.status__content a,
.reply-indicator__content a {
color: lighten($ui-highlight-color, 12%);
text-decoration: none;
&.mention {
text-decoration: none;
}
&.mention span {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
&:hover,
&:focus,
&:active {
text-decoration: none;
}
&.status__content__spoiler-link {
color: $secondary-text-color;
text-decoration: none;
}
}
.status__content__read-more-button {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
.getting-started__footer a {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
.nothing-here {
color: $darker-text-color;
}
.public-layout .public-account-header__tabs__tabs .counter.active::after {
border-bottom: 4px solid $ui-highlight-color;
}

@ -0,0 +1,56 @@
// Commonly used web colors
$black: #000000; // Black
$white: #ffffff; // White
$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
$red-bookmark: $warning-red;
// Values from the classic Mastodon UI
$classic-base-color: #282c37; // Midnight Express
$classic-primary-color: #9baec8; // Echo Blue
$classic-secondary-color: #d9e1e8; // Pattens Blue
$classic-highlight-color: #d92b2b; // Summer Sky
// Variables for defaults in UI
$base-shadow-color: $black !default;
$base-overlay-background: $black !default;
$base-border-color: $white !default;
$simple-background-color: $white !default;
$valid-value-color: $success-green !default;
$error-value-color: $error-red !default;
// Tell UI to use selected colors
$ui-base-color: $classic-base-color !default; // Darkest
$ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default;
// Variables for texts
$primary-text-color: $white !default;
$darker-text-color: $ui-primary-color !default;
$dark-text-color: $ui-base-lighter-color !default;
$secondary-text-color: $ui-secondary-color !default;
$highlight-text-color: $ui-highlight-color !default;
$action-button-color: $ui-base-lighter-color !default;
// For texts on inverted backgrounds
$inverted-text-color: $ui-base-color !default;
$lighter-text-color: $ui-base-lighter-color !default;
$light-text-color: $ui-primary-color !default;
// Language codes that uses CJK fonts
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
// Variables for components
$media-modal-media-max-width: 100%;
// put margins on top and bottom of image to avoid the screen covered by image.
$media-modal-media-max-height: 80%;
$no-gap-breakpoint: 415px;
$font-sans-serif: 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default;
$font-monospace: 'mastodon-font-monospace' !default;

@ -0,0 +1,48 @@
.detailed-status > .media-spoiler,
.status > .media-spoiler,
.status .video-player,
.media-gallery,
.status .status-card.interactive {
margin-top: 20px;
margin-left: -68px;
width: calc(100% + 80px);
}
.detailed-status > .media-spoiler,
.status > .media-spoiler,
.video-player {
max-width: none;
}
/* If there's no status text, add an extra margin on top */
.status .status__info + .media-gallery,
.status .status__info + .media-spoiler,
.status .status__info + .video-player,
.status .status__info + .status-card {
margin-top: 40px;
}
.status__video-player-video {
transform: unset;
top: unset;
}
.detailed-status .media-gallery {
margin-left: -10px;
width: calc(100% + 22px);
}
.public-layout .status {
.status__content {
min-height: 15px;
}
& > .media-spoiler,
.video-player,
.media-gallery,
.status-card {
margin-top: 20px;
width: calc(100% + 94px);
margin-left: -78px;
}
}

@ -0,0 +1,3 @@
@import 'light-red/variables';
@import 'application';
@import 'light-red/diff';

@ -0,0 +1,776 @@
// Notes!
// Sass color functions, "darken" and "lighten" are automatically replaced.
html {
scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
}
// Change the colors of button texts
.button {
color: $white;
&.button-alternative-2 {
color: $white;
}
}
.status-card__actions button,
.status-card__actions a {
color: rgba($white, 0.8);
&:hover,
&:active,
&:focus {
color: $white;
}
}
// Change default background colors of columns
.column > .scrollable,
.getting-started,
.column-inline-form,
.error-column,
.regeneration-indicator {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.directory__card__img {
background: lighten($ui-base-color, 12%);
}
.filter-form,
.directory__card__bar {
background: $white;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.scrollable .directory__list {
width: calc(100% + 2px);
margin-left: -1px;
margin-right: -1px;
}
.directory__card,
.table-of-contents {
border: 1px solid lighten($ui-base-color, 8%);
}
.column-back-button,
.column-header {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
}
&--slim-button {
top: -50px;
right: 0;
}
}
.column-header__back-button,
.column-header__button,
.column-header__button.active,
.account__header__bar,
.directory__card__extra {
background: $white;
}
.column-header__button.active {
color: $ui-highlight-color;
&:hover,
&:active,
&:focus {
color: $ui-highlight-color;
background: $white;
}
}
.account__header__bar .avatar .account__avatar {
border-color: $white;
}
.getting-started__footer a {
color: $ui-secondary-color;
text-decoration: underline;
}
.confirmation-modal__secondary-button,
.confirmation-modal__cancel-button,
.mute-modal__cancel-button,
.block-modal__cancel-button {
color: lighten($ui-base-color, 26%);
&:hover,
&:focus,
&:active {
color: $primary-text-color;
}
}
.column-subheading {
background: darken($ui-base-color, 4%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.getting-started,
.scrollable {
.column-link {
background: $white;
border-bottom: 1px solid lighten($ui-base-color, 8%);
&:hover,
&:active,
&:focus {
background: $ui-base-color;
}
}
}
.getting-started .navigation-bar {
border-top: 1px solid lighten($ui-base-color, 8%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
}
}
.compose-form__autosuggest-wrapper,
.poll__option input[type="text"],
.compose-form .spoiler-input__input,
.compose-form__poll-wrapper select,
.search__input,
.setting-text,
.box-widget input[type="text"],
.box-widget input[type="email"],
.box-widget input[type="password"],
.box-widget textarea,
.statuses-grid .detailed-status,
.audio-player {
border: 1px solid lighten($ui-base-color, 8%);
}
.search__input {
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
border-bottom: 0;
}
}
.list-editor .search .search__input {
border-top: 0;
border-bottom: 0;
}
.compose-form__poll-wrapper select {
background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>") no-repeat right 8px center / auto 16px;
}
.compose-form__poll-wrapper,
.compose-form__poll-wrapper .poll__footer {
border-top-color: lighten($ui-base-color, 8%);
}
.notification__filter-bar {
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.compose-form .compose-form__buttons-wrapper {
background: $ui-base-color;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.drawer__header,
.drawer__inner {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
}
.drawer__inner__mastodon {
background: $white url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto;
}
// Change the colors used in compose-form
.compose-form {
.compose-form__modifiers {
.compose-form__upload__actions .icon-button {
color: lighten($white, 7%);
&:active,
&:focus,
&:hover {
color: $white;
}
}
.compose-form__upload-description input {
color: lighten($white, 7%);
&::placeholder {
color: lighten($white, 7%);
}
}
}
.compose-form__buttons-wrapper {
background: darken($ui-base-color, 6%);
}
.autosuggest-textarea__suggestions {
background: darken($ui-base-color, 6%);
}
.autosuggest-textarea__suggestions__item {
&:hover,
&:focus,
&:active,
&.selected {
background: lighten($ui-base-color, 4%);
}
}
}
.emoji-mart-bar {
border-color: lighten($ui-base-color, 4%);
&:first-child {
background: darken($ui-base-color, 6%);
}
}
.emoji-mart-search input {
background: rgba($ui-base-color, 0.3);
border-color: $ui-base-color;
}
// Change the background colors of statuses
.focusable:focus {
background: $ui-base-color;
}
.status.status-direct {
background: lighten($ui-base-color, 4%);
}
.focusable:focus .status.status-direct {
background: lighten($ui-base-color, 8%);
}
.detailed-status,
.detailed-status__action-bar {
background: $white;
}
// Change the background colors of status__content__spoiler-link
.reply-indicator__content .status__content__spoiler-link,
.status__content .status__content__spoiler-link {
background: $ui-base-color;
&:hover {
background: lighten($ui-base-color, 4%);
}
}
// Change the background colors of media and video spoilers
.media-spoiler,
.video-player__spoiler {
background: $ui-base-color;
}
.privacy-dropdown.active .privacy-dropdown__value.active .icon-button {
color: $white;
}
.account-gallery__item a {
background-color: $ui-base-color;
}
// Change the colors used in the dropdown menu
.dropdown-menu {
background: $white;
&__arrow {
&.left {
border-left-color: $white;
}
&.top {
border-top-color: $white;
}
&.bottom {
border-bottom-color: $white;
}
&.right {
border-right-color: $white;
}
}
&__item {
a {
background: $white;
color: $darker-text-color;
}
}
}
// Change the text colors on inverted background
.privacy-dropdown__option.active,
.privacy-dropdown__option:hover,
.privacy-dropdown__option.active .privacy-dropdown__option__content,
.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
.privacy-dropdown__option:hover .privacy-dropdown__option__content,
.privacy-dropdown__option:hover .privacy-dropdown__option__content strong,
.dropdown-menu__item a:active,
.dropdown-menu__item a:focus,
.dropdown-menu__item a:hover,
.actions-modal ul li:not(:empty) a.active,
.actions-modal ul li:not(:empty) a.active button,
.actions-modal ul li:not(:empty) a:active,
.actions-modal ul li:not(:empty) a:active button,
.actions-modal ul li:not(:empty) a:focus,
.actions-modal ul li:not(:empty) a:focus button,
.actions-modal ul li:not(:empty) a:hover,
.actions-modal ul li:not(:empty) a:hover button,
.admin-wrapper .sidebar ul .simple-navigation-active-leaf a,
.simple_form .block-button,
.simple_form .button,
.simple_form button {
color: $white;
}
.dropdown-menu__separator {
border-bottom-color: lighten($ui-base-color, 4%);
}
// Change the background colors of modals
.actions-modal,
.boost-modal,
.confirmation-modal,
.mute-modal,
.block-modal,
.report-modal,
.embed-modal,
.error-modal,
.onboarding-modal,
.report-modal__comment .setting-text__wrapper,
.report-modal__comment .setting-text {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
}
.report-modal__comment {
border-right-color: lighten($ui-base-color, 8%);
}
.report-modal__container {
border-top-color: lighten($ui-base-color, 8%);
}
.column-header__collapsible-inner {
background: darken($ui-base-color, 4%);
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.focal-point__preview strong {
color: $white;
}
.boost-modal__action-bar,
.confirmation-modal__action-bar,
.mute-modal__action-bar,
.block-modal__action-bar,
.onboarding-modal__paginator,
.error-modal__footer {
background: darken($ui-base-color, 6%);
.onboarding-modal__nav,
.error-modal__nav {
&:hover,
&:focus,
&:active {
background-color: darken($ui-base-color, 12%);
}
}
}
.display-case__case {
background: $white;
}
.embed-modal .embed-modal__container .embed-modal__html {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
&:focus {
border-color: lighten($ui-base-color, 12%);
background: $white;
}
}
.react-toggle-track {
background: $ui-secondary-color;
}
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
background: darken($ui-secondary-color, 10%);
}
.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
background: lighten($ui-highlight-color, 10%);
}
// Change the default color used for the text in an empty column or on the error column
.empty-column-indicator,
.error-column {
color: $primary-text-color;
background: $white;
}
.tabs-bar {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
border-bottom: 0;
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
}
&__link {
padding-bottom: 14px;
border-bottom-width: 1px;
border-bottom-color: lighten($ui-base-color, 8%);
&:hover,
&:active,
&:focus {
background: $ui-base-color;
}
&.active {
&:hover,
&:active,
&:focus {
background: transparent;
border-bottom-color: $ui-highlight-color;
}
}
}
}
// Change the default colors used on some parts of the profile pages
.activity-stream-tabs {
background: $account-background-color;
border-bottom-color: lighten($ui-base-color, 8%);
}
.box-widget,
.nothing-here,
.page-header,
.directory__tag > a,
.directory__tag > div,
.landing-page__call-to-action,
.contact-widget,
.landing .hero-widget__text,
.landing-page__information.contact-widget {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border-left: 0;
border-right: 0;
border-top: 0;
}
}
.landing .hero-widget__text {
border-top: 0;
border-bottom: 0;
}
.simple_form {
input[type=text],
input[type=number],
input[type=email],
input[type=password],
textarea {
&:hover {
border-color: lighten($ui-base-color, 12%);
}
}
}
.landing .hero-widget__footer {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
@media screen and (max-width: $no-gap-breakpoint) {
border: 0;
}
}
.brand__tagline {
color: $ui-secondary-color;
}
.directory__tag > a {
&:hover,
&:active,
&:focus {
background: $ui-base-color;
}
@media screen and (max-width: $no-gap-breakpoint) {
border: 0;
}
}
.directory__tag.active > a,
.directory__tag.active > div {
border-color: $ui-highlight-color;
&,
h4,
h4 small,
.fa,
.trends__item__current {
color: $white;
}
&:hover,
&:active,
&:focus {
background: $ui-highlight-color;
}
}
.batch-table {
&__toolbar,
&__row,
.nothing-here {
border-color: lighten($ui-base-color, 8%);
}
}
.activity-stream {
border: 1px solid lighten($ui-base-color, 8%);
&--under-tabs {
border-top: 0;
}
.entry {
background: $account-background-color;
.detailed-status.light,
.more.light,
.status.light {
border-bottom-color: lighten($ui-base-color, 8%);
}
}
.status.light {
.status__content {
color: $primary-text-color;
}
.display-name {
strong {
color: $primary-text-color;
}
}
}
}
.accounts-grid {
.account-grid-card {
.controls {
.icon-button {
color: $darker-text-color;
}
}
.name {
a {
color: $primary-text-color;
}
}
.username {
color: $darker-text-color;
}
.account__header__content {
color: $primary-text-color;
}
}
}
.simple_form,
.table-form {
.warning {
box-shadow: none;
background: rgba($error-red, 0.5);
text-shadow: none;
}
.recommended {
border-color: $ui-highlight-color;
color: $ui-highlight-color;
background-color: rgba($ui-highlight-color, 0.1);
}
}
.compose-form .compose-form__warning {
border-color: $ui-highlight-color;
background-color: rgba($ui-highlight-color, 0.1);
&,
a {
color: $ui-highlight-color;
}
}
.status__content,
.reply-indicator__content {
a {
color: $highlight-text-color;
}
}
.button.logo-button {
color: $white;
svg {
fill: $white;
}
}
.public-layout {
.account__section-headline {
border: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
}
}
.header,
.public-account-header,
.public-account-bio {
box-shadow: none;
}
.public-account-bio,
.hero-widget__text {
background: $account-background-color;
border: 1px solid lighten($ui-base-color, 8%);
}
.header {
background: $ui-base-color;
border: 1px solid lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
border: 0;
}
.brand {
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 4%);
}
}
}
.public-account-header {
&__image {
background: lighten($ui-base-color, 12%);
&::after {
box-shadow: none;
}
}
&__bar {
&::before {
background: $account-background-color;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
.avatar img {
border-color: $account-background-color;
}
@media screen and (max-width: $no-columns-breakpoint) {
background: $account-background-color;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
}
}
&__tabs {
&__name {
h1,
h1 small {
color: $white;
@media screen and (max-width: $no-columns-breakpoint) {
color: $primary-text-color;
}
}
}
}
&__extra {
.public-account-bio {
border: 0;
}
.public-account-bio .account__header__fields {
border-color: lighten($ui-base-color, 8%);
}
}
}
}
.notification__filter-bar button.active::after,
.account__section-headline a.active::after {
border-color: transparent transparent $white;
}
.hero-widget,
.box-widget,
.contact-widget,
.landing-page__information.contact-widget,
.moved-account-widget,
.memoriam-widget,
.activity-stream,
.nothing-here,
.directory__tag > a,
.directory__tag > div,
.card > a,
.page-header,
.compose-form .compose-form__warning {
box-shadow: none;
}
.audio-player .video-player__controls button,
.audio-player .video-player__time-sep,
.audio-player .video-player__time-current,
.audio-player .video-player__time-total {
color: $primary-text-color;
}

@ -0,0 +1,41 @@
// Dependent colors
$black: #000000;
$white: #ffffff;
$classic-base-color: #282c37;
$classic-primary-color: #9baec8;
$classic-secondary-color: #d9e1e8;
$classic-highlight-color: #d92b2b;
// Differences
$success-green: lighten(#3c754d, 8%);
$base-overlay-background: $white !default;
$valid-value-color: $success-green !default;
$ui-base-color: $classic-secondary-color !default;
$ui-base-lighter-color: #b0c0cf;
$ui-primary-color: #9bcbed;
$ui-secondary-color: $classic-base-color !default;
$ui-highlight-color: #d92b2b;
$primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default;
$dark-text-color: #444b5d;
$action-button-color: #606984;
$inverted-text-color: $black !default;
$lighter-text-color: $classic-base-color !default;
$light-text-color: #444b5d;
//Newly added colors
$account-background-color: $white !default;
//Invert darkened and lightened colors
@function darken($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) + $amount);
}
@function lighten($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) - $amount);
}

@ -0,0 +1,10 @@
/*
Make search results look better:
- adds contrast to search icon
- overlay-style shadowed background
author: trwnh
license: Public Domain
*/
.search__icon .fa.active {opacity: 1}
.drawer__inner.darker {background: rgba(0,0,0,0.5)}

@ -0,0 +1,9 @@
/*
Add a line break between display name and account handle:
- this allows user/display names to expand more by default.
- it also makes names look better in general.
author: trwnh
license: Public Domain
*/
.display-name__html {display: block;}

@ -0,0 +1,20 @@
/*
Use browser default font:
- override mastodon-font-sans-serif with sans-serif
- note: this is not the same as "use system default font"
in mastodon's preferences! that option uses a font that
would be *expected to load on that system*, and ignores
your browser's settings entirely. for example, if you
are running ms windows, you will see segoe ui, even if
your browser's default font is something else!
author: trwnh
license: Public Domain
*/
body,
.landing-page #mastodon-timeline,
.landing-page li,
.landing-page p
{
font-family: sans-serif
}

@ -0,0 +1,15 @@
/*
* Rounded avatars:
* - adjust the border radius around all avatar elements.
* - default override is 50% (i.e. turn squares into circles),
* but you can set it to whatever you want.
*
* author: trwnh
* license: Public Domain
*/
.card .avatar img,
.activity-stream .status.light .status__avatar img,
.account__avatar,
.account__avatar-overlay-base,
.account__avatar-overlay-overlay
{border-radius: 50%}

@ -0,0 +1,37 @@
/*
Collapse fave/boost/poll notifications
- limits display to just a few lines (~3), so you can at least identify it
- hides the display name on fave/boost, because you already know you posted it
- tighter margins, remove space between CW and content
- hides the buttons, but you can expand a status to interact with it
author: trwnh
license: Public Domain
*/
.notification-favourite .status,
.notification-reblog .status,
.notification-poll .status{
max-height: 4em;
overflow: hidden;
}
.notification-favourite .display-name,
.notification-reblog .display-name {
display: none;
}
.notification-favourite .status__content,
.notification-reblog .status__content {
margin-top: -4px;
}
.notification-favourite .status__content p,
.notification-reblog .status__content p {
margin-bottom: 0px;
}
.notification-favourite .status__action-bar,
.notification-reblog .status__action-bar {
display: none;
}

@ -0,0 +1,23 @@
/*
Emoji hover zoom:
- makes emoji grow in size when moused over
author: noiob
license: CC0 - Public Domain
source: https://userstyles.org/styles/150165
*/
.emojione:hover
{
width: 50px !important;
/* set the width and height of the expanded emojo here */
height: 50px !important;
transition: all 0.3s ease-in-out !important;
/* the 0.3s is the animation time for growing the emojo, it can be set to 0 */;
}
.emojione
{
transition: all 0.2s ease-in-out;
/* the 0.2s is the animation time for shrinking the emojo, it can be set to 0 */;
}

@ -0,0 +1,9 @@
/*
Fade out faved/boosted toots in notifications:
- for "x favourited your toot" / "x boosted your toot",
make the faved/boosted toot half-transparent.
author: trwnh
license: Public Domain
*/
.status.muted {opacity: 0.5}

@ -0,0 +1,31 @@
/*
Full-height media previews:
- normal media previews are forced to be 16:9 for consistency
- use this if you prefer to see the aspect ratio unchanged
author: Kevin
license: CC0 - Public Domain
source: https://userstyles.org/styles/167207 [in part]
*/
.media-gallery {
max-height: 100% !important;
height: 100% !important;
}
.media-gallery__item-gifv-thumbnail, .media-gallery__item-gifv-thumbnail img {
transform: translateY(0%) !important;
max-height: 100% !important;
}
.media-gallery__item-thumbnail, .media-gallery__item-thumbnail img, .media-gallery__gifv {
max-height: 100% !important;
}
.media-gallery__item {
width: 100% !important;
height: 100% !important;
max-height: 100% !important;
inset: 0 !important;
margin-bottom: 4px;
}

@ -0,0 +1,11 @@
/*
Always show full name and handle:
- this removes the `...` and allows text to overflow past the column.
- this can look worse, but it can also prevent having to mouse over
to see the full name or handle.
- by default, it will also break long names onto a new line.
author: trwnh
license: Public Domain
*/
.display-name {overflow: visible; white-space: normal; word-wrap: break-word}

@ -0,0 +1,10 @@
/*
Hide the following and follower counters on profiles.
- full counts are still available by hovering over the text, though
author: trwnh
license: Public Domain
*/
.account__header__extra__links a:not(:first-child) strong
{display: none}
.details-counters .counter:not(:first-child) .counter-number
{visibility: hidden}

@ -0,0 +1,7 @@
/*
Hide the 0/1/1+ counters of replies.
author: trwnh
license: Public Domain
*/
.status__action-bar__counter__label {display: none}

@ -0,0 +1,16 @@
/*
Turn stars into hearts:
- similar to twitter's change
author: numimyon
license: CC0 - Public Domain
source: https://userstyles.org/styles/151233
*/
.notification__favourite-icon-wrapper .star-icon,
.star-icon.active,
.star-icon:hover,
.star-icon:active
{color: crimson !important;}
.fa-star:before {content: "";}

@ -0,0 +1,10 @@
/*
Remove the checker-board background from the media modal:
- this makes transparent images actually transparent
author: trwnh
license: Public Domain
*/
.media-modal canvas,
.media-modal img
{background: none}

@ -0,0 +1,12 @@
/*
Allow for wider layout on bigger screens
- vanilla max-width is 1200px
- there is no penalty to slightly expanding flexbox on bigger screens
- only applies on landing pages (webapp will expand as you add columns)
author: trwnh
license: Public Domain
*/
@media (min-width: 1600px) {
.landing-page .container {max-width: 1600px}
}

@ -0,0 +1,20 @@
/*
Release elephant friend from their confines:
- elephant friend will now hang out in the corner of your browser,
instead of being trapped in the drawer.
author: trwnh
license: Public Domain
*/
.drawer__inner, .drawer__inner__mastodon {
background: none; z-index: 0
}
.drawer__inner__mastodon > img {
position: fixed;
bottom: 0;
left: 0;
height: 180px;
z-index: -1
}
.compose-form {z-index: 1}
.drawer__inner {height: 100%} /* firefox bug highlights drawer text on click? */

@ -0,0 +1,13 @@
/*
Make "getting started" column height consistent with all other columns:
- puts the footer back at the bottom of the page, instead of floating.
author: trwnh
license: Public Domain
*/
.getting-started {
height: 100%;
display: flex;
flex-flow: column;
justify-content: space-between
}

@ -0,0 +1,17 @@
/*
Hide buttons that can't be clicked
- columns on /about and tag pages have buttons that don't work.
- so, this snippet hides those nonworking buttons to save space
- and to avoid confusion.
- unboostable buttons are made transparent on hover instead.
this is fixed in https://github.com/tootsuite/mastodon/pull/10054
author: trwnh
license: Public Domain
*/
.status__action-bar .icon-button.disabled:hover,
.notification-favourite .status.status-direct .icon-button.disabled:hover
{color: transparent !important}
#mastodon-timeline .status__action-bar {display: none}

@ -0,0 +1,9 @@
/*
Remove the "Filtered" tombstone from timelines.
- WARNING: this breaks keyboard scrolling with j/k!
author: trwnh
license: Public Domain
*/
.status__wrapper--filtered {display: none}

@ -0,0 +1,29 @@
/*
Bottom tabs on mobile:
- Places the tab bar at the bottom instead of the top.
- Fixes layout errors that are a result of this change.
author: trwnh
license: Public Domain
*/
@media (max-width: 630px) {
.tabs-bar {
position: fixed;
bottom: 0;
z-index: 1;
width: 100%;
margin: 0 !important;
}
.getting-started {overflow: auto} /* can be removed after PR #10075 is merged */
.columns-area {padding: 0}
.getting-started__trends, .getting-started__wrapper, .search {margin: 0}
.columns-area__panels__main, .tabs-bar__wrapper {padding: 0}
.floating-action-button, .column .scrollable > div:last-child {margin-bottom: 50px}
.react-swipeable-view-container {height: calc(100% - 50px)}
.react-swipeable-view-container .columns-area {height: 100% !important}
}

@ -0,0 +1,25 @@
/*
Single column layout:
- re-uses tab bar from mobile layout
- hides search from drawer (redundant with search tab)
author: trwnh
license: Public Domain
*/
@media (min-width: 1024px) {
/* place constraints on app layout */
.ui {max-width: 960px; max-height: 100vh;}
.drawer {width: 300px}
.column:last-child, .drawer:last-child
{display: flex; flex: 1 1 100%;}
/* show tabs bar (from mobile layout) as header */
.tabs-bar {display: flex;}
/* hide redundant ui elements */
.column,
.drawer__header,
.drawer:first-child .search,
.drawer:first-child .search-results
{display: none;}
.drawer:first-child .drawer__inner.darker {z-index: -1}
}

@ -0,0 +1,10 @@
/*
* Wider columns:
* - Make the multi-column layout use wider columns by default.
*
* author: trwnh
* license: Public Domain
*/
@media (min-width: 580px) {
.column, #mastodon-timeline {min-width: 55ch;}
}

@ -0,0 +1,12 @@
/*
Colorize logo on landing page:
- DO NOT IMPORT. It works as standalone CSS, but it makes Sass choke.
author: trwnh
license: Public Domain
*/
.landing-page__logo img {
filter: sepia(100%) hue-rotate(160deg) saturate(400%) brightness(40%);
mix-blend-mode: normal
}

@ -0,0 +1,11 @@
@import 'oe7drt-blue/variables';
@import 'application';
@import 'oe7drt-blue/diff';
//@import 'boost';
//@import 'mods/display_browserfont';
@import 'mods/display_breakname';
@import 'mods/display_fullname';
@import 'mods/display_emojizoom';
//@import 'mods/display_circleavatar';
@import 'mods/layout_1600px';
@import 'mods/layout_widercolumns';

@ -0,0 +1,77 @@
// components.scss
.compose-form {
.compose-form__modifiers {
.compose-form__upload {
&-description {
input {
&::placeholder {
opacity: 1;
}
}
}
}
}
}
.rich-formatting a,
.rich-formatting p a,
.rich-formatting li a,
.landing-page__short-description p a,
.status__content a,
.reply-indicator__content a {
color: lighten($ui-highlight-color, 12%);
text-decoration: none;
&.mention {
text-decoration: none;
}
&.mention span {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
&:hover,
&:focus,
&:active {
text-decoration: none;
}
&.status__content__spoiler-link {
color: $secondary-text-color;
text-decoration: none;
}
}
.status__content__read-more-button {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
.getting-started__footer a {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
.nothing-here {
color: $darker-text-color;
}
.public-layout .public-account-header__tabs__tabs .counter.active::after {
border-bottom: 4px solid $ui-highlight-color;
}

@ -0,0 +1,66 @@
// Commonly used web colors
$black: #000000; // Black
$white: #ffffff; // White
$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
$red-bookmark: $warning-red;
// Values from the classic Mastodon UI
$classic-base-color: #282c37; // Midnight Express
$classic-primary-color: #9baec8; // Echo Blue
$classic-secondary-color: #d9e1e8; // Pattens Blue
//$classic-highlight-color: #e7b01c; // Summer Sky
//$classic-highlight-color: #4285f4; // OE7DRT (new) Blue
//$classic-highlight-color: #1e57b6; // OE7DRT (new) Blue (darker) (this is a bit too dark)
//$classic-highlight-color: #3a74d5; // OE7DRT (new) Blue (darker) (use this)
//$classic-highlight-color: #7612cc; // OE7DRT violet (too dark)
//$classic-highlight-color: #8737cc; // OE7DRT violet (still a bit too dark)
//$classic-highlight-color: #9d59d8; // OE7DRT violet (damn white)
//$classic-highlight-color: #4c7899; // OE7DRT /\rch (not bad) bit too high
//$classic-highlight-color: #00bc8c; // OE7DRT /\rch (greenish like cloudlog)
$classic-highlight-color: #1b83c8; // OE7DRT /\dark (blue like chaos.social)
// Variables for defaults in UI
$base-shadow-color: $black !default;
$base-overlay-background: $black !default;
$base-border-color: $white !default;
$simple-background-color: $white !default;
$valid-value-color: $success-green !default;
$error-value-color: $error-red !default;
// Tell UI to use selected colors
$ui-base-color: $classic-base-color !default; // Darkest
$ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default;
// Variables for texts
$primary-text-color: $white !default;
$darker-text-color: $ui-primary-color !default;
$dark-text-color: $ui-base-lighter-color !default;
$secondary-text-color: $ui-secondary-color !default;
$highlight-text-color: $ui-highlight-color !default;
$action-button-color: $ui-base-lighter-color !default;
// For texts on inverted backgrounds
$inverted-text-color: $ui-base-color !default;
$lighter-text-color: $ui-base-lighter-color !default;
$light-text-color: $ui-primary-color !default;
// Language codes that uses CJK fonts
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
// Variables for components
$media-modal-media-max-width: 100%;
// put margins on top and bottom of image to avoid the screen covered by image.
$media-modal-media-max-height: 80%;
$no-gap-breakpoint: 415px;
$font-sans-serif: 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default;
$font-monospace: 'mastodon-font-monospace' !default;

@ -0,0 +1,11 @@
@import 'oe7drt-greeny/variables';
@import 'application';
@import 'oe7drt-greeny/diff';
//@import 'boost';
//@import 'mods/display_browserfont';
@import 'mods/display_breakname';
@import 'mods/display_fullname';
@import 'mods/display_emojizoom';
//@import 'mods/display_circleavatar';
@import 'mods/layout_1600px';
@import 'mods/layout_widercolumns';

@ -0,0 +1,77 @@
// components.scss
.compose-form {
.compose-form__modifiers {
.compose-form__upload {
&-description {
input {
&::placeholder {
opacity: 1;
}
}
}
}
}
}
.rich-formatting a,
.rich-formatting p a,
.rich-formatting li a,
.landing-page__short-description p a,
.status__content a,
.reply-indicator__content a {
color: lighten($ui-highlight-color, 12%);
text-decoration: none;
&.mention {
text-decoration: none;
}
&.mention span {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
&:hover,
&:focus,
&:active {
text-decoration: none;
}
&.status__content__spoiler-link {
color: $secondary-text-color;
text-decoration: none;
}
}
.status__content__read-more-button {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
.getting-started__footer a {
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
.nothing-here {
color: $darker-text-color;
}
.public-layout .public-account-header__tabs__tabs .counter.active::after {
border-bottom: 4px solid $ui-highlight-color;
}

@ -0,0 +1,66 @@
// Commonly used web colors
$black: #000000; // Black
$white: #ffffff; // White
$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
$red-bookmark: $warning-red;
// Values from the classic Mastodon UI
$classic-base-color: #282c37; // Midnight Express
$classic-primary-color: #9baec8; // Echo Blue
$classic-secondary-color: #d9e1e8; // Pattens Blue
//$classic-highlight-color: #e7b01c; // Summer Sky
//$classic-highlight-color: #4285f4; // OE7DRT (new) Blue
//$classic-highlight-color: #1e57b6; // OE7DRT (new) Blue (darker) (this is a bit too dark)
//$classic-highlight-color: #3a74d5; // OE7DRT (new) Blue (darker) (use this)
//$classic-highlight-color: #7612cc; // OE7DRT violet (too dark)
//$classic-highlight-color: #8737cc; // OE7DRT violet (still a bit too dark)
//$classic-highlight-color: #9d59d8; // OE7DRT violet (damn white)
//$classic-highlight-color: #4c7899; // OE7DRT /\rch (not bad) bit too high
$classic-highlight-color: #00bc8c; // OE7DRT /\rch (greenish like cloudlog)
//$classic-highlight-color: #1b83c8; // OE7DRT /\dark (blue like chaos.social)
// Variables for defaults in UI
$base-shadow-color: $black !default;
$base-overlay-background: $black !default;
$base-border-color: $white !default;
$simple-background-color: $white !default;
$valid-value-color: $success-green !default;
$error-value-color: $error-red !default;
// Tell UI to use selected colors
$ui-base-color: $classic-base-color !default; // Darkest
$ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default;
// Variables for texts
$primary-text-color: $white !default;
$darker-text-color: $ui-primary-color !default;
$dark-text-color: $ui-base-lighter-color !default;
$secondary-text-color: $ui-secondary-color !default;
$highlight-text-color: $ui-highlight-color !default;
$action-button-color: $ui-base-lighter-color !default;
// For texts on inverted backgrounds
$inverted-text-color: $ui-base-color !default;
$lighter-text-color: $ui-base-lighter-color !default;
$light-text-color: $ui-primary-color !default;
// Language codes that uses CJK fonts
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
// Variables for components
$media-modal-media-max-width: 100%;
// put margins on top and bottom of image to avoid the screen covered by image.
$media-modal-media-max-height: 80%;
$no-gap-breakpoint: 415px;
$font-sans-serif: 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default;
$font-monospace: 'mastodon-font-monospace' !default;

@ -0,0 +1,597 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: accounts
#
# id :bigint(8) not null, primary key
# username :string default(""), not null
# domain :string
# private_key :text
# public_key :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# note :text default(""), not null
# display_name :string default(""), not null
# uri :string default(""), not null
# url :string
# avatar_file_name :string
# avatar_content_type :string
# avatar_file_size :integer
# avatar_updated_at :datetime
# header_file_name :string
# header_content_type :string
# header_file_size :integer
# header_updated_at :datetime
# avatar_remote_url :string
# locked :boolean default(FALSE), not null
# header_remote_url :string default(""), not null
# last_webfingered_at :datetime
# inbox_url :string default(""), not null
# outbox_url :string default(""), not null
# shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null
# memorial :boolean default(FALSE), not null
# moved_to_account_id :bigint(8)
# featured_collection_url :string
# fields :jsonb
# actor_type :string
# discoverable :boolean
# also_known_as :string is an Array
# silenced_at :datetime
# suspended_at :datetime
# hide_collections :boolean
# avatar_storage_schema_version :integer
# header_storage_schema_version :integer
# devices_url :string
# suspension_origin :integer
# sensitized_at :datetime
# trendable :boolean
# reviewed_at :datetime
# requested_review_at :datetime
#
class Account < ApplicationRecord
self.ignored_columns = %w(
subscription_expires_at
secret
remote_url
salmon_url
hub_url
trust_level
)
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i
URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
USERNAME_ONLY_RE = /\A#{USERNAME_RE}\z/i
include Attachmentable
include AccountAssociations
include AccountAvatar
include AccountFinderConcern
include AccountHeader
include AccountInteractions
include Paginable
include AccountCounters
include DomainNormalizable
include DomainMaterializable
include AccountMerging
enum protocol: [:ostatus, :activitypub]
enum suspension_origin: [:local, :remote], _prefix: true
validates :username, presence: true
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
# Remote user validations, also applies to internal actors
validates :username, format: { with: USERNAME_ONLY_RE }, if: -> { (!local? || actor_type == 'Application') && will_save_change_to_username? }
# Local user validations
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' }
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' }
validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? }
validates :note, note_length: { maximum: 500 }, if: -> { local? && will_save_change_to_note? }
validates :fields, length: { maximum: 12 }, if: -> { local? && will_save_change_to_fields? }
scope :remote, -> { where.not(domain: nil) }
scope :local, -> { where(domain: nil) }
scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }
scope :silenced, -> { where.not(silenced_at: nil) }
scope :suspended, -> { where.not(suspended_at: nil) }
scope :sensitized, -> { where.not(sensitized_at: nil) }
scope :without_suspended, -> { where(suspended_at: nil) }
scope :without_silenced, -> { where(silenced_at: nil) }
scope :without_instance_actor, -> { where.not(id: -99) }
scope :recent, -> { reorder(id: :desc) }
scope :bots, -> { where(actor_type: %w(Application Service)) }
scope :groups, -> { where(actor_type: 'Group') }
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") }
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :without_unapproved, -> { left_outer_joins(:user).remote.or(left_outer_joins(:user).merge(User.approved.confirmed)) }
scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
scope :popular, -> { order('account_stats.followers_count desc') }
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches("%.#{domain}"))) }
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
delegate :email,
:unconfirmed_email,
:current_sign_in_at,
:created_at,
:sign_up_ip,
:confirmed?,
:approved?,
:pending?,
:disabled?,
:unconfirmed?,
:unconfirmed_or_pending?,
:role,
:locale,
:shows_application?,
:prefers_noindex?,
to: :user,
prefix: true,
allow_nil: true
delegate :chosen_languages, to: :user, prefix: false, allow_nil: true
update_index('accounts', :self)
def local?
domain.nil?
end
def moved?
moved_to_account_id.present?
end
def bot?
%w(Application Service).include? actor_type
end
def instance_actor?
id == -99
end
alias bot bot?
def bot=(val)
self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person'
end
def group?
actor_type == 'Group'
end
alias group group?
def acct
local? ? username : "#{username}@#{domain}"
end
def pretty_acct
local? ? username : "#{username}@#{Addressable::IDNA.to_unicode(domain)}"
end
def local_username_and_domain
"#{username}@#{Rails.configuration.x.local_domain}"
end
def local_followers_count
Follow.where(target_account_id: id).count
end
def to_webfinger_s
"acct:#{local_username_and_domain}"
end
def possibly_stale?
last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago
end
def refresh!
ResolveAccountService.new.call(acct) unless local?
end
def silenced?
silenced_at.present?
end
def silence!(date = Time.now.utc)
update!(silenced_at: date)
end
def unsilence!
update!(silenced_at: nil)
end
def suspended?
suspended_at.present? && !instance_actor?
end
def suspended_permanently?
suspended? && deletion_request.nil?
end
def suspended_temporarily?
suspended? && deletion_request.present?
end
def suspend!(date: Time.now.utc, origin: :local, block_email: true)
transaction do
create_deletion_request!
update!(suspended_at: date, suspension_origin: origin)
create_canonical_email_block! if block_email
end
end
def unsuspend!
transaction do
deletion_request&.destroy!
update!(suspended_at: nil, suspension_origin: nil)
destroy_canonical_email_block!
end
end
def sensitized?
sensitized_at.present?
end
def sensitize!(date = Time.now.utc)
update!(sensitized_at: date)
end
def unsensitize!
update!(sensitized_at: nil)
end
def memorialize!
update!(memorial: true)
end
def trendable?
boolean_with_default('trendable', Setting.trendable_by_default)
end
def sign?
true
end
def previous_strikes_count
strikes.where(overruled_at: nil).count
end
def keypair
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
end
def tags_as_strings=(tag_names)
hashtags_map = Tag.find_or_create_by_names(tag_names).index_by(&:name)
# Remove hashtags that are to be deleted
tags.each do |tag|
if hashtags_map.key?(tag.name)
hashtags_map.delete(tag.name)
else
tags.delete(tag)
end
end
# Add hashtags that were so far missing
hashtags_map.each_value do |tag|
tags << tag
end
end
def also_known_as
self[:also_known_as] || []
end
def fields
(self[:fields] || []).map do |f|
Account::Field.new(self, f)
rescue
nil
end.compact
end
def fields_attributes=(attributes)
fields = []
old_fields = self[:fields] || []
old_fields = [] if old_fields.is_a?(Hash)
if attributes.is_a?(Hash)
attributes.each_value do |attr|
next if attr[:name].blank?
previous = old_fields.find { |item| item['value'] == attr[:value] }
if previous && previous['verified_at'].present?
attr[:verified_at] = previous['verified_at']
end
fields << attr
end
end
self[:fields] = fields
end
DEFAULT_FIELDS_SIZE = 4
def build_fields
return if fields.size >= DEFAULT_FIELDS_SIZE
tmp = self[:fields] || []
tmp = [] if tmp.is_a?(Hash)
(DEFAULT_FIELDS_SIZE - tmp.size).times do
tmp << { name: '', value: '' }
end
self.fields = tmp
end
def save_with_optional_media!
save!
rescue ActiveRecord::RecordInvalid => e
errors = e.record.errors.errors
errors.each do |err|
if err.attribute == :avatar
self.avatar = nil
elsif err.attribute == :header
self.header = nil
end
end
save!
end
def hides_followers?
hide_collections?
end
def hides_following?
hide_collections?
end
def object_type
:person
end
def to_param
username
end
def to_log_human_identifier
acct
end
def excluded_from_timeline_account_ids
Rails.cache.fetch("exclude_account_ids_for:#{id}") { block_relationships.pluck(:target_account_id) + blocked_by_relationships.pluck(:account_id) + mute_relationships.pluck(:target_account_id) }
end
def excluded_from_timeline_domains
Rails.cache.fetch("exclude_domains_for:#{id}") { domain_blocks.pluck(:domain) }
end
def preferred_inbox_url
shared_inbox_url.presence || inbox_url
end
def synchronization_uri_prefix
return 'local' if local?
@synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
end
def requires_review?
reviewed_at.nil?
end
def reviewed?
reviewed_at.present?
end
def requested_review?
requested_review_at.present?
end
def requires_review_notification?
requires_review? && !requested_review?
end
class << self
DISALLOWED_TSQUERY_CHARACTERS = /['?\\:]/.freeze
TEXTSEARCH = "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))"
REPUTATION_SCORE_FUNCTION = '(greatest(0, coalesce(s.followers_count, 0)) / (greatest(0, coalesce(s.following_count, 0)) + 1.0))'
FOLLOWERS_SCORE_FUNCTION = 'log(greatest(0, coalesce(s.followers_count, 0)) + 2)'
TIME_DISTANCE_FUNCTION = '(case when s.last_status_at is null then 0 else exp(-1.0 * ((greatest(0, abs(extract(DAY FROM age(s.last_status_at))) - 30.0)^2) / (2.0 * ((-1.0 * 30^2) / (2.0 * ln(0.3)))))) end)'
BOOST = "((#{REPUTATION_SCORE_FUNCTION} + #{FOLLOWERS_SCORE_FUNCTION} + #{TIME_DISTANCE_FUNCTION}) / 3.0)"
def readonly_attributes
super - %w(statuses_count following_count followers_count)
end
def inboxes
urls = reorder(nil).where(protocol: :activitypub).group(:preferred_inbox_url).pluck(Arel.sql("coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url) AS preferred_inbox_url"))
DeliveryFailureTracker.without_unavailable(urls)
end
def search_for(terms, limit: 10, offset: 0)
tsquery = generate_query_for_search(terms)
sql = <<-SQL.squish
SELECT
accounts.*,
#{BOOST} * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
FROM accounts
LEFT JOIN users ON accounts.id = users.account_id
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL
AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
ORDER BY rank DESC
LIMIT :limit OFFSET :offset
SQL
records = find_by_sql([sql, limit: limit, offset: offset, tsquery: tsquery])
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
records
end
def advanced_search_for(terms, account, limit: 10, following: false, offset: 0)
tsquery = generate_query_for_search(terms)
sql = advanced_search_for_sql_template(following)
records = find_by_sql([sql, id: account.id, limit: limit, offset: offset, tsquery: tsquery])
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
records
end
def from_text(text)
return [] if text.blank?
text.scan(MENTION_RE).map { |match| match.first.split('@', 2) }.uniq.filter_map do |(username, domain)|
domain = begin
if TagManager.instance.local_domain?(domain)
nil
else
TagManager.instance.normalize_domain(domain)
end
end
EntityCache.instance.mention(username, domain)
end
end
private
def generate_query_for_search(unsanitized_terms)
terms = unsanitized_terms.gsub(DISALLOWED_TSQUERY_CHARACTERS, ' ')
# The final ":*" is for prefix search.
# The trailing space does not seem to fit any purpose, but `to_tsquery`
# behaves differently with and without a leading space if the terms start
# with `./`, `../`, or `.. `. I don't understand why, so, in doubt, keep
# the same query.
"' #{terms} ':*"
end
def advanced_search_for_sql_template(following)
if following
<<-SQL.squish
WITH first_degree AS (
SELECT target_account_id
FROM follows
WHERE account_id = :id
UNION ALL
SELECT :id
)
SELECT
accounts.*,
(count(f.id) + 1) * #{BOOST} * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
FROM accounts
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id)
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
WHERE accounts.id IN (SELECT * FROM first_degree)
AND to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL
GROUP BY accounts.id, s.id
ORDER BY rank DESC
LIMIT :limit OFFSET :offset
SQL
else
<<-SQL.squish
SELECT
accounts.*,
#{BOOST} * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank,
count(f.id) AS followed
FROM accounts
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id) OR (accounts.id = f.target_account_id AND f.account_id = :id)
LEFT JOIN users ON accounts.id = users.account_id
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL
AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
GROUP BY accounts.id, s.id
ORDER BY followed DESC, rank DESC
LIMIT :limit OFFSET :offset
SQL
end
end
end
def emojis
@emojis ||= CustomEmoji.from_text(emojifiable_text, domain)
end
before_create :generate_keys
before_validation :prepare_contents, if: :local?
before_validation :prepare_username, on: :create
before_destroy :clean_feed_manager
def ensure_keys!
return unless local? && private_key.blank? && public_key.blank?
generate_keys
save!
end
private
def prepare_contents
display_name&.strip!
note&.strip!
end
def prepare_username
username&.squish!
end
def generate_keys
return unless local? && private_key.blank? && public_key.blank?
keypair = OpenSSL::PKey::RSA.new(2048)
self.private_key = keypair.to_pem
self.public_key = keypair.public_key.to_pem
end
def normalize_domain
return if local?
super
end
def emojifiable_text
[note, display_name, fields.map(&:name), fields.map(&:value)].join(' ')
end
def clean_feed_manager
FeedManager.instance.clean_feeds!(:home, [id])
end
def create_canonical_email_block!
return unless local? && user_email.present?
begin
CanonicalEmailBlock.create(reference_account: self, email: user_email)
rescue ActiveRecord::RecordNotUnique
# A canonical e-mail block may already exist for the same e-mail
end
end
def destroy_canonical_email_block!
return unless local?
CanonicalEmailBlock.where(reference_account: self).delete_all
end
end

@ -0,0 +1,19 @@
# frozen_string_literal: true
class PollValidator < ActiveModel::Validator
MAX_OPTIONS = 12
MAX_OPTION_CHARS = 180
MAX_EXPIRATION = 1.month.freeze
MIN_EXPIRATION = 5.minutes.freeze
def validate(poll)
current_time = Time.now.utc
poll.errors.add(:options, I18n.t('polls.errors.too_few_options')) unless poll.options.size > 1
poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS
poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS }
poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time > MAX_EXPIRATION
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && (poll.expires_at - current_time).ceil < MIN_EXPIRATION
end
end

@ -0,0 +1,59 @@
# frozen_string_literal: true
class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = 2000
URL_PLACEHOLDER_CHARS = 23
URL_PLACEHOLDER = 'x' * 23
def validate(status)
return unless status.local? && !status.reblog?
status.errors.add(:text, I18n.t('statuses.over_character_limit', max: MAX_CHARS)) if too_long?(status)
end
private
def too_long?(status)
countable_length(combined_text(status)) > MAX_CHARS
end
def countable_length(str)
str.mb_chars.grapheme_length
end
def combined_text(status)
[status.spoiler_text, countable_text(status.text)].join
end
def countable_text(str)
return '' if str.blank?
# To ensure that we only give length concessions to entities that
# will be correctly parsed during formatting, we go through full
# entity extraction
entities = Extractor.remove_overlapping_entities(Extractor.extract_urls_with_indices(str, extract_url_without_protocol: false) + Extractor.extract_mentions_or_lists_with_indices(str))
rewrite_entities(str, entities) do |entity|
if entity[:url]
URL_PLACEHOLDER
elsif entity[:screen_name]
"@#{entity[:screen_name].split('@').first}"
end
end
end
def rewrite_entities(str, entities)
entities.sort_by! { |entity| entity[:indices].first }
result = ''.dup
last_index = entities.reduce(0) do |index, entity|
result << str[index...entity[:indices].first]
result << yield(entity)
entity[:indices].last
end
result << str[last_index..-1]
result
end
end

@ -0,0 +1,82 @@
# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
def host_to_url(str)
"http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}" unless str.blank?
end
base_host = Rails.configuration.x.web_domain
assets_host = Rails.configuration.action_controller.asset_host
assets_host ||= host_to_url(base_host)
media_host = host_to_url(ENV['S3_ALIAS_HOST'])
media_host ||= host_to_url(ENV['S3_CLOUDFRONT_HOST'])
media_host ||= host_to_url(ENV['S3_HOSTNAME']) if ENV['S3_ENABLED'] == 'true'
media_host ||= assets_host
google_fonts_host = "https://fonts.gstatic.com" # Google Fonts
Rails.application.config.content_security_policy do |p|
p.base_uri :none
p.default_src :none
p.frame_ancestors :none
p.font_src :self, assets_host, google_fonts_host
p.img_src :self, :https, :data, :blob, assets_host
p.style_src :self, assets_host
p.media_src :self, :https, :data, assets_host
p.frame_src :self, :https
p.manifest_src :self, assets_host
p.form_action :self
if Rails.env.development?
webpacker_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{Webpacker.dev_server.host_with_port}" }
p.connect_src :self, :data, :blob, assets_host, media_host, Rails.configuration.x.streaming_api_base_url, *webpacker_urls
p.script_src :self, :unsafe_inline, :unsafe_eval, assets_host
p.child_src :self, :blob, assets_host
p.worker_src :self, :blob, assets_host
else
p.connect_src :self, :data, :blob, assets_host, media_host, Rails.configuration.x.streaming_api_base_url
p.script_src :self, assets_host, "'wasm-unsafe-eval'"
p.child_src :self, :blob, assets_host
p.worker_src :self, :blob, assets_host
end
end
# Report CSP violations to a specified URI
# For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
Rails.application.config.content_security_policy_nonce_directives = %w(style-src)
Rails.application.reloader.to_prepare do
PgHero::HomeController.content_security_policy do |p|
p.script_src :self, :unsafe_inline, assets_host
p.style_src :self, :unsafe_inline, assets_host
end
PgHero::HomeController.after_action do
request.content_security_policy_nonce_generator = nil
end
if Rails.env.development?
LetterOpenerWeb::LettersController.content_security_policy do |p|
p.child_src :self
p.connect_src :none
p.frame_ancestors :self
p.frame_src :self
p.script_src :unsafe_inline
p.style_src :unsafe_inline
p.worker_src :none
end
LetterOpenerWeb::LettersController.after_action do |p|
request.content_security_policy_nonce_directives = %w(script-src)
end
end
end

@ -0,0 +1,11 @@
default: styles/application.scss
contrast: styles/contrast.scss
mastodon-light: styles/mastodon-light.scss
coffee-dark: styles/coffee-dark.scss
coffee-light: styles/coffee-light.scss
oe7drt-blue: styles/oe7drt-blue.scss
oe7drt-greeny: styles/oe7drt-greeny.scss
dark-red: styles/dark-red.scss
light-red: styles/light-red.scss
#cute: styles/cute.scss
#droid: styles/droid.scss

@ -0,0 +1,19 @@
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
User-agent: *
Disallow: /media_proxy/
Disallow: /interact/
Allow: /about
Disallow: /
Disallow: /users/*/followers
Disallow: /users/*/following
Disallow: /@*/media
Disallow: /@*/with_replies
Disallow: /@*/tagged/*
Disallow: /media_proxy/*
Disallow: /emoji/*
Disallow: /packs/*
Disallow: /sounds/*
Disallow: /system/*
Disallow: /avatars/*
Disallow: /headers/*
Loading…
Cancel
Save