diff --git a/patched-files/README.md b/patched-files/README.md new file mode 100644 index 0000000..190d7de --- /dev/null +++ b/patched-files/README.md @@ -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 CSP to +allow the use of Google Fonts (if you use the material theme (not in this +repo)). diff --git a/patched-files/app/javascript/mastodon/features/compose/components/compose_form.js b/patched-files/app/javascript/mastodon/features/compose/components/compose_form.js new file mode 100644 index 0000000..7aab424 --- /dev/null +++ b/patched-files/app/javascript/mastodon/features/compose/components/compose_form.js @@ -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 = {intl.formatMessage(messages.publish)}; + } else { + publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); + } + + return ( +
+ + + + +
+ +
+ + + + +
+ + +
+
+ +
+
+ + + + + +
+ +
+ +
+
+ +
+
+
+
+ + ); + } + +} diff --git a/patched-files/app/javascript/styles/boost.scss b/patched-files/app/javascript/styles/boost.scss new file mode 100644 index 0000000..67f9a85 --- /dev/null +++ b/patched-files/app/javascript/styles/boost.scss @@ -0,0 +1,61 @@ +@function hex-color($color) { + @if type-of($color) == 'color' { + $color: str-slice(ie-hex-str($color), 4); + } + @return '%23' + unquote($color) +} + +@mixin boost-svg($color) { + background-image: url("data:image/svg+xml;utf8,"); +} + +@mixin boost-locked-svg($color) { + background-image: url("data:image/svg+xml;utf8,"); +} + +@mixin boost-svg-single($color) { + background-image: url("data:image/svg+xml;utf8,"); +} + +@mixin boost-locked-svg-single($color) { + background-image: url("data:image/svg+xml;utf8,"); +} + +@mixin envelope($color) { + background-image: url("data:image/svg+xml;utf8,"); +} + +button.icon-button i.fa-retweet { + @include boost-svg-single($ui-base-lighter-color); +} + +.status-private button.icon-button i.fa-retweet { + @include boost-locked-svg-single($ui-base-lighter-color); +} + +// Disabled variant +button.icon-button.disabled i.fa-retweet { + @include boost-locked-svg-single(lighten($ui-base-color, 13%)); +} + +// Disabled variant for use with DMs +.status-direct button.icon-button.disabled i.fa-retweet { + @include envelope(lighten($ui-base-color, 16%)); + background-position: center center; + background-repeat: no-repeat; +} + +.no-reduce-motion button.icon-button i.fa-retweet { + transition: none; + background-position: 0px 684px; +} + +.no-reduce-motion button.icon-button.active i.fa-retweet { + @include boost-svg($ui-highlight-color); + transition: background-position 0.6s steps(36); + background-size: 22px 684px; + background-position: 0px 0px; +} +.no-reduce-motion .status-private button.icon-button.active i.fa-retweet { + @include boost-locked-svg($ui-highlight-color); +} diff --git a/patched-files/app/javascript/styles/coffee-dark.scss b/patched-files/app/javascript/styles/coffee-dark.scss new file mode 100644 index 0000000..6d61398 --- /dev/null +++ b/patched-files/app/javascript/styles/coffee-dark.scss @@ -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'; diff --git a/patched-files/app/javascript/styles/coffee-dark/diff.scss b/patched-files/app/javascript/styles/coffee-dark/diff.scss new file mode 100644 index 0000000..b612b87 --- /dev/null +++ b/patched-files/app/javascript/styles/coffee-dark/diff.scss @@ -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; +} diff --git a/patched-files/app/javascript/styles/coffee-dark/variables.scss b/patched-files/app/javascript/styles/coffee-dark/variables.scss new file mode 100644 index 0000000..9b7eb2e --- /dev/null +++ b/patched-files/app/javascript/styles/coffee-dark/variables.scss @@ -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; diff --git a/patched-files/app/javascript/styles/coffee-light.scss b/patched-files/app/javascript/styles/coffee-light.scss new file mode 100644 index 0000000..118721f --- /dev/null +++ b/patched-files/app/javascript/styles/coffee-light.scss @@ -0,0 +1,6 @@ +@import 'coffee-light/variables'; +@import 'application'; +@import 'coffee-light/diff'; +//@import 'boost'; +//@import 'mods/display_fullname'; +@import 'mods/display_circleavatar'; diff --git a/patched-files/app/javascript/styles/coffee-light/diff.scss b/patched-files/app/javascript/styles/coffee-light/diff.scss new file mode 100644 index 0000000..7a846bc --- /dev/null +++ b/patched-files/app/javascript/styles/coffee-light/diff.scss @@ -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,") 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,') 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; +} diff --git a/patched-files/app/javascript/styles/coffee-light/variables.scss b/patched-files/app/javascript/styles/coffee-light/variables.scss new file mode 100644 index 0000000..5faae83 --- /dev/null +++ b/patched-files/app/javascript/styles/coffee-light/variables.scss @@ -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); +} diff --git a/patched-files/app/javascript/styles/dark-red.scss b/patched-files/app/javascript/styles/dark-red.scss new file mode 100644 index 0000000..60c35c0 --- /dev/null +++ b/patched-files/app/javascript/styles/dark-red.scss @@ -0,0 +1,3 @@ +@import 'dark-red/variables'; +@import 'application'; +@import 'dark-red/diff'; diff --git a/patched-files/app/javascript/styles/dark-red/diff.scss b/patched-files/app/javascript/styles/dark-red/diff.scss new file mode 100644 index 0000000..b612b87 --- /dev/null +++ b/patched-files/app/javascript/styles/dark-red/diff.scss @@ -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; +} diff --git a/patched-files/app/javascript/styles/dark-red/variables.scss b/patched-files/app/javascript/styles/dark-red/variables.scss new file mode 100644 index 0000000..d8456fe --- /dev/null +++ b/patched-files/app/javascript/styles/dark-red/variables.scss @@ -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; diff --git a/patched-files/app/javascript/styles/fullwidth-media.scss b/patched-files/app/javascript/styles/fullwidth-media.scss new file mode 100644 index 0000000..f6a036d --- /dev/null +++ b/patched-files/app/javascript/styles/fullwidth-media.scss @@ -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; + } +} diff --git a/patched-files/app/javascript/styles/light-red.scss b/patched-files/app/javascript/styles/light-red.scss new file mode 100644 index 0000000..6969822 --- /dev/null +++ b/patched-files/app/javascript/styles/light-red.scss @@ -0,0 +1,3 @@ +@import 'light-red/variables'; +@import 'application'; +@import 'light-red/diff'; diff --git a/patched-files/app/javascript/styles/light-red/diff.scss b/patched-files/app/javascript/styles/light-red/diff.scss new file mode 100644 index 0000000..7a846bc --- /dev/null +++ b/patched-files/app/javascript/styles/light-red/diff.scss @@ -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,") 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,') 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; +} diff --git a/patched-files/app/javascript/styles/light-red/variables.scss b/patched-files/app/javascript/styles/light-red/variables.scss new file mode 100644 index 0000000..3bbef4a --- /dev/null +++ b/patched-files/app/javascript/styles/light-red/variables.scss @@ -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); +} diff --git a/patched-files/app/javascript/styles/mods/deprecated/display_bettersearch.css b/patched-files/app/javascript/styles/mods/deprecated/display_bettersearch.css new file mode 100644 index 0000000..c9bf885 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/deprecated/display_bettersearch.css @@ -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)} diff --git a/patched-files/app/javascript/styles/mods/display_breakname.css b/patched-files/app/javascript/styles/mods/display_breakname.css new file mode 100644 index 0000000..fc3936e --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_breakname.css @@ -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;} \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/display_browserfont.css b/patched-files/app/javascript/styles/mods/display_browserfont.css new file mode 100644 index 0000000..6389f20 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_browserfont.css @@ -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 +} \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/display_circleavatar.css b/patched-files/app/javascript/styles/mods/display_circleavatar.css new file mode 100644 index 0000000..b7d5d49 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_circleavatar.css @@ -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%} diff --git a/patched-files/app/javascript/styles/mods/display_collapsedinteractions.css b/patched-files/app/javascript/styles/mods/display_collapsedinteractions.css new file mode 100644 index 0000000..7cb1f7c --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_collapsedinteractions.css @@ -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; +} diff --git a/patched-files/app/javascript/styles/mods/display_emojizoom.css b/patched-files/app/javascript/styles/mods/display_emojizoom.css new file mode 100644 index 0000000..32ba536 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_emojizoom.css @@ -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 */; + } \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/display_fadedinteractions.css b/patched-files/app/javascript/styles/mods/display_fadedinteractions.css new file mode 100644 index 0000000..8945d26 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_fadedinteractions.css @@ -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} \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/display_fullmedia.css b/patched-files/app/javascript/styles/mods/display_fullmedia.css new file mode 100644 index 0000000..04cdbf5 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_fullmedia.css @@ -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; +} diff --git a/patched-files/app/javascript/styles/mods/display_fullname.css b/patched-files/app/javascript/styles/mods/display_fullname.css new file mode 100644 index 0000000..1f2e541 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_fullname.css @@ -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} \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/display_hidefollowcounts.css b/patched-files/app/javascript/styles/mods/display_hidefollowcounts.css new file mode 100644 index 0000000..e9ac9ed --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_hidefollowcounts.css @@ -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} diff --git a/patched-files/app/javascript/styles/mods/display_hidereplycounts.css b/patched-files/app/javascript/styles/mods/display_hidereplycounts.css new file mode 100644 index 0000000..513251c --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_hidereplycounts.css @@ -0,0 +1,7 @@ +/* +Hide the 0/1/1+ counters of replies. + +author: trwnh +license: Public Domain +*/ +.status__action-bar__counter__label {display: none} \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/display_starstohearts.css b/patched-files/app/javascript/styles/mods/display_starstohearts.css new file mode 100644 index 0000000..53efb55 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_starstohearts.css @@ -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: "";} diff --git a/patched-files/app/javascript/styles/mods/display_transparentmedia.css b/patched-files/app/javascript/styles/mods/display_transparentmedia.css new file mode 100644 index 0000000..afa18a1 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/display_transparentmedia.css @@ -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} \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/layout_1600px.css b/patched-files/app/javascript/styles/mods/layout_1600px.css new file mode 100644 index 0000000..f81af00 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/layout_1600px.css @@ -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} +} \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/layout_elefriend.css b/patched-files/app/javascript/styles/mods/layout_elefriend.css new file mode 100644 index 0000000..3d79a2c --- /dev/null +++ b/patched-files/app/javascript/styles/mods/layout_elefriend.css @@ -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? */ \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/layout_gettingstartedheight.css b/patched-files/app/javascript/styles/mods/layout_gettingstartedheight.css new file mode 100644 index 0000000..536a707 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/layout_gettingstartedheight.css @@ -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 +} \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/layout_hidedisabled.css b/patched-files/app/javascript/styles/mods/layout_hidedisabled.css new file mode 100644 index 0000000..3196db9 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/layout_hidedisabled.css @@ -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} \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/layout_hidefiltered.css b/patched-files/app/javascript/styles/mods/layout_hidefiltered.css new file mode 100644 index 0000000..f701e5f --- /dev/null +++ b/patched-files/app/javascript/styles/mods/layout_hidefiltered.css @@ -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} diff --git a/patched-files/app/javascript/styles/mods/layout_mobile_bottombar.css b/patched-files/app/javascript/styles/mods/layout_mobile_bottombar.css new file mode 100644 index 0000000..e5ddaf4 --- /dev/null +++ b/patched-files/app/javascript/styles/mods/layout_mobile_bottombar.css @@ -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} + +} diff --git a/patched-files/app/javascript/styles/mods/layout_singlecolumn.css b/patched-files/app/javascript/styles/mods/layout_singlecolumn.css new file mode 100644 index 0000000..88bf19e --- /dev/null +++ b/patched-files/app/javascript/styles/mods/layout_singlecolumn.css @@ -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} +} \ No newline at end of file diff --git a/patched-files/app/javascript/styles/mods/layout_widercolumns.css b/patched-files/app/javascript/styles/mods/layout_widercolumns.css new file mode 100644 index 0000000..557297c --- /dev/null +++ b/patched-files/app/javascript/styles/mods/layout_widercolumns.css @@ -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;} +} diff --git a/patched-files/app/javascript/styles/mods/test_colorizedlogo.css b/patched-files/app/javascript/styles/mods/test_colorizedlogo.css new file mode 100644 index 0000000..634ae5f --- /dev/null +++ b/patched-files/app/javascript/styles/mods/test_colorizedlogo.css @@ -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 +} \ No newline at end of file diff --git a/patched-files/app/javascript/styles/oe7drt-blue.scss b/patched-files/app/javascript/styles/oe7drt-blue.scss new file mode 100644 index 0000000..efde786 --- /dev/null +++ b/patched-files/app/javascript/styles/oe7drt-blue.scss @@ -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'; diff --git a/patched-files/app/javascript/styles/oe7drt-blue/diff.scss b/patched-files/app/javascript/styles/oe7drt-blue/diff.scss new file mode 100644 index 0000000..b612b87 --- /dev/null +++ b/patched-files/app/javascript/styles/oe7drt-blue/diff.scss @@ -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; +} diff --git a/patched-files/app/javascript/styles/oe7drt-blue/variables.scss b/patched-files/app/javascript/styles/oe7drt-blue/variables.scss new file mode 100644 index 0000000..d3f858a --- /dev/null +++ b/patched-files/app/javascript/styles/oe7drt-blue/variables.scss @@ -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; diff --git a/patched-files/app/javascript/styles/oe7drt-greeny.scss b/patched-files/app/javascript/styles/oe7drt-greeny.scss new file mode 100644 index 0000000..993c3bb --- /dev/null +++ b/patched-files/app/javascript/styles/oe7drt-greeny.scss @@ -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'; diff --git a/patched-files/app/javascript/styles/oe7drt-greeny/diff.scss b/patched-files/app/javascript/styles/oe7drt-greeny/diff.scss new file mode 100644 index 0000000..b612b87 --- /dev/null +++ b/patched-files/app/javascript/styles/oe7drt-greeny/diff.scss @@ -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; +} diff --git a/patched-files/app/javascript/styles/oe7drt-greeny/variables.scss b/patched-files/app/javascript/styles/oe7drt-greeny/variables.scss new file mode 100644 index 0000000..34db656 --- /dev/null +++ b/patched-files/app/javascript/styles/oe7drt-greeny/variables.scss @@ -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; diff --git a/patched-files/app/models/account.rb b/patched-files/app/models/account.rb new file mode 100644 index 0000000..d144ef5 --- /dev/null +++ b/patched-files/app/models/account.rb @@ -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 diff --git a/patched-files/app/validators/poll_validator.rb b/patched-files/app/validators/poll_validator.rb new file mode 100644 index 0000000..b5e380c --- /dev/null +++ b/patched-files/app/validators/poll_validator.rb @@ -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 diff --git a/patched-files/app/validators/status_length_validator.rb b/patched-files/app/validators/status_length_validator.rb new file mode 100644 index 0000000..5e2641d --- /dev/null +++ b/patched-files/app/validators/status_length_validator.rb @@ -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 diff --git a/patched-files/config/initializers/content_security_policy.rb b/patched-files/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..bf7b7ed --- /dev/null +++ b/patched-files/config/initializers/content_security_policy.rb @@ -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 diff --git a/patched-files/config/themes.yml b/patched-files/config/themes.yml new file mode 100644 index 0000000..77a8a55 --- /dev/null +++ b/patched-files/config/themes.yml @@ -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 diff --git a/patched-files/public/robots.txt b/patched-files/public/robots.txt new file mode 100644 index 0000000..e5cbdc3 --- /dev/null +++ b/patched-files/public/robots.txt @@ -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/*