diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 8f54dfd8a01..11a199db6ae 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -13,21 +13,9 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; -export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE'; - export function updateTimeline(timeline, status) { return (dispatch, getState) => { const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : []; - const parents = []; - - if (status.in_reply_to_id) { - let parent = getState().getIn(['statuses', status.in_reply_to_id]); - - while (parent && parent.get('in_reply_to_id')) { - parents.push(parent.get('id')); - parent = getState().getIn(['statuses', parent.get('in_reply_to_id')]); - } - } dispatch(importFetchedStatus(status)); @@ -37,14 +25,6 @@ export function updateTimeline(timeline, status) { status, references, }); - - if (parents.length > 0) { - dispatch({ - type: TIMELINE_CONTEXT_UPDATE, - status, - references: parents, - }); - } }; }; diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index 58b8a8b731c..f9ee835bc66 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -8,7 +8,7 @@ import ColumnHeader from '../../components/column_header'; import { expandCommunityTimeline } from '../../actions/timelines'; import { addColumn, removeColumn, moveColumn, changeColumnParams } from '../../actions/columns'; import ColumnSettingsContainer from './containers/column_settings_container'; -// import SectionHeadline from './components/section_headline'; +import SectionHeadline from './components/section_headline'; import { connectCommunityStream } from '../../actions/streaming'; const messages = defineMessages({ @@ -100,17 +100,15 @@ export default class CommunityTimeline extends React.PureComponent { const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props; const pinned = !!columnId; - // pending - // - // const headline = ( - // - // ); + const headline = ( + + ); return ( @@ -128,7 +126,7 @@ export default class CommunityTimeline extends React.PureComponent { - // ); + const headline = ( + + ); return ( @@ -128,7 +126,7 @@ export default class PublicTimeline extends React.PureComponent {
-
+
{shareButton}
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index d5af2a459c5..2e53dfa7e7c 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -1,3 +1,4 @@ +import Immutable from 'immutable'; import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; @@ -54,11 +55,47 @@ const messages = defineMessages({ const makeMapStateToProps = () => { const getStatus = makeGetStatus(); - const mapStateToProps = (state, props) => ({ - status: getStatus(state, props.params.statusId), - ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]), - descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]), - }); + const mapStateToProps = (state, props) => { + const status = getStatus(state, props.params.statusId); + let ancestorsIds = Immutable.List(); + let descendantsIds = Immutable.List(); + + if (status) { + ancestorsIds = ancestorsIds.withMutations(mutable => { + function addAncestor(id) { + if (id) { + const inReplyTo = state.getIn(['contexts', 'inReplyTos', id]); + + mutable.unshift(id); + addAncestor(inReplyTo); + } + } + + addAncestor(status.get('in_reply_to_id')); + }); + + descendantsIds = descendantsIds.withMutations(mutable => { + function addDescendantOf(id) { + const replies = state.getIn(['contexts', 'replies', id]); + + if (replies) { + replies.forEach(reply => { + mutable.push(reply); + addDescendantOf(reply); + }); + } + } + + addDescendantOf(status.get('id')); + }); + } + + return { + status, + ancestorsIds, + descendantsIds, + }; + }; return mapStateToProps; }; diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js index ebd01e5327a..53e70b58ef6 100644 --- a/app/javascript/mastodon/reducers/contexts.js +++ b/app/javascript/mastodon/reducers/contexts.js @@ -3,38 +3,62 @@ import { ACCOUNT_MUTE_SUCCESS, } from '../actions/accounts'; import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; -import { TIMELINE_DELETE, TIMELINE_CONTEXT_UPDATE } from '../actions/timelines'; +import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; const initialState = ImmutableMap({ - ancestors: ImmutableMap(), - descendants: ImmutableMap(), + inReplyTos: ImmutableMap(), + replies: ImmutableMap(), }); -const normalizeContext = (state, id, ancestors, descendants) => { - const ancestorsIds = ImmutableList(ancestors.map(ancestor => ancestor.id)); - const descendantsIds = ImmutableList(descendants.map(descendant => descendant.id)); +const normalizeContext = (immutableState, id, ancestors, descendants) => immutableState.withMutations(state => { + state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => { + state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => { + function addReply({ id, in_reply_to_id }) { + if (in_reply_to_id) { + const siblings = replies.get(in_reply_to_id, ImmutableList()); - return state.withMutations(map => { - map.setIn(['ancestors', id], ancestorsIds); - map.setIn(['descendants', id], descendantsIds); - }); -}; + if (!siblings.includes(id)) { + const index = siblings.findLastIndex(sibling => sibling.id < id); + replies.set(in_reply_to_id, siblings.insert(index + 1, id)); + } + + inReplyTos.set(id, in_reply_to_id); + } + } + + if (ancestors[0]) { + addReply({ id, in_reply_to_id: ancestors[0].id }); + } + + if (descendants[0]) { + addReply({ id: descendants[0].id, in_reply_to_id: id }); + } + + [ancestors, descendants].forEach(statuses => statuses.forEach(addReply)); + })); + })); +}); const deleteFromContexts = (immutableState, ids) => immutableState.withMutations(state => { - state.update('ancestors', immutableAncestors => immutableAncestors.withMutations(ancestors => { - state.update('descendants', immutableDescendants => immutableDescendants.withMutations(descendants => { + state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => { + state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => { ids.forEach(id => { - descendants.get(id, ImmutableList()).forEach(descendantId => { - ancestors.update(descendantId, ImmutableList(), list => list.filterNot(itemId => itemId === id)); - }); + const inReplyToIdOfId = inReplyTos.get(id); + const repliesOfId = replies.get(id); + const siblings = replies.get(inReplyToIdOfId); - ancestors.get(id, ImmutableList()).forEach(ancestorId => { - descendants.update(ancestorId, ImmutableList(), list => list.filterNot(itemId => itemId === id)); - }); + if (siblings) { + replies.set(inReplyToIdOfId, siblings.filterNot(sibling => sibling === id)); + } - descendants.delete(id); - ancestors.delete(id); + + if (repliesOfId) { + repliesOfId.forEach(reply => inReplyTos.delete(reply)); + } + + inReplyTos.delete(id); + replies.delete(id); }); })); })); @@ -48,23 +72,23 @@ const filterContexts = (state, relationship, statuses) => { return deleteFromContexts(state, ownedStatusIds); }; -const updateContext = (state, status, references) => { - return state.update('descendants', map => { - references.forEach(parentId => { - map = map.update(parentId, ImmutableList(), list => { - if (list.includes(status.id)) { - return list; - } +const updateContext = (state, status) => { + if (status.in_reply_to_id) { + return state.withMutations(mutable => { + const replies = mutable.getIn(['replies', status.in_reply_to_id], ImmutableList()); - return list.push(status.id); - }); + mutable.setIn(['inReplyTos', status.id], status.in_reply_to_id); + + if (!replies.includes(status.id)) { + mutable.setIn(['replies', status.id], replies.push(status.id)); + } }); + } - return map; - }); + return state; }; -export default function contexts(state = initialState, action) { +export default function replies(state = initialState, action) { switch(action.type) { case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_MUTE_SUCCESS: @@ -73,8 +97,8 @@ export default function contexts(state = initialState, action) { return normalizeContext(state, action.id, action.ancestors, action.descendants); case TIMELINE_DELETE: return deleteFromContexts(state, [action.id]); - case TIMELINE_CONTEXT_UPDATE: - return updateContext(state, action.status, action.references); + case TIMELINE_UPDATE: + return updateContext(state, action.status); default: return state; } diff --git a/app/javascript/styles/mastodon-light.scss b/app/javascript/styles/mastodon-light.scss index 6a22a78226c..756a12d8689 100644 --- a/app/javascript/styles/mastodon-light.scss +++ b/app/javascript/styles/mastodon-light.scss @@ -1,228 +1,3 @@ -// Set variables -$ui-base-color: #d9e1e8; -$ui-base-lighter-color: darken($ui-base-color, 57%); -$ui-highlight-color: #2b90d9; -$ui-primary-color: darken($ui-highlight-color, 28%); -$ui-secondary-color: #282c37; - -$primary-text-color: black; -$base-overlay-background: $ui-base-color; - -$login-button-color: white; -$account-background-color: white; - -// Import defaults +@import 'mastodon-light/variables'; @import 'application'; - -// Change the color of the log in button -.button { - &.button-alternative-2 { - color: $login-button-color; - } -} - -// Change columns' default background colors -.column { - > .scrollable { - background: lighten($ui-base-color, 13%); - } -} - -.drawer__inner { - background: $ui-base-color; -} - -.drawer__inner__mastodon { - background: $ui-base-color url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto; -} - -// Change the default appearance of the content warning button -.status__content, -.reply-indicator__content { - - .status__content__spoiler-link { - - background: darken($ui-base-color, 30%); - - &:hover { - background: darken($ui-base-color, 35%); - text-decoration: none; - } - - } - -} - -// Change the default appearance of the action buttons -.icon-button { - - &:hover, - &:active, - &:focus { - color: darken($ui-base-color, 40%); - transition: color 200ms ease-out; - } - - &.disabled { - color: darken($ui-base-color, 30%); - } - -} - -.status { - &.status-direct { - .icon-button.disabled { - color: darken($ui-base-color, 30%); - } - } -} - -button.icon-button i.fa-retweet { - &:hover { - background-image: url("data:image/svg+xml;utf8,"); - } -} - -button.icon-button.disabled i.fa-retweet { - background-image: url("data:image/svg+xml;utf8,"); -} - -// Change the colors used in the dropdown menu -.dropdown-menu { - background: $ui-base-color; -} - -.dropdown-menu__arrow { - - &.left { - border-left-color: $ui-base-color; - } - - &.top { - border-top-color: $ui-base-color; - } - - &.bottom { - border-bottom-color: $ui-base-color; - } - - &.right { - border-right-color: $ui-base-color; - } - -} - -.dropdown-menu__item { - a { - background: $ui-base-color; - color: $ui-secondary-color; - } -} - -// Change the default color of several parts of the compose form -.compose-form { - - .compose-form__warning { - color: lighten($ui-secondary-color, 65%); - } - - strong { - color: lighten($ui-secondary-color, 65%); - } - - .autosuggest-textarea__textarea, - .spoiler-input__input { - - color: darken($ui-base-color, 80%); - - &::placeholder { - color: darken($ui-base-color, 70%); - } - - } - - .compose-form__buttons-wrapper { - background: darken($ui-base-color, 10%); - } - - .privacy-dropdown__option { - color: $ui-primary-color; - } - - .privacy-dropdown__option__content { - - strong { - color: $ui-primary-color; - } - - } - -} - -// Change the default color used for the text in an empty column or on the error column -.empty-column-indicator, -.error-column { - color: darken($ui-base-color, 60%); -} - -// Change the default colors used on some parts of the profile pages -.activity-stream-tabs { - - background: $account-background-color; - - a { - &.active { - color: $ui-primary-color; - } - } - -} - -.activity-stream { - - .entry { - background: $account-background-color; - } - - .status.light { - - .status__content { - color: $primary-text-color; - } - - .display-name { - strong { - color: $primary-text-color; - } - } - - } - -} - -.accounts-grid { - .account-grid-card { - - .controls { - .icon-button { - color: $ui-secondary-color; - } - } - - .name { - a { - color: $primary-text-color; - } - } - - .username { - color: $ui-secondary-color; - } - - .account__header__content { - color: $primary-text-color; - } - - } -} - +@import 'mastodon-light/diff'; diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss new file mode 100644 index 00000000000..42c790bac64 --- /dev/null +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -0,0 +1,157 @@ +// Notes! +// Sass color functions, "darken" and "lighten" are automatically replaced. + +// Change the colors of button texts +.button { + color: $white; + + &.button-alternative-2 { + color: $white; + } +} + +// Change default background colors of columns +.column { + > .scrollable { + background: $white; + } +} + +.drawer__inner { + background: $ui-base-color; +} + +.drawer__inner__mastodon { + background: $ui-base-color url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto; +} + +// Change the colors used in the dropdown menu +.dropdown-menu { + background: $ui-base-color; +} + +.dropdown-menu__arrow { + &.left { + border-left-color: $ui-base-color; + } + + &.top { + border-top-color: $ui-base-color; + } + + &.bottom { + border-bottom-color: $ui-base-color; + } + + &.right { + border-right-color: $ui-base-color; + } +} + +.dropdown-menu__item { + a { + background: $ui-base-color; + color: $ui-secondary-color; + } +} + +// Change the text colors on inverted background +.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 ul a.selected, +.simple_form .block-button, +.simple_form .button, +.simple_form button { + color: $white; +} + +// Change the background colors of modals +.actions-modal, +.boost-modal, +.confirmation-modal, +.mute-modal, +.report-modal { + background: $ui-secondary-color; +} + +.boost-modal__action-bar, +.confirmation-modal__action-bar, +.mute-modal__action-bar { + background: darken($ui-secondary-color, 6%); +} + +.react-toggle-track { + background: $ui-base-color; +} + +// 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; +} + +// Change the default colors used on some parts of the profile pages +.activity-stream-tabs { + background: $account-background-color; + + a { + &.active { + color: $ui-primary-color; + } + } +} + +.activity-stream { + .entry { + background: $account-background-color; + } + + .status.light { + .status__content { + color: $primary-text-color; + } + + .display-name { + strong { + color: $primary-text-color; + } + } + } +} + +.accounts-grid { + .account-grid-card { + .controls { + .icon-button { + color: $ui-secondary-color; + } + } + + .name { + a { + color: $primary-text-color; + } + } + + .username { + color: $ui-secondary-color; + } + + .account__header__content { + color: $primary-text-color; + } + } +} diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss new file mode 100644 index 00000000000..4be454e66a0 --- /dev/null +++ b/app/javascript/styles/mastodon-light/variables.scss @@ -0,0 +1,38 @@ +// Dependent colors +$black: #000000; +$white: #ffffff; + +$classic-base-color: #282c37; +$classic-primary-color: #9baec8; +$classic-secondary-color: #d9e1e8; +$classic-highlight-color: #2b90d9; + +// Differences +$base-overlay-background: $white; + +$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: #2b5fd9; + +$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; + +//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/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index c9c0e3081a0..77728995d13 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -396,7 +396,7 @@ $small-breakpoint: 960px; display: flex; justify-content: center; align-items: center; - color: $ui-primary-color; + color: $darker-text-color; text-decoration: none; padding: 12px 16px; line-height: 32px; diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index ab1d63cd496..010ed1bb5f0 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -80,7 +80,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase hashtag = Tag.where(name: hashtag).first_or_initialize(name: hashtag) - status.tags << hashtag + status.tags << hashtag unless status.tags.include?(hashtag) rescue ActiveRecord::RecordInvalid nil end diff --git a/config/locales/en.yml b/config/locales/en.yml index 68d5f835841..3c2a8c3db7f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -771,9 +771,13 @@ en:
-

Children's Online Privacy Protection Act Compliance

+

Site usage by children

-

Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site.

+

If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (General Data Protection Regulation) do not use this site.

+ +

If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site.

+ +

Law requirements can be different if this server is in another jurisdiction.


diff --git a/docker-compose.yml b/docker-compose.yml index 8058326dc68..496fb254874 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: image: tootsuite/mastodon restart: always env_file: .env.production - command: bundle exec rails s -p 3000 -b '0.0.0.0' + command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000 -b '0.0.0.0'" networks: - external_network - internal_network