diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4210d18671..bfc771ab98 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -13,7 +13,7 @@ Below are the guidelines for working on pull requests:
## General
-- 2 spaces indendation
+- 2 spaces indentation
## Documentation
diff --git a/Dockerfile b/Dockerfile
index 1f95f4f497..bcc911343c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,24 +1,31 @@
-FROM ruby:2.3.1
+FROM ruby:2.3.1-alpine
-ENV RAILS_ENV=production
-ENV NODE_ENV=production
-
-RUN echo 'deb http://httpredir.debian.org/debian jessie-backports main contrib non-free' >> /etc/apt/sources.list
-RUN curl -sL https://deb.nodesource.com/setup_4.x | bash -
-RUN apt-get update -qq && apt-get install -y build-essential libpq-dev libxml2-dev libxslt1-dev nodejs ffmpeg && rm -rf /var/lib/apt/lists/*
-RUN npm install -g npm@3 && npm install -g yarn
-RUN mkdir /mastodon
+ENV RAILS_ENV=production \
+ NODE_ENV=production
WORKDIR /mastodon
-ADD Gemfile /mastodon/Gemfile
-ADD Gemfile.lock /mastodon/Gemfile.lock
-RUN bundle install --deployment --without test development
+COPY . /mastodon
-ADD package.json /mastodon/package.json
-ADD yarn.lock /mastodon/yarn.lock
-RUN yarn
+RUN BUILD_DEPS=" \
+ postgresql-dev \
+ libxml2-dev \
+ libxslt-dev \
+ build-base" \
+ && apk -U upgrade && apk add \
+ $BUILD_DEPS \
+ nodejs \
+ libpq \
+ libxml2 \
+ libxslt \
+ ffmpeg \
+ file \
+ imagemagick \
+ && npm install -g npm@3 && npm install -g yarn \
+ && bundle install --deployment --without test development \
+ && yarn \
+ && npm cache clean \
+ && apk del $BUILD_DEPS \
+ && rm -rf /tmp/* /var/cache/apk/*
-ADD . /mastodon
-
-VOLUME ["/mastodon/public/system", "/mastodon/public/assets"]
+VOLUME /mastodon/public/system /mastodon/public/assets
diff --git a/Gemfile b/Gemfile
index 440f2e87b7..46baed3079 100644
--- a/Gemfile
+++ b/Gemfile
@@ -50,6 +50,8 @@ gem 'rails-settings-cached'
gem 'simple-navigation'
gem 'statsd-instrument'
gem 'ruby-oembed', require: 'oembed'
+gem 'rack-timeout'
+gem 'tzinfo-data'
gem 'react-rails'
gem 'browserify-rails'
@@ -89,5 +91,4 @@ group :production do
gem 'rails_12factor'
gem 'redis-rails'
gem 'lograge'
- gem 'rack-timeout'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 3ad5353793..6e3115249d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -423,6 +423,8 @@ GEM
unf (~> 0.1.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
+ tzinfo-data (1.2017.2)
+ tzinfo (>= 1.0.0)
uglifier (3.0.1)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
@@ -513,6 +515,7 @@ DEPENDENCIES
simplecov
statsd-instrument
twitter-text
+ tzinfo-data
uglifier (>= 1.3.0)
webmock
will_paginate
diff --git a/README.md b/README.md
index 592a4ed73c..20499e6e3b 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ Mastodon
[travis]: https://travis-ci.org/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
-Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
+Mastodon is a free, open-source social network server. A decentralized solution to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
An alternative implementation of the GNU social project. Based on ActivityStreams, Webfinger, PubsubHubbub and Salmon.
diff --git a/app/assets/images/fluffy-elephant-friend.png b/app/assets/images/fluffy-elephant-friend.png
index 11787e9360..f0df299278 100644
Binary files a/app/assets/images/fluffy-elephant-friend.png and b/app/assets/images/fluffy-elephant-friend.png differ
diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx
index 05fa8e68d0..37ebb99690 100644
--- a/app/assets/javascripts/components/actions/accounts.jsx
+++ b/app/assets/javascripts/components/actions/accounts.jsx
@@ -579,15 +579,18 @@ export function expandFollowingFail(id, error) {
};
};
-export function fetchRelationships(account_ids) {
+export function fetchRelationships(accountIds) {
return (dispatch, getState) => {
- if (account_ids.length === 0) {
+ const loadedRelationships = getState().get('relationships');
+ const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
+
+ if (newAccountIds.length === 0) {
return;
}
- dispatch(fetchRelationshipsRequest(account_ids));
+ dispatch(fetchRelationshipsRequest(newAccountIds));
- api(getState).get(`/api/v1/accounts/relationships?${account_ids.map(id => `id[]=${id}`).join('&')}`).then(response => {
+ api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess(response.data));
}).catch(error => {
dispatch(fetchRelationshipsFail(error));
diff --git a/app/assets/javascripts/components/actions/modal.jsx b/app/assets/javascripts/components/actions/modal.jsx
index d19218c485..615cd6bfe8 100644
--- a/app/assets/javascripts/components/actions/modal.jsx
+++ b/app/assets/javascripts/components/actions/modal.jsx
@@ -1,14 +1,11 @@
-export const MEDIA_OPEN = 'MEDIA_OPEN';
+export const MODAL_OPEN = 'MODAL_OPEN';
export const MODAL_CLOSE = 'MODAL_CLOSE';
-export const MODAL_INDEX_DECREASE = 'MODAL_INDEX_DECREASE';
-export const MODAL_INDEX_INCREASE = 'MODAL_INDEX_INCREASE';
-
-export function openMedia(media, index) {
+export function openModal(type, props) {
return {
- type: MEDIA_OPEN,
- media,
- index
+ type: MODAL_OPEN,
+ modalType: type,
+ modalProps: props
};
};
@@ -17,15 +14,3 @@ export function closeModal() {
type: MODAL_CLOSE
};
};
-
-export function decreaseIndexInModal() {
- return {
- type: MODAL_INDEX_DECREASE
- };
-};
-
-export function increaseIndexInModal() {
- return {
- type: MODAL_INDEX_INCREASE
- };
-};
diff --git a/app/assets/javascripts/components/actions/search.jsx b/app/assets/javascripts/components/actions/search.jsx
index e4af716eef..df3ae0db1a 100644
--- a/app/assets/javascripts/components/actions/search.jsx
+++ b/app/assets/javascripts/components/actions/search.jsx
@@ -1,9 +1,12 @@
import api from '../api'
-export const SEARCH_CHANGE = 'SEARCH_CHANGE';
-export const SEARCH_SUGGESTIONS_CLEAR = 'SEARCH_SUGGESTIONS_CLEAR';
-export const SEARCH_SUGGESTIONS_READY = 'SEARCH_SUGGESTIONS_READY';
-export const SEARCH_RESET = 'SEARCH_RESET';
+export const SEARCH_CHANGE = 'SEARCH_CHANGE';
+export const SEARCH_CLEAR = 'SEARCH_CLEAR';
+export const SEARCH_SHOW = 'SEARCH_SHOW';
+
+export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST';
+export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS';
+export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL';
export function changeSearch(value) {
return {
@@ -12,42 +15,59 @@ export function changeSearch(value) {
};
};
-export function clearSearchSuggestions() {
+export function clearSearch() {
return {
- type: SEARCH_SUGGESTIONS_CLEAR
+ type: SEARCH_CLEAR
};
};
-export function readySearchSuggestions(value, { accounts, hashtags, statuses }) {
- return {
- type: SEARCH_SUGGESTIONS_READY,
- value,
- accounts,
- hashtags,
- statuses
- };
-};
-
-export function fetchSearchSuggestions(value) {
+export function submitSearch() {
return (dispatch, getState) => {
- if (getState().getIn(['search', 'loaded_value']) === value) {
+ const value = getState().getIn(['search', 'value']);
+
+ if (value.length === 0) {
return;
}
+ dispatch(fetchSearchRequest());
+
api(getState).get('/api/v1/search', {
params: {
q: value,
- resolve: true,
- limit: 4
+ resolve: true
}
}).then(response => {
- dispatch(readySearchSuggestions(value, response.data));
+ dispatch(fetchSearchSuccess(response.data));
+ }).catch(error => {
+ dispatch(fetchSearchFail(error));
});
};
};
-export function resetSearch() {
+export function fetchSearchRequest() {
return {
- type: SEARCH_RESET
+ type: SEARCH_FETCH_REQUEST
+ };
+};
+
+export function fetchSearchSuccess(results) {
+ return {
+ type: SEARCH_FETCH_SUCCESS,
+ results,
+ accounts: results.accounts,
+ statuses: results.statuses
+ };
+};
+
+export function fetchSearchFail(error) {
+ return {
+ type: SEARCH_FETCH_FAIL,
+ error
+ };
+};
+
+export function showSearch() {
+ return {
+ type: SEARCH_SHOW
};
};
diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx
index 3e2d4ff431..6cd1f04b31 100644
--- a/app/assets/javascripts/components/actions/timelines.jsx
+++ b/app/assets/javascripts/components/actions/timelines.jsx
@@ -14,6 +14,9 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
+export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
+export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
+
export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) {
return {
type: TIMELINE_REFRESH_SUCCESS,
@@ -76,6 +79,11 @@ export function refreshTimeline(timeline, id = null) {
let skipLoading = false;
if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) {
+ if (id === null && getState().getIn(['timelines', timeline, 'online'])) {
+ // Skip refreshing when timeline is live anyway
+ return;
+ }
+
params = { ...params, since_id: newestId };
skipLoading = true;
}
@@ -162,3 +170,17 @@ export function scrollTopTimeline(timeline, top) {
top
};
};
+
+export function connectTimeline(timeline) {
+ return {
+ type: TIMELINE_CONNECT,
+ timeline
+ };
+};
+
+export function disconnectTimeline(timeline) {
+ return {
+ type: TIMELINE_DISCONNECT,
+ timeline
+ };
+};
diff --git a/app/assets/javascripts/components/components/lightbox.jsx b/app/assets/javascripts/components/components/lightbox.jsx
deleted file mode 100644
index f04ca47bae..0000000000
--- a/app/assets/javascripts/components/components/lightbox.jsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
-import IconButton from './icon_button';
-import { Motion, spring } from 'react-motion';
-import { injectIntl } from 'react-intl';
-
-const overlayStyle = {
- position: 'fixed',
- top: '0',
- left: '0',
- width: '100%',
- height: '100%',
- background: 'rgba(0, 0, 0, 0.5)',
- display: 'flex',
- justifyContent: 'center',
- alignContent: 'center',
- flexDirection: 'row',
- zIndex: '9999'
-};
-
-const dialogStyle = {
- color: '#282c37',
- boxShadow: '0 0 30px rgba(0, 0, 0, 0.8)',
- margin: 'auto',
- position: 'relative'
-};
-
-const closeStyle = {
- position: 'absolute',
- top: '4px',
- right: '4px'
-};
-
-const Lightbox = React.createClass({
-
- propTypes: {
- isVisible: React.PropTypes.bool,
- onOverlayClicked: React.PropTypes.func,
- onCloseClicked: React.PropTypes.func,
- intl: React.PropTypes.object.isRequired,
- children: React.PropTypes.node
- },
-
- mixins: [PureRenderMixin],
-
- componentDidMount () {
- this._listener = e => {
- if (this.props.isVisible && e.key === 'Escape') {
- this.props.onCloseClicked();
- }
- };
-
- window.addEventListener('keyup', this._listener);
- },
-
- componentWillUnmount () {
- window.removeEventListener('keyup', this._listener);
- },
-
- stopPropagation (e) {
- e.stopPropagation();
- },
-
- render () {
- const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props;
-
- return (
-