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