Merge pull request #518 from ThibG/glitch-soc/merge-master

Merge upstream changes
This commit is contained in:
ThibG 2018-05-25 21:41:25 +02:00 committed by GitHub
commit 2e58fed4c6
13 changed files with 330 additions and 319 deletions

View file

@ -13,21 +13,9 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE';
export function updateTimeline(timeline, status) { export function updateTimeline(timeline, status) {
return (dispatch, getState) => { 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 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)); dispatch(importFetchedStatus(status));
@ -37,14 +25,6 @@ export function updateTimeline(timeline, status) {
status, status,
references, references,
}); });
if (parents.length > 0) {
dispatch({
type: TIMELINE_CONTEXT_UPDATE,
status,
references: parents,
});
}
}; };
}; };

View file

@ -8,7 +8,7 @@ import ColumnHeader from '../../components/column_header';
import { expandCommunityTimeline } from '../../actions/timelines'; import { expandCommunityTimeline } from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn, changeColumnParams } from '../../actions/columns'; import { addColumn, removeColumn, moveColumn, changeColumnParams } from '../../actions/columns';
import ColumnSettingsContainer from './containers/column_settings_container'; import ColumnSettingsContainer from './containers/column_settings_container';
// import SectionHeadline from './components/section_headline'; import SectionHeadline from './components/section_headline';
import { connectCommunityStream } from '../../actions/streaming'; import { connectCommunityStream } from '../../actions/streaming';
const messages = defineMessages({ const messages = defineMessages({
@ -100,17 +100,15 @@ export default class CommunityTimeline extends React.PureComponent {
const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props; const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
// pending const headline = (
// <SectionHeadline
// const headline = ( timelineId='community'
// <SectionHeadline to='/timelines/public/local'
// timelineId='community' pinned={pinned}
// to='/timelines/public/local' onlyMedia={onlyMedia}
// pinned={pinned} onClick={this.handleHeadlineLinkClick}
// onlyMedia={onlyMedia} />
// onClick={this.handleHeadlineLinkClick} );
// />
// );
return ( return (
<Column ref={this.setRef}> <Column ref={this.setRef}>
@ -128,7 +126,7 @@ export default class CommunityTimeline extends React.PureComponent {
</ColumnHeader> </ColumnHeader>
<StatusListContainer <StatusListContainer
// prepend={headline} prepend={headline}
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`community_timeline-${columnId}`} scrollKey={`community_timeline-${columnId}`}
timelineId={`community${onlyMedia ? ':media' : ''}`} timelineId={`community${onlyMedia ? ':media' : ''}`}

View file

@ -8,7 +8,7 @@ import ColumnHeader from '../../components/column_header';
import { expandPublicTimeline } from '../../actions/timelines'; import { expandPublicTimeline } from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn, changeColumnParams } from '../../actions/columns'; import { addColumn, removeColumn, moveColumn, changeColumnParams } from '../../actions/columns';
import ColumnSettingsContainer from './containers/column_settings_container'; import ColumnSettingsContainer from './containers/column_settings_container';
// import SectionHeadline from '../community_timeline/components/section_headline'; import SectionHeadline from '../community_timeline/components/section_headline';
import { connectPublicStream } from '../../actions/streaming'; import { connectPublicStream } from '../../actions/streaming';
const messages = defineMessages({ const messages = defineMessages({
@ -100,17 +100,15 @@ export default class PublicTimeline extends React.PureComponent {
const { intl, columnId, hasUnread, multiColumn, onlyMedia } = this.props; const { intl, columnId, hasUnread, multiColumn, onlyMedia } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
// pending const headline = (
// <SectionHeadline
// const headline = ( timelineId='public'
// <SectionHeadline to='/timelines/public'
// timelineId='public' pinned={pinned}
// to='/timelines/public' onlyMedia={onlyMedia}
// pinned={pinned} onClick={this.handleHeadlineLinkClick}
// onlyMedia={onlyMedia} />
// onClick={this.handleHeadlineLinkClick} );
// />
// );
return ( return (
<Column ref={this.setRef}> <Column ref={this.setRef}>
@ -128,7 +126,7 @@ export default class PublicTimeline extends React.PureComponent {
</ColumnHeader> </ColumnHeader>
<StatusListContainer <StatusListContainer
// prepend={headline} prepend={headline}
timelineId={`public${onlyMedia ? ':media' : ''}`} timelineId={`public${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
trackScroll={!pinned} trackScroll={!pinned}

View file

@ -155,7 +155,7 @@ export default class ActionBar extends React.PureComponent {
<div className='detailed-status__action-bar'> <div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div> <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
{shareButton} {shareButton}
<div className='detailed-status__action-bar-dropdown'> <div className='detailed-status__action-bar-dropdown'>

View file

@ -1,3 +1,4 @@
import Immutable from 'immutable';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -54,11 +55,47 @@ const messages = defineMessages({
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => {
status: getStatus(state, props.params.statusId), const status = getStatus(state, props.params.statusId);
ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]), let ancestorsIds = Immutable.List();
descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]), 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; return mapStateToProps;
}; };

View file

@ -3,38 +3,62 @@ import {
ACCOUNT_MUTE_SUCCESS, ACCOUNT_MUTE_SUCCESS,
} from '../actions/accounts'; } from '../actions/accounts';
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; 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'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
ancestors: ImmutableMap(), inReplyTos: ImmutableMap(),
descendants: ImmutableMap(), replies: ImmutableMap(),
}); });
const normalizeContext = (state, id, ancestors, descendants) => { const normalizeContext = (immutableState, id, ancestors, descendants) => immutableState.withMutations(state => {
const ancestorsIds = ImmutableList(ancestors.map(ancestor => ancestor.id)); state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => {
const descendantsIds = ImmutableList(descendants.map(descendant => descendant.id)); 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 => { if (!siblings.includes(id)) {
map.setIn(['ancestors', id], ancestorsIds); const index = siblings.findLastIndex(sibling => sibling.id < id);
map.setIn(['descendants', id], descendantsIds); 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 => { const deleteFromContexts = (immutableState, ids) => immutableState.withMutations(state => {
state.update('ancestors', immutableAncestors => immutableAncestors.withMutations(ancestors => { state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => {
state.update('descendants', immutableDescendants => immutableDescendants.withMutations(descendants => { state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => {
ids.forEach(id => { ids.forEach(id => {
descendants.get(id, ImmutableList()).forEach(descendantId => { const inReplyToIdOfId = inReplyTos.get(id);
ancestors.update(descendantId, ImmutableList(), list => list.filterNot(itemId => itemId === id)); const repliesOfId = replies.get(id);
}); const siblings = replies.get(inReplyToIdOfId);
ancestors.get(id, ImmutableList()).forEach(ancestorId => { if (siblings) {
descendants.update(ancestorId, ImmutableList(), list => list.filterNot(itemId => itemId === id)); 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); return deleteFromContexts(state, ownedStatusIds);
}; };
const updateContext = (state, status, references) => { const updateContext = (state, status) => {
return state.update('descendants', map => { if (status.in_reply_to_id) {
references.forEach(parentId => { return state.withMutations(mutable => {
map = map.update(parentId, ImmutableList(), list => { const replies = mutable.getIn(['replies', status.in_reply_to_id], ImmutableList());
if (list.includes(status.id)) {
return list;
}
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) { switch(action.type) {
case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_BLOCK_SUCCESS:
case ACCOUNT_MUTE_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); return normalizeContext(state, action.id, action.ancestors, action.descendants);
case TIMELINE_DELETE: case TIMELINE_DELETE:
return deleteFromContexts(state, [action.id]); return deleteFromContexts(state, [action.id]);
case TIMELINE_CONTEXT_UPDATE: case TIMELINE_UPDATE:
return updateContext(state, action.status, action.references); return updateContext(state, action.status);
default: default:
return state; return state;
} }

File diff suppressed because one or more lines are too long

View file

@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($white)}"/></svg>') 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;
}
}
}

View file

@ -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);
}

View file

@ -396,7 +396,7 @@ $small-breakpoint: 960px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: $ui-primary-color; color: $darker-text-color;
text-decoration: none; text-decoration: none;
padding: 12px 16px; padding: 12px 16px;
line-height: 32px; line-height: 32px;

View file

@ -80,7 +80,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase
hashtag = Tag.where(name: hashtag).first_or_initialize(name: hashtag) hashtag = Tag.where(name: hashtag).first_or_initialize(name: hashtag)
status.tags << hashtag status.tags << hashtag unless status.tags.include?(hashtag)
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
nil nil
end end

View file

@ -771,9 +771,13 @@ en:
<hr class="spacer" /> <hr class="spacer" />
<h3 id="coppa">Children's Online Privacy Protection Act Compliance</h3> <h3 id="children">Site usage by children</h3>
<p>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 (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) do not use this site.</p> <p>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 (<a href="https://en.wikipedia.org/wiki/General_Data_Protection_Regulation">General Data Protection Regulation</a>) do not use this site.</p>
<p>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 (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) do not use this site.</p>
<p>Law requirements can be different if this server is in another jurisdiction.</p>
<hr class="spacer" /> <hr class="spacer" />

View file

@ -35,7 +35,7 @@ services:
image: tootsuite/mastodon image: tootsuite/mastodon
restart: always restart: always
env_file: .env.production 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: networks:
- external_network - external_network
- internal_network - internal_network