diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4210d1867..bfc771ab9 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 1f95f4f49..bcc911343 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 440f2e87b..46baed307 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 3ad535379..6e3115249 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 592a4ed73..20499e6e3 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 11787e936..f0df29927 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 05fa8e68d..37ebb9969 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 d19218c48..615cd6bfe 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 e4af716ee..df3ae0db1 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 3e2d4ff43..6cd1f04b3 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 f04ca47ba..000000000
--- 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 (
-