Merge branch 'master' into development
This commit is contained in:
		
						commit
						3abb0f7bc7
					
				| 
						 | 
					@ -13,7 +13,7 @@ Below are the guidelines for working on pull requests:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## General
 | 
					## General
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- 2 spaces indendation
 | 
					- 2 spaces indentation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Documentation
 | 
					## Documentation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										43
									
								
								Dockerfile
								
								
								
								
							
							
						
						
									
										43
									
								
								Dockerfile
								
								
								
								
							| 
						 | 
					@ -1,24 +1,31 @@
 | 
				
			||||||
FROM ruby:2.3.1
 | 
					FROM ruby:2.3.1-alpine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENV RAILS_ENV=production
 | 
					ENV RAILS_ENV=production \
 | 
				
			||||||
ENV NODE_ENV=production
 | 
					    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
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /mastodon
 | 
					WORKDIR /mastodon
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ADD Gemfile /mastodon/Gemfile
 | 
					COPY . /mastodon
 | 
				
			||||||
ADD Gemfile.lock /mastodon/Gemfile.lock
 | 
					 | 
				
			||||||
RUN bundle install --deployment --without test development
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
ADD package.json /mastodon/package.json
 | 
					RUN BUILD_DEPS=" \
 | 
				
			||||||
ADD yarn.lock /mastodon/yarn.lock
 | 
					    postgresql-dev \
 | 
				
			||||||
RUN yarn
 | 
					    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"]
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										3
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										3
									
								
								Gemfile
								
								
								
								
							| 
						 | 
					@ -50,6 +50,8 @@ gem 'rails-settings-cached'
 | 
				
			||||||
gem 'simple-navigation'
 | 
					gem 'simple-navigation'
 | 
				
			||||||
gem 'statsd-instrument'
 | 
					gem 'statsd-instrument'
 | 
				
			||||||
gem 'ruby-oembed', require: 'oembed'
 | 
					gem 'ruby-oembed', require: 'oembed'
 | 
				
			||||||
 | 
					gem 'rack-timeout'
 | 
				
			||||||
 | 
					gem 'tzinfo-data'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gem 'react-rails'
 | 
					gem 'react-rails'
 | 
				
			||||||
gem 'browserify-rails'
 | 
					gem 'browserify-rails'
 | 
				
			||||||
| 
						 | 
					@ -89,5 +91,4 @@ group :production do
 | 
				
			||||||
  gem 'rails_12factor'
 | 
					  gem 'rails_12factor'
 | 
				
			||||||
  gem 'redis-rails'
 | 
					  gem 'redis-rails'
 | 
				
			||||||
  gem 'lograge'
 | 
					  gem 'lograge'
 | 
				
			||||||
  gem 'rack-timeout'
 | 
					 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -423,6 +423,8 @@ GEM
 | 
				
			||||||
      unf (~> 0.1.0)
 | 
					      unf (~> 0.1.0)
 | 
				
			||||||
    tzinfo (1.2.2)
 | 
					    tzinfo (1.2.2)
 | 
				
			||||||
      thread_safe (~> 0.1)
 | 
					      thread_safe (~> 0.1)
 | 
				
			||||||
 | 
					    tzinfo-data (1.2017.2)
 | 
				
			||||||
 | 
					      tzinfo (>= 1.0.0)
 | 
				
			||||||
    uglifier (3.0.1)
 | 
					    uglifier (3.0.1)
 | 
				
			||||||
      execjs (>= 0.3.0, < 3)
 | 
					      execjs (>= 0.3.0, < 3)
 | 
				
			||||||
    unf (0.1.4)
 | 
					    unf (0.1.4)
 | 
				
			||||||
| 
						 | 
					@ -513,6 +515,7 @@ DEPENDENCIES
 | 
				
			||||||
  simplecov
 | 
					  simplecov
 | 
				
			||||||
  statsd-instrument
 | 
					  statsd-instrument
 | 
				
			||||||
  twitter-text
 | 
					  twitter-text
 | 
				
			||||||
 | 
					  tzinfo-data
 | 
				
			||||||
  uglifier (>= 1.3.0)
 | 
					  uglifier (>= 1.3.0)
 | 
				
			||||||
  webmock
 | 
					  webmock
 | 
				
			||||||
  will_paginate
 | 
					  will_paginate
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ Mastodon
 | 
				
			||||||
[travis]: https://travis-ci.org/tootsuite/mastodon
 | 
					[travis]: https://travis-ci.org/tootsuite/mastodon
 | 
				
			||||||
[code_climate]: https://codeclimate.com/github/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.
 | 
					An alternative implementation of the GNU social project. Based on ActivityStreams, Webfinger, PubsubHubbub and Salmon.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 59 KiB  | 
| 
						 | 
					@ -579,15 +579,18 @@ export function expandFollowingFail(id, error) {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchRelationships(account_ids) {
 | 
					export function fetchRelationships(accountIds) {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  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;
 | 
					      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));
 | 
					      dispatch(fetchRelationshipsSuccess(response.data));
 | 
				
			||||||
    }).catch(error => {
 | 
					    }).catch(error => {
 | 
				
			||||||
      dispatch(fetchRelationshipsFail(error));
 | 
					      dispatch(fetchRelationshipsFail(error));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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_CLOSE = 'MODAL_CLOSE';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MODAL_INDEX_DECREASE = 'MODAL_INDEX_DECREASE';
 | 
					export function openModal(type, props) {
 | 
				
			||||||
export const MODAL_INDEX_INCREASE = 'MODAL_INDEX_INCREASE';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function openMedia(media, index) {
 | 
					 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: MEDIA_OPEN,
 | 
					    type: MODAL_OPEN,
 | 
				
			||||||
    media,
 | 
					    modalType: type,
 | 
				
			||||||
    index
 | 
					    modalProps: props
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,15 +14,3 @@ export function closeModal() {
 | 
				
			||||||
    type: MODAL_CLOSE
 | 
					    type: MODAL_CLOSE
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					 | 
				
			||||||
export function decreaseIndexInModal() {
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    type: MODAL_INDEX_DECREASE
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function increaseIndexInModal() {
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    type: MODAL_INDEX_INCREASE
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,12 @@
 | 
				
			||||||
import api from '../api'
 | 
					import api from '../api'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SEARCH_CHANGE            = 'SEARCH_CHANGE';
 | 
					export const SEARCH_CHANGE = 'SEARCH_CHANGE';
 | 
				
			||||||
export const SEARCH_SUGGESTIONS_CLEAR = 'SEARCH_SUGGESTIONS_CLEAR';
 | 
					export const SEARCH_CLEAR  = 'SEARCH_CLEAR';
 | 
				
			||||||
export const SEARCH_SUGGESTIONS_READY = 'SEARCH_SUGGESTIONS_READY';
 | 
					export const SEARCH_SHOW   = 'SEARCH_SHOW';
 | 
				
			||||||
export const SEARCH_RESET             = 'SEARCH_RESET';
 | 
					
 | 
				
			||||||
 | 
					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) {
 | 
					export function changeSearch(value) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
| 
						 | 
					@ -12,42 +15,59 @@ export function changeSearch(value) {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function clearSearchSuggestions() {
 | 
					export function clearSearch() {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: SEARCH_SUGGESTIONS_CLEAR
 | 
					    type: SEARCH_CLEAR
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function readySearchSuggestions(value, { accounts, hashtags, statuses }) {
 | 
					export function submitSearch() {
 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    type: SEARCH_SUGGESTIONS_READY,
 | 
					 | 
				
			||||||
    value,
 | 
					 | 
				
			||||||
    accounts,
 | 
					 | 
				
			||||||
    hashtags,
 | 
					 | 
				
			||||||
    statuses
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function fetchSearchSuggestions(value) {
 | 
					 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
    if (getState().getIn(['search', 'loaded_value']) === value) {
 | 
					    const value = getState().getIn(['search', 'value']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (value.length === 0) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dispatch(fetchSearchRequest());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api(getState).get('/api/v1/search', {
 | 
					    api(getState).get('/api/v1/search', {
 | 
				
			||||||
      params: {
 | 
					      params: {
 | 
				
			||||||
        q: value,
 | 
					        q: value,
 | 
				
			||||||
        resolve: true,
 | 
					        resolve: true
 | 
				
			||||||
        limit: 4
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }).then(response => {
 | 
					    }).then(response => {
 | 
				
			||||||
      dispatch(readySearchSuggestions(value, response.data));
 | 
					      dispatch(fetchSearchSuccess(response.data));
 | 
				
			||||||
 | 
					    }).catch(error => {
 | 
				
			||||||
 | 
					      dispatch(fetchSearchFail(error));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function resetSearch() {
 | 
					export function fetchSearchRequest() {
 | 
				
			||||||
  return {
 | 
					  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
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,9 @@ export const TIMELINE_EXPAND_FAIL    = 'TIMELINE_EXPAND_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
 | 
					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) {
 | 
					export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: TIMELINE_REFRESH_SUCCESS,
 | 
					    type: TIMELINE_REFRESH_SUCCESS,
 | 
				
			||||||
| 
						 | 
					@ -76,6 +79,11 @@ export function refreshTimeline(timeline, id = null) {
 | 
				
			||||||
    let skipLoading = false;
 | 
					    let skipLoading = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) {
 | 
					    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 };
 | 
					      params          = { ...params, since_id: newestId };
 | 
				
			||||||
      skipLoading     = true;
 | 
					      skipLoading     = true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -162,3 +170,17 @@ export function scrollTopTimeline(timeline, top) {
 | 
				
			||||||
    top
 | 
					    top
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function connectTimeline(timeline) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: TIMELINE_CONNECT,
 | 
				
			||||||
 | 
					    timeline
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function disconnectTimeline(timeline) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: TIMELINE_DISCONNECT,
 | 
				
			||||||
 | 
					    timeline
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 (
 | 
					 | 
				
			||||||
      <Motion defaultStyle={{ backgroundOpacity: 0, opacity: 0, y: -400 }} style={{ backgroundOpacity: spring(isVisible ? 50 : 0), opacity: isVisible ? spring(200) : 0, y: spring(isVisible ? 0 : -400, { stiffness: 150, damping: 12 }) }}>
 | 
					 | 
				
			||||||
        {({ backgroundOpacity, opacity, y }) =>
 | 
					 | 
				
			||||||
          <div className='lightbox' style={{...overlayStyle, background: `rgba(0, 0, 0, ${backgroundOpacity / 100})`, display: Math.floor(backgroundOpacity) === 0 ? 'none' : 'flex', pointerEvents: !isVisible ? 'none' : 'auto'}} onClick={onOverlayClicked}>
 | 
					 | 
				
			||||||
            <div style={{...dialogStyle, transform: `translateY(${y}px)`, opacity: opacity / 100 }} onClick={this.stopPropagation}>
 | 
					 | 
				
			||||||
              <IconButton title={intl.formatMessage({ id: 'lightbox.close', defaultMessage: 'Close' })} icon='times' onClick={onCloseClicked} size={16} style={closeStyle} />
 | 
					 | 
				
			||||||
              {children}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      </Motion>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default injectIntl(Lightbox);
 | 
					 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl';
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  delete: { id: 'status.delete', defaultMessage: 'Delete' },
 | 
					  delete: { id: 'status.delete', defaultMessage: 'Delete' },
 | 
				
			||||||
  mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
 | 
					  mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
 | 
				
			||||||
 | 
					  mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
 | 
				
			||||||
  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
 | 
					  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
 | 
				
			||||||
  reply: { id: 'status.reply', defaultMessage: 'Reply' },
 | 
					  reply: { id: 'status.reply', defaultMessage: 'Reply' },
 | 
				
			||||||
  reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
 | 
					  reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
 | 
				
			||||||
| 
						 | 
					@ -28,6 +29,7 @@ const StatusActionBar = React.createClass({
 | 
				
			||||||
    onReblog: React.PropTypes.func,
 | 
					    onReblog: React.PropTypes.func,
 | 
				
			||||||
    onDelete: React.PropTypes.func,
 | 
					    onDelete: React.PropTypes.func,
 | 
				
			||||||
    onMention: React.PropTypes.func,
 | 
					    onMention: React.PropTypes.func,
 | 
				
			||||||
 | 
					    onMute: React.PropTypes.func,
 | 
				
			||||||
    onBlock: React.PropTypes.func,
 | 
					    onBlock: React.PropTypes.func,
 | 
				
			||||||
    onReport: React.PropTypes.func,
 | 
					    onReport: React.PropTypes.func,
 | 
				
			||||||
    me: React.PropTypes.number.isRequired,
 | 
					    me: React.PropTypes.number.isRequired,
 | 
				
			||||||
| 
						 | 
					@ -56,6 +58,10 @@ const StatusActionBar = React.createClass({
 | 
				
			||||||
    this.props.onMention(this.props.status.get('account'), this.context.router);
 | 
					    this.props.onMention(this.props.status.get('account'), this.context.router);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleMuteClick () {
 | 
				
			||||||
 | 
					    this.props.onMute(this.props.status.get('account'));
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleBlockClick () {
 | 
					  handleBlockClick () {
 | 
				
			||||||
    this.props.onBlock(this.props.status.get('account'));
 | 
					    this.props.onBlock(this.props.status.get('account'));
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -81,6 +87,7 @@ const StatusActionBar = React.createClass({
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
 | 
					      menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
 | 
				
			||||||
      menu.push(null);
 | 
					      menu.push(null);
 | 
				
			||||||
 | 
					      menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
 | 
					      menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
 | 
					      menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,8 @@ const muteStyle = {
 | 
				
			||||||
  position: 'absolute',
 | 
					  position: 'absolute',
 | 
				
			||||||
  top: '10px',
 | 
					  top: '10px',
 | 
				
			||||||
  right: '10px',
 | 
					  right: '10px',
 | 
				
			||||||
 | 
					  color: 'white',
 | 
				
			||||||
 | 
					  textShadow: "0px 1px 1px black, 1px 0px 1px black",
 | 
				
			||||||
  opacity: '0.8',
 | 
					  opacity: '0.8',
 | 
				
			||||||
  zIndex: '5'
 | 
					  zIndex: '5'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -54,6 +56,8 @@ const spoilerButtonStyle = {
 | 
				
			||||||
  position: 'absolute',
 | 
					  position: 'absolute',
 | 
				
			||||||
  top: '6px',
 | 
					  top: '6px',
 | 
				
			||||||
  left: '8px',
 | 
					  left: '8px',
 | 
				
			||||||
 | 
					  color: 'white',
 | 
				
			||||||
 | 
					  textShadow: "0px 1px 1px black, 1px 0px 1px black",
 | 
				
			||||||
  zIndex: '100'
 | 
					  zIndex: '100'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,9 @@ import {
 | 
				
			||||||
  refreshTimelineSuccess,
 | 
					  refreshTimelineSuccess,
 | 
				
			||||||
  updateTimeline,
 | 
					  updateTimeline,
 | 
				
			||||||
  deleteFromTimelines,
 | 
					  deleteFromTimelines,
 | 
				
			||||||
  refreshTimeline
 | 
					  refreshTimeline,
 | 
				
			||||||
 | 
					  connectTimeline,
 | 
				
			||||||
 | 
					  disconnectTimeline
 | 
				
			||||||
} from '../actions/timelines';
 | 
					} from '../actions/timelines';
 | 
				
			||||||
import { updateNotifications, refreshNotifications } from '../actions/notifications';
 | 
					import { updateNotifications, refreshNotifications } from '../actions/notifications';
 | 
				
			||||||
import createBrowserHistory from 'history/lib/createBrowserHistory';
 | 
					import createBrowserHistory from 'history/lib/createBrowserHistory';
 | 
				
			||||||
| 
						 | 
					@ -44,6 +46,7 @@ import fr from 'react-intl/locale-data/fr';
 | 
				
			||||||
import pt from 'react-intl/locale-data/pt';
 | 
					import pt from 'react-intl/locale-data/pt';
 | 
				
			||||||
import hu from 'react-intl/locale-data/hu';
 | 
					import hu from 'react-intl/locale-data/hu';
 | 
				
			||||||
import uk from 'react-intl/locale-data/uk';
 | 
					import uk from 'react-intl/locale-data/uk';
 | 
				
			||||||
 | 
					import fi from 'react-intl/locale-data/fi';
 | 
				
			||||||
import getMessagesForLocale from '../locales';
 | 
					import getMessagesForLocale from '../locales';
 | 
				
			||||||
import { hydrateStore } from '../actions/store';
 | 
					import { hydrateStore } from '../actions/store';
 | 
				
			||||||
import createStream from '../stream';
 | 
					import createStream from '../stream';
 | 
				
			||||||
| 
						 | 
					@ -56,7 +59,7 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
 | 
				
			||||||
  basename: '/web'
 | 
					  basename: '/web'
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk]);
 | 
					addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Mastodon = React.createClass({
 | 
					const Mastodon = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,6 +73,14 @@ const Mastodon = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.subscription = createStream(accessToken, 'user', {
 | 
					    this.subscription = createStream(accessToken, 'user', {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      connected () {
 | 
				
			||||||
 | 
					        store.dispatch(connectTimeline('home'));
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      disconnected () {
 | 
				
			||||||
 | 
					        store.dispatch(disconnectTimeline('home'));
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      received (data) {
 | 
					      received (data) {
 | 
				
			||||||
        switch(data.event) {
 | 
					        switch(data.event) {
 | 
				
			||||||
        case 'update':
 | 
					        case 'update':
 | 
				
			||||||
| 
						 | 
					@ -85,6 +96,7 @@ const Mastodon = React.createClass({
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      reconnected () {
 | 
					      reconnected () {
 | 
				
			||||||
 | 
					        store.dispatch(connectTimeline('home'));
 | 
				
			||||||
        store.dispatch(refreshTimeline('home'));
 | 
					        store.dispatch(refreshTimeline('home'));
 | 
				
			||||||
        store.dispatch(refreshNotifications());
 | 
					        store.dispatch(refreshNotifications());
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ import {
 | 
				
			||||||
} from '../actions/accounts';
 | 
					} from '../actions/accounts';
 | 
				
			||||||
import { deleteStatus } from '../actions/statuses';
 | 
					import { deleteStatus } from '../actions/statuses';
 | 
				
			||||||
import { initReport } from '../actions/reports';
 | 
					import { initReport } from '../actions/reports';
 | 
				
			||||||
import { openMedia } from '../actions/modal';
 | 
					import { openModal } from '../actions/modal';
 | 
				
			||||||
import { createSelector } from 'reselect'
 | 
					import { createSelector } from 'reselect'
 | 
				
			||||||
import { isMobile } from '../is_mobile'
 | 
					import { isMobile } from '../is_mobile'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,7 +63,7 @@ const mapDispatchToProps = (dispatch) => ({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onOpenMedia (media, index) {
 | 
					  onOpenMedia (media, index) {
 | 
				
			||||||
    dispatch(openMedia(media, index));
 | 
					    dispatch(openModal('MEDIA', { media, index }));
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onBlock (account) {
 | 
					  onBlock (account) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import emojify from '../../../emoji';
 | 
				
			||||||
import escapeTextContentForBrowser from 'escape-html';
 | 
					import escapeTextContentForBrowser from 'escape-html';
 | 
				
			||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
import IconButton from '../../../components/icon_button';
 | 
					import IconButton from '../../../components/icon_button';
 | 
				
			||||||
 | 
					import { Motion, spring } from 'react-motion';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
 | 
					  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
 | 
				
			||||||
| 
						 | 
					@ -11,6 +12,47 @@ const messages = defineMessages({
 | 
				
			||||||
  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }
 | 
					  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Avatar = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  propTypes: {
 | 
				
			||||||
 | 
					    account: ImmutablePropTypes.map.isRequired
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getInitialState () {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      isHovered: false
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleMouseOver () {
 | 
				
			||||||
 | 
					    if (this.state.isHovered) return;
 | 
				
			||||||
 | 
					    this.setState({ isHovered: true });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleMouseOut () {
 | 
				
			||||||
 | 
					    if (!this.state.isHovered) return;
 | 
				
			||||||
 | 
					    this.setState({ isHovered: false });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { account }   = this.props;
 | 
				
			||||||
 | 
					    const { isHovered } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
 | 
				
			||||||
 | 
					        {({ radius }) =>
 | 
				
			||||||
 | 
					          <a href={account.get('url')} className='account__header__avatar' target='_blank' rel='noopener' style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }} onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
 | 
				
			||||||
 | 
					            <img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      </Motion>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Header = React.createClass({
 | 
					const Header = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
| 
						 | 
					@ -68,14 +110,9 @@ const Header = React.createClass({
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
 | 
					      <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
 | 
				
			||||||
        <div style={{ padding: '20px 10px' }}>
 | 
					        <div style={{ padding: '20px 10px' }}>
 | 
				
			||||||
          <a href={account.get('url')} target='_blank' rel='noopener' style={{ display: 'block', color: 'inherit', textDecoration: 'none' }}>
 | 
					          <Avatar account={account} />
 | 
				
			||||||
            <div className='account__header__avatar' style={{ width: '90px', margin: '0 auto', marginBottom: '10px' }}>
 | 
					 | 
				
			||||||
              <img src={account.get('avatar')} alt='' style={{ display: 'block', width: '90px', height: '90px', borderRadius: '90px' }} />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <span style={{ display: 'inline-block', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
 | 
					 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <span style={{ display: 'inline-block', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
 | 
				
			||||||
          <span className='account__header__username' style={{ fontSize: '14px', fontWeight: '400', display: 'block', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span>
 | 
					          <span className='account__header__username' style={{ fontSize: '14px', fontWeight: '400', display: 'block', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span>
 | 
				
			||||||
          <div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
 | 
					          <div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,9 @@ import Column from '../ui/components/column';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  refreshTimeline,
 | 
					  refreshTimeline,
 | 
				
			||||||
  updateTimeline,
 | 
					  updateTimeline,
 | 
				
			||||||
  deleteFromTimelines
 | 
					  deleteFromTimelines,
 | 
				
			||||||
 | 
					  connectTimeline,
 | 
				
			||||||
 | 
					  disconnectTimeline
 | 
				
			||||||
} from '../../actions/timelines';
 | 
					} from '../../actions/timelines';
 | 
				
			||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 | 
					import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 | 
				
			||||||
| 
						 | 
					@ -44,6 +46,18 @@ const CommunityTimeline = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    subscription = createStream(accessToken, 'public:local', {
 | 
					    subscription = createStream(accessToken, 'public:local', {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      connected () {
 | 
				
			||||||
 | 
					        dispatch(connectTimeline('community'));
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      reconnected () {
 | 
				
			||||||
 | 
					        dispatch(connectTimeline('community'));
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      disconnected () {
 | 
				
			||||||
 | 
					        dispatch(disconnectTimeline('community'));
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      received (data) {
 | 
					      received (data) {
 | 
				
			||||||
        switch(data.event) {
 | 
					        switch(data.event) {
 | 
				
			||||||
        case 'update':
 | 
					        case 'update':
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,44 +0,0 @@
 | 
				
			||||||
import { Link } from 'react-router';
 | 
					 | 
				
			||||||
import { injectIntl, defineMessages } from 'react-intl';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const messages = defineMessages({
 | 
					 | 
				
			||||||
  start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
 | 
					 | 
				
			||||||
  public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
 | 
					 | 
				
			||||||
  community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
 | 
					 | 
				
			||||||
  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
 | 
					 | 
				
			||||||
  logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Drawer = ({ children, withHeader, intl }) => {
 | 
					 | 
				
			||||||
  let header = '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (withHeader) {
 | 
					 | 
				
			||||||
    header = (
 | 
					 | 
				
			||||||
      <div className='drawer__header'>
 | 
					 | 
				
			||||||
        <Link title={intl.formatMessage(messages.start)} className='drawer__tab' to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link>
 | 
					 | 
				
			||||||
        <Link title={intl.formatMessage(messages.community)} className='drawer__tab' to='/timelines/public/local'><i className='fa fa-fw fa-users' /></Link>
 | 
					 | 
				
			||||||
        <Link title={intl.formatMessage(messages.public)} className='drawer__tab' to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link>
 | 
					 | 
				
			||||||
        <a title={intl.formatMessage(messages.preferences)} className='drawer__tab' href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a>
 | 
					 | 
				
			||||||
        <a title={intl.formatMessage(messages.logout)} className='drawer__tab' href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div className='drawer'>
 | 
					 | 
				
			||||||
      {header}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div className='drawer__inner'>
 | 
					 | 
				
			||||||
        {children}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Drawer.propTypes = {
 | 
					 | 
				
			||||||
  withHeader: React.PropTypes.bool,
 | 
					 | 
				
			||||||
  children: React.PropTypes.node,
 | 
					 | 
				
			||||||
  intl: React.PropTypes.object
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default injectIntl(Drawer);
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,123 +1,68 @@
 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import Autosuggest from 'react-autosuggest';
 | 
					 | 
				
			||||||
import AutosuggestAccountContainer from '../containers/autosuggest_account_container';
 | 
					 | 
				
			||||||
import AutosuggestStatusContainer from '../containers/autosuggest_status_container';
 | 
					 | 
				
			||||||
import { debounce } from 'react-decoration';
 | 
					 | 
				
			||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }
 | 
					  placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getSuggestionValue = suggestion => suggestion.value;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const renderSuggestion = suggestion => {
 | 
					 | 
				
			||||||
  if (suggestion.type === 'account') {
 | 
					 | 
				
			||||||
    return <AutosuggestAccountContainer id={suggestion.id} />;
 | 
					 | 
				
			||||||
  } else if (suggestion.type === 'hashtag') {
 | 
					 | 
				
			||||||
    return <span>#{suggestion.id}</span>;
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    return <AutosuggestStatusContainer id={suggestion.id} />;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const renderSectionTitle = section => (
 | 
					 | 
				
			||||||
  <strong><FormattedMessage id={`search.${section.title}`} defaultMessage={section.title} /></strong>
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const getSectionSuggestions = section => section.items;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const outerStyle = {
 | 
					 | 
				
			||||||
  padding: '10px',
 | 
					 | 
				
			||||||
  lineHeight: '20px',
 | 
					 | 
				
			||||||
  position: 'relative'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const iconStyle = {
 | 
					 | 
				
			||||||
  position: 'absolute',
 | 
					 | 
				
			||||||
  top: '18px',
 | 
					 | 
				
			||||||
  right: '20px',
 | 
					 | 
				
			||||||
  fontSize: '18px',
 | 
					 | 
				
			||||||
  pointerEvents: 'none'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Search = React.createClass({
 | 
					const Search = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  contextTypes: {
 | 
					 | 
				
			||||||
    router: React.PropTypes.object
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    suggestions: React.PropTypes.array.isRequired,
 | 
					 | 
				
			||||||
    value: React.PropTypes.string.isRequired,
 | 
					    value: React.PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    submitted: React.PropTypes.bool,
 | 
				
			||||||
    onChange: React.PropTypes.func.isRequired,
 | 
					    onChange: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onSubmit: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onClear: React.PropTypes.func.isRequired,
 | 
					    onClear: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onFetch: React.PropTypes.func.isRequired,
 | 
					    onShow: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onReset: React.PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    intl: React.PropTypes.object.isRequired
 | 
					    intl: React.PropTypes.object.isRequired
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onChange (_, { newValue }) {
 | 
					  handleChange (e) {
 | 
				
			||||||
    if (typeof newValue !== 'string') {
 | 
					    this.props.onChange(e.target.value);
 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.props.onChange(newValue);
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onSuggestionsClearRequested () {
 | 
					  handleClear (e) {
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
    this.props.onClear();
 | 
					    this.props.onClear();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @debounce(500)
 | 
					  handleKeyDown (e) {
 | 
				
			||||||
  onSuggestionsFetchRequested ({ value }) {
 | 
					    if (e.key === 'Enter') {
 | 
				
			||||||
    value = value.replace('#', '');
 | 
					      e.preventDefault();
 | 
				
			||||||
    this.props.onFetch(value.trim());
 | 
					      this.props.onSubmit();
 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onSuggestionSelected (_, { suggestion }) {
 | 
					 | 
				
			||||||
    if (suggestion.type === 'account') {
 | 
					 | 
				
			||||||
      this.context.router.push(`/accounts/${suggestion.id}`);
 | 
					 | 
				
			||||||
    } else if(suggestion.type === 'hashtag') {
 | 
					 | 
				
			||||||
      this.context.router.push(`/timelines/tag/${suggestion.id}`);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this.context.router.push(`/statuses/${suggestion.id}`);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleFocus () {
 | 
				
			||||||
 | 
					    this.props.onShow();
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const inputProps = {
 | 
					    const { intl, value, submitted } = this.props;
 | 
				
			||||||
      placeholder: this.props.intl.formatMessage(messages.placeholder),
 | 
					    const hasValue = value.length > 0 || submitted;
 | 
				
			||||||
      value: this.props.value,
 | 
					 | 
				
			||||||
      onChange: this.onChange,
 | 
					 | 
				
			||||||
      className: 'search__input'
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className='search' style={outerStyle}>
 | 
					      <div className='search'>
 | 
				
			||||||
        <Autosuggest
 | 
					        <input
 | 
				
			||||||
          multiSection={true}
 | 
					          className='search__input'
 | 
				
			||||||
          suggestions={this.props.suggestions}
 | 
					          type='text'
 | 
				
			||||||
          focusFirstSuggestion={true}
 | 
					          placeholder={intl.formatMessage(messages.placeholder)}
 | 
				
			||||||
          focusInputOnSuggestionClick={false}
 | 
					          value={value}
 | 
				
			||||||
          alwaysRenderSuggestions={false}
 | 
					          onChange={this.handleChange}
 | 
				
			||||||
          onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
 | 
					          onKeyUp={this.handleKeyDown}
 | 
				
			||||||
          onSuggestionsClearRequested={this.onSuggestionsClearRequested}
 | 
					          onFocus={this.handleFocus}
 | 
				
			||||||
          onSuggestionSelected={this.onSuggestionSelected}
 | 
					 | 
				
			||||||
          getSuggestionValue={getSuggestionValue}
 | 
					 | 
				
			||||||
          renderSuggestion={renderSuggestion}
 | 
					 | 
				
			||||||
          renderSectionTitle={renderSectionTitle}
 | 
					 | 
				
			||||||
          getSectionSuggestions={getSectionSuggestions}
 | 
					 | 
				
			||||||
          inputProps={inputProps}
 | 
					 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div style={iconStyle}><i className='fa fa-search' /></div>
 | 
					        <div className='search__icon'>
 | 
				
			||||||
 | 
					          <i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
 | 
				
			||||||
 | 
					          <i className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} onClick={this.handleClear} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  },
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,68 @@
 | 
				
			||||||
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					import AccountContainer from '../../../containers/account_container';
 | 
				
			||||||
 | 
					import StatusContainer from '../../../containers/status_container';
 | 
				
			||||||
 | 
					import { Link } from 'react-router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SearchResults = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  propTypes: {
 | 
				
			||||||
 | 
					    results: ImmutablePropTypes.map.isRequired
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { results } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let accounts, statuses, hashtags;
 | 
				
			||||||
 | 
					    let count = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (results.get('accounts') && results.get('accounts').size > 0) {
 | 
				
			||||||
 | 
					      count   += results.get('accounts').size;
 | 
				
			||||||
 | 
					      accounts = (
 | 
				
			||||||
 | 
					        <div className='search-results__section'>
 | 
				
			||||||
 | 
					          {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (results.get('statuses') && results.get('statuses').size > 0) {
 | 
				
			||||||
 | 
					      count   += results.get('statuses').size;
 | 
				
			||||||
 | 
					      statuses = (
 | 
				
			||||||
 | 
					        <div className='search-results__section'>
 | 
				
			||||||
 | 
					          {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (results.get('hashtags') && results.get('hashtags').size > 0) {
 | 
				
			||||||
 | 
					      count += results.get('hashtags').size;
 | 
				
			||||||
 | 
					      hashtags = (
 | 
				
			||||||
 | 
					        <div className='search-results__section'>
 | 
				
			||||||
 | 
					          {results.get('hashtags').map(hashtag =>
 | 
				
			||||||
 | 
					            <Link className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}>
 | 
				
			||||||
 | 
					              #{hashtag}
 | 
				
			||||||
 | 
					            </Link>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className='search-results'>
 | 
				
			||||||
 | 
					        <div className='search-results__header'>
 | 
				
			||||||
 | 
					          <FormattedMessage id='search_results.total' defaultMessage='{count} {count, plural, one {result} other {results}}' values={{ count }} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {accounts}
 | 
				
			||||||
 | 
					        {statuses}
 | 
				
			||||||
 | 
					        {hashtags}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default SearchResults;
 | 
				
			||||||
| 
						 | 
					@ -1,31 +0,0 @@
 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					 | 
				
			||||||
import Toggle from 'react-toggle';
 | 
					 | 
				
			||||||
import Collapsable from '../../../components/collapsable';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const SensitiveToggle = React.createClass({
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  propTypes: {
 | 
					 | 
				
			||||||
    hasMedia: React.PropTypes.bool,
 | 
					 | 
				
			||||||
    isSensitive: React.PropTypes.bool,
 | 
					 | 
				
			||||||
    onChange: React.PropTypes.func.isRequired
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					 | 
				
			||||||
    const { hasMedia, isSensitive, onChange } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <Collapsable isVisible={hasMedia} fullHeight={39.5}>
 | 
					 | 
				
			||||||
        <label className='compose-form__label'>
 | 
					 | 
				
			||||||
          <Toggle checked={isSensitive} onChange={onChange} />
 | 
					 | 
				
			||||||
          <span className='compose-form__label__text'><FormattedMessage id='compose_form.sensitive' defaultMessage='Mark media as sensitive' /></span>
 | 
					 | 
				
			||||||
        </label>
 | 
					 | 
				
			||||||
      </Collapsable>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default SensitiveToggle;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,27 +0,0 @@
 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					 | 
				
			||||||
import Toggle from 'react-toggle';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const SpoilerToggle = React.createClass({
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  propTypes: {
 | 
					 | 
				
			||||||
    isSpoiler: React.PropTypes.bool,
 | 
					 | 
				
			||||||
    onChange: React.PropTypes.func.isRequired
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					 | 
				
			||||||
    const { isSpoiler, onChange } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <label className='compose-form__label with-border' style={{ marginTop: '10px' }}>
 | 
					 | 
				
			||||||
        <Toggle checked={isSpoiler} onChange={onChange} />
 | 
					 | 
				
			||||||
        <span className='compose-form__label__text'><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide text behind warning' /></span>
 | 
					 | 
				
			||||||
      </label>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default SpoilerToggle;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,15 @@
 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  changeSearch,
 | 
					  changeSearch,
 | 
				
			||||||
  clearSearchSuggestions,
 | 
					  clearSearch,
 | 
				
			||||||
  fetchSearchSuggestions,
 | 
					  submitSearch,
 | 
				
			||||||
  resetSearch
 | 
					  showSearch
 | 
				
			||||||
} from '../../../actions/search';
 | 
					} from '../../../actions/search';
 | 
				
			||||||
import Search from '../components/search';
 | 
					import Search from '../components/search';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  suggestions: state.getIn(['search', 'suggestions']),
 | 
					  value: state.getIn(['search', 'value']),
 | 
				
			||||||
  value: state.getIn(['search', 'value'])
 | 
					  submitted: state.getIn(['search', 'submitted'])
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = dispatch => ({
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
| 
						 | 
					@ -19,15 +19,15 @@ const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onClear () {
 | 
					  onClear () {
 | 
				
			||||||
    dispatch(clearSearchSuggestions());
 | 
					    dispatch(clearSearch());
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onFetch (value) {
 | 
					  onSubmit () {
 | 
				
			||||||
    dispatch(fetchSearchSuggestions(value));
 | 
					    dispatch(submitSearch());
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onReset () {
 | 
					  onShow () {
 | 
				
			||||||
    dispatch(resetSearch());
 | 
					    dispatch(showSearch());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import SearchResults from '../components/search_results';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
 | 
					  results: state.getIn(['search', 'results'])
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default connect(mapStateToProps)(SearchResults);
 | 
				
			||||||
| 
						 | 
					@ -1,17 +1,34 @@
 | 
				
			||||||
import Drawer from './components/drawer';
 | 
					 | 
				
			||||||
import ComposeFormContainer from './containers/compose_form_container';
 | 
					import ComposeFormContainer from './containers/compose_form_container';
 | 
				
			||||||
import UploadFormContainer from './containers/upload_form_container';
 | 
					import UploadFormContainer from './containers/upload_form_container';
 | 
				
			||||||
import NavigationContainer from './containers/navigation_container';
 | 
					import NavigationContainer from './containers/navigation_container';
 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
import SearchContainer from './containers/search_container';
 | 
					 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
import { mountCompose, unmountCompose } from '../../actions/compose';
 | 
					import { mountCompose, unmountCompose } from '../../actions/compose';
 | 
				
			||||||
 | 
					import { Link } from 'react-router';
 | 
				
			||||||
 | 
					import { injectIntl, defineMessages } from 'react-intl';
 | 
				
			||||||
 | 
					import SearchContainer from './containers/search_container';
 | 
				
			||||||
 | 
					import { Motion, spring } from 'react-motion';
 | 
				
			||||||
 | 
					import SearchResultsContainer from './containers/search_results_container';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
 | 
				
			||||||
 | 
					  public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
 | 
				
			||||||
 | 
					  community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
 | 
				
			||||||
 | 
					  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
 | 
				
			||||||
 | 
					  logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
 | 
					  showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden'])
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Compose = React.createClass({
 | 
					const Compose = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    dispatch: React.PropTypes.func.isRequired,
 | 
					    dispatch: React.PropTypes.func.isRequired,
 | 
				
			||||||
    withHeader: React.PropTypes.bool
 | 
					    withHeader: React.PropTypes.bool,
 | 
				
			||||||
 | 
					    showSearch: React.PropTypes.bool,
 | 
				
			||||||
 | 
					    intl: React.PropTypes.object.isRequired
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
| 
						 | 
					@ -25,15 +42,46 @@ const Compose = React.createClass({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { withHeader, showSearch, intl } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let header = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (withHeader) {
 | 
				
			||||||
 | 
					      header = (
 | 
				
			||||||
 | 
					        <div className='drawer__header'>
 | 
				
			||||||
 | 
					          <Link title={intl.formatMessage(messages.start)} className='drawer__tab' to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link>
 | 
				
			||||||
 | 
					          <Link title={intl.formatMessage(messages.community)} className='drawer__tab' to='/timelines/public/local'><i className='fa fa-fw fa-users' /></Link>
 | 
				
			||||||
 | 
					          <Link title={intl.formatMessage(messages.public)} className='drawer__tab' to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link>
 | 
				
			||||||
 | 
					          <a title={intl.formatMessage(messages.preferences)} className='drawer__tab' href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a>
 | 
				
			||||||
 | 
					          <a title={intl.formatMessage(messages.logout)} className='drawer__tab' href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Drawer withHeader={this.props.withHeader}>
 | 
					      <div className='drawer'>
 | 
				
			||||||
 | 
					        {header}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <SearchContainer />
 | 
					        <SearchContainer />
 | 
				
			||||||
        <NavigationContainer />
 | 
					
 | 
				
			||||||
        <ComposeFormContainer />
 | 
					        <div className='drawer__pager'>
 | 
				
			||||||
      </Drawer>
 | 
					          <div className='drawer__inner'>
 | 
				
			||||||
 | 
					            <NavigationContainer />
 | 
				
			||||||
 | 
					            <ComposeFormContainer />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
 | 
				
			||||||
 | 
					            {({ x }) =>
 | 
				
			||||||
 | 
					              <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
 | 
				
			||||||
 | 
					                <SearchResultsContainer />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          </Motion>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default connect()(Compose);
 | 
					export default connect(mapStateToProps)(injectIntl(Compose));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,9 +43,7 @@ const GettingStarted = ({ intl, me }) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div className='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}>
 | 
					      <div className='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}>
 | 
				
			||||||
        <div className='static-content getting-started'>
 | 
					        <div className='static-content getting-started'>
 | 
				
			||||||
          <p><FormattedMessage id='getting_started.about_addressing' defaultMessage='You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the form at the top of the sidebar.' /></p>
 | 
					          <p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p>
 | 
				
			||||||
          <p><FormattedMessage id='getting_started.about_shortcuts' defaultMessage='If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.' /></p>
 | 
					 | 
				
			||||||
          <p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on github at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </Column>
 | 
					    </Column>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ import SettingToggle from '../../notifications/components/setting_toggle';
 | 
				
			||||||
import SettingText from './setting_text';
 | 
					import SettingText from './setting_text';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter by regular expressions' }
 | 
					  filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const outerStyle = {
 | 
					const outerStyle = {
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,7 @@ const ColumnSettings = React.createClass({
 | 
				
			||||||
          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
 | 
					          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div style={rowStyle}>
 | 
					          <div style={rowStyle}>
 | 
				
			||||||
            <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reblogs' />} />
 | 
					            <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div style={rowStyle}>
 | 
					          <div style={rowStyle}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,9 @@ import Column from '../ui/components/column';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  refreshTimeline,
 | 
					  refreshTimeline,
 | 
				
			||||||
  updateTimeline,
 | 
					  updateTimeline,
 | 
				
			||||||
  deleteFromTimelines
 | 
					  deleteFromTimelines,
 | 
				
			||||||
 | 
					  connectTimeline,
 | 
				
			||||||
 | 
					  disconnectTimeline
 | 
				
			||||||
} from '../../actions/timelines';
 | 
					} from '../../actions/timelines';
 | 
				
			||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 | 
					import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 | 
				
			||||||
| 
						 | 
					@ -44,6 +46,18 @@ const PublicTimeline = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    subscription = createStream(accessToken, 'public', {
 | 
					    subscription = createStream(accessToken, 'public', {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      connected () {
 | 
				
			||||||
 | 
					        dispatch(connectTimeline('public'));
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      reconnected () {
 | 
				
			||||||
 | 
					        dispatch(connectTimeline('public'));
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      disconnected () {
 | 
				
			||||||
 | 
					        dispatch(disconnectTimeline('public'));
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      received (data) {
 | 
					      received (data) {
 | 
				
			||||||
        switch(data.event) {
 | 
					        switch(data.event) {
 | 
				
			||||||
        case 'update':
 | 
					        case 'update':
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,7 @@ import {
 | 
				
			||||||
import { ScrollContainer } from 'react-router-scroll';
 | 
					import { ScrollContainer } from 'react-router-scroll';
 | 
				
			||||||
import ColumnBackButton from '../../components/column_back_button';
 | 
					import ColumnBackButton from '../../components/column_back_button';
 | 
				
			||||||
import StatusContainer from '../../containers/status_container';
 | 
					import StatusContainer from '../../containers/status_container';
 | 
				
			||||||
import { openMedia } from '../../actions/modal';
 | 
					import { openModal } from '../../actions/modal';
 | 
				
			||||||
import { isMobile } from '../../is_mobile'
 | 
					import { isMobile } from '../../is_mobile'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const makeMapStateToProps = () => {
 | 
					const makeMapStateToProps = () => {
 | 
				
			||||||
| 
						 | 
					@ -99,7 +99,7 @@ const Status = React.createClass({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleOpenMedia (media, index) {
 | 
					  handleOpenMedia (media, index) {
 | 
				
			||||||
    this.props.dispatch(openMedia(media, index));
 | 
					    this.props.dispatch(openModal('MEDIA', { media, index }));
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleReport (status) {
 | 
					  handleReport (status) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,133 @@
 | 
				
			||||||
 | 
					import LoadingIndicator from '../../../components/loading_indicator';
 | 
				
			||||||
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					import ExtendedVideoPlayer from '../../../components/extended_video_player';
 | 
				
			||||||
 | 
					import ImageLoader from 'react-imageloader';
 | 
				
			||||||
 | 
					import { defineMessages, injectIntl } from 'react-intl';
 | 
				
			||||||
 | 
					import IconButton from '../../../components/icon_button';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  close: { id: 'lightbox.close', defaultMessage: 'Close' }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const leftNavStyle = {
 | 
				
			||||||
 | 
					  position: 'absolute',
 | 
				
			||||||
 | 
					  background: 'rgba(0, 0, 0, 0.5)',
 | 
				
			||||||
 | 
					  padding: '30px 15px',
 | 
				
			||||||
 | 
					  cursor: 'pointer',
 | 
				
			||||||
 | 
					  fontSize: '24px',
 | 
				
			||||||
 | 
					  top: '0',
 | 
				
			||||||
 | 
					  left: '-61px',
 | 
				
			||||||
 | 
					  boxSizing: 'border-box',
 | 
				
			||||||
 | 
					  height: '100%',
 | 
				
			||||||
 | 
					  display: 'flex',
 | 
				
			||||||
 | 
					  alignItems: 'center'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const rightNavStyle = {
 | 
				
			||||||
 | 
					  position: 'absolute',
 | 
				
			||||||
 | 
					  background: 'rgba(0, 0, 0, 0.5)',
 | 
				
			||||||
 | 
					  padding: '30px 15px',
 | 
				
			||||||
 | 
					  cursor: 'pointer',
 | 
				
			||||||
 | 
					  fontSize: '24px',
 | 
				
			||||||
 | 
					  top: '0',
 | 
				
			||||||
 | 
					  right: '-61px',
 | 
				
			||||||
 | 
					  boxSizing: 'border-box',
 | 
				
			||||||
 | 
					  height: '100%',
 | 
				
			||||||
 | 
					  display: 'flex',
 | 
				
			||||||
 | 
					  alignItems: 'center'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const closeStyle = {
 | 
				
			||||||
 | 
					  position: 'absolute',
 | 
				
			||||||
 | 
					  top: '4px',
 | 
				
			||||||
 | 
					  right: '4px'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MediaModal = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  propTypes: {
 | 
				
			||||||
 | 
					    media: ImmutablePropTypes.list.isRequired,
 | 
				
			||||||
 | 
					    index: React.PropTypes.number.isRequired,
 | 
				
			||||||
 | 
					    onClose: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    intl: React.PropTypes.object.isRequired
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getInitialState () {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      index: null
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleNextClick () {
 | 
				
			||||||
 | 
					    this.setState({ index: (this.getIndex() + 1) % this.props.media.size});
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handlePrevClick () {
 | 
				
			||||||
 | 
					    this.setState({ index: (this.getIndex() - 1) % this.props.media.size});
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleKeyUp (e) {
 | 
				
			||||||
 | 
					    switch(e.key) {
 | 
				
			||||||
 | 
					    case 'ArrowLeft':
 | 
				
			||||||
 | 
					      this.handlePrevClick();
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'ArrowRight':
 | 
				
			||||||
 | 
					      this.handleNextClick();
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentDidMount () {
 | 
				
			||||||
 | 
					    window.addEventListener('keyup', this.handleKeyUp, false);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentWillUnmount () {
 | 
				
			||||||
 | 
					    window.removeEventListener('keyup', this.handleKeyUp);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getIndex () {
 | 
				
			||||||
 | 
					    return this.state.index !== null ? this.state.index : this.props.index;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { media, intl, onClose } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const index = this.getIndex();
 | 
				
			||||||
 | 
					    const attachment = media.get(index);
 | 
				
			||||||
 | 
					    const url = attachment.get('url');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let leftNav, rightNav, content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    leftNav = rightNav = content = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (media.size > 1) {
 | 
				
			||||||
 | 
					      leftNav  = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
 | 
				
			||||||
 | 
					      rightNav = <div style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (attachment.get('type') === 'image') {
 | 
				
			||||||
 | 
					      content = <ImageLoader src={url} imgProps={{ style: { display: 'block' } }} />;
 | 
				
			||||||
 | 
					    } else if (attachment.get('type') === 'gifv') {
 | 
				
			||||||
 | 
					      content = <ExtendedVideoPlayer src={url} />;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className='modal-root__modal media-modal'>
 | 
				
			||||||
 | 
					        {leftNav}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <IconButton title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} style={closeStyle} />
 | 
				
			||||||
 | 
					          {content}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {rightNav}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default injectIntl(MediaModal);
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,80 @@
 | 
				
			||||||
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
 | 
					import MediaModal from './media_modal';
 | 
				
			||||||
 | 
					import { TransitionMotion, spring } from 'react-motion';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MODAL_COMPONENTS = {
 | 
				
			||||||
 | 
					  'MEDIA': MediaModal
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ModalRoot = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  propTypes: {
 | 
				
			||||||
 | 
					    type: React.PropTypes.string,
 | 
				
			||||||
 | 
					    props: React.PropTypes.object,
 | 
				
			||||||
 | 
					    onClose: React.PropTypes.func.isRequired
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleKeyUp (e) {
 | 
				
			||||||
 | 
					    if (e.key === 'Escape' && !!this.props.type) {
 | 
				
			||||||
 | 
					      this.props.onClose();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentDidMount () {
 | 
				
			||||||
 | 
					    window.addEventListener('keyup', this.handleKeyUp, false);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentWillUnmount () {
 | 
				
			||||||
 | 
					    window.removeEventListener('keyup', this.handleKeyUp);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  willEnter () {
 | 
				
			||||||
 | 
					    return { opacity: 0, scale: 0.98 };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  willLeave () {
 | 
				
			||||||
 | 
					    return { opacity: spring(0), scale: spring(0.98) };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { type, props, onClose } = this.props;
 | 
				
			||||||
 | 
					    const items = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!!type) {
 | 
				
			||||||
 | 
					      items.push({
 | 
				
			||||||
 | 
					        key: type,
 | 
				
			||||||
 | 
					        data: { type, props },
 | 
				
			||||||
 | 
					        style: { opacity: spring(1), scale: spring(1, { stiffness: 120, damping: 14 }) }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <TransitionMotion
 | 
				
			||||||
 | 
					        styles={items}
 | 
				
			||||||
 | 
					        willEnter={this.willEnter}
 | 
				
			||||||
 | 
					        willLeave={this.willLeave}>
 | 
				
			||||||
 | 
					        {interpolatedStyles =>
 | 
				
			||||||
 | 
					          <div className='modal-root'>
 | 
				
			||||||
 | 
					            {interpolatedStyles.map(({ key, data: { type, props }, style }) => {
 | 
				
			||||||
 | 
					              const SpecificComponent = MODAL_COMPONENTS[type];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return (
 | 
				
			||||||
 | 
					                <div key={key}>
 | 
				
			||||||
 | 
					                  <div className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
 | 
				
			||||||
 | 
					                  <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
 | 
				
			||||||
 | 
					                    <SpecificComponent {...props} onClose={onClose} />
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            })}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      </TransitionMotion>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ModalRoot;
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,23 @@
 | 
				
			||||||
import { Link } from 'react-router';
 | 
					import { Link } from 'react-router';
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TabsBar = () => {
 | 
					const TabsBar = React.createClass({
 | 
				
			||||||
  return (
 | 
					
 | 
				
			||||||
    <div className='tabs-bar'>
 | 
					  render () {
 | 
				
			||||||
      <Link className='tabs-bar__link' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /> <FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link>
 | 
					    return (
 | 
				
			||||||
      <Link className='tabs-bar__link' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /> <FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link>
 | 
					      <div className='tabs-bar'>
 | 
				
			||||||
      <Link className='tabs-bar__link' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /> <FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link>
 | 
					        <Link className='tabs-bar__link primary' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link>
 | 
				
			||||||
      <Link className='tabs-bar__link' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-bars' /></Link>
 | 
					        <Link className='tabs-bar__link primary' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link>
 | 
				
			||||||
    </div>
 | 
					        <Link className='tabs-bar__link primary' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link>
 | 
				
			||||||
  );
 | 
					
 | 
				
			||||||
};
 | 
					        <Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public/local'><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></Link>
 | 
				
			||||||
 | 
					        <Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public'><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></Link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Link className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-bars' /></Link>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default TabsBar;
 | 
					export default TabsBar;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,170 +1,16 @@
 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
import {
 | 
					import { closeModal } from '../../../actions/modal';
 | 
				
			||||||
  closeModal,
 | 
					import ModalRoot from '../components/modal_root';
 | 
				
			||||||
  decreaseIndexInModal,
 | 
					 | 
				
			||||||
  increaseIndexInModal
 | 
					 | 
				
			||||||
} from '../../../actions/modal';
 | 
					 | 
				
			||||||
import Lightbox from '../../../components/lightbox';
 | 
					 | 
				
			||||||
import ImageLoader from 'react-imageloader';
 | 
					 | 
				
			||||||
import LoadingIndicator from '../../../components/loading_indicator';
 | 
					 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					 | 
				
			||||||
import ExtendedVideoPlayer from '../../../components/extended_video_player';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  media: state.getIn(['modal', 'media']),
 | 
					  type: state.get('modal').modalType,
 | 
				
			||||||
  index: state.getIn(['modal', 'index']),
 | 
					  props: state.get('modal').modalProps
 | 
				
			||||||
  isVisible: state.getIn(['modal', 'open'])
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = dispatch => ({
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
  onCloseClicked () {
 | 
					  onClose () {
 | 
				
			||||||
    dispatch(closeModal());
 | 
					    dispatch(closeModal());
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					 | 
				
			||||||
  onOverlayClicked () {
 | 
					 | 
				
			||||||
    dispatch(closeModal());
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onNextClicked () {
 | 
					 | 
				
			||||||
    dispatch(increaseIndexInModal());
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onPrevClicked () {
 | 
					 | 
				
			||||||
    dispatch(decreaseIndexInModal());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const imageStyle = {
 | 
					export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot);
 | 
				
			||||||
  display: 'block',
 | 
					 | 
				
			||||||
  maxWidth: '80vw',
 | 
					 | 
				
			||||||
  maxHeight: '80vh'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const loadingStyle = {
 | 
					 | 
				
			||||||
  width: '400px',
 | 
					 | 
				
			||||||
  paddingBottom: '120px'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const preloader = () => (
 | 
					 | 
				
			||||||
  <div className='modal-container--preloader' style={loadingStyle}>
 | 
					 | 
				
			||||||
    <LoadingIndicator />
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const leftNavStyle = {
 | 
					 | 
				
			||||||
  position: 'absolute',
 | 
					 | 
				
			||||||
  background: 'rgba(0, 0, 0, 0.5)',
 | 
					 | 
				
			||||||
  padding: '30px 15px',
 | 
					 | 
				
			||||||
  cursor: 'pointer',
 | 
					 | 
				
			||||||
  fontSize: '24px',
 | 
					 | 
				
			||||||
  top: '0',
 | 
					 | 
				
			||||||
  left: '-61px',
 | 
					 | 
				
			||||||
  boxSizing: 'border-box',
 | 
					 | 
				
			||||||
  height: '100%',
 | 
					 | 
				
			||||||
  display: 'flex',
 | 
					 | 
				
			||||||
  alignItems: 'center'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const rightNavStyle = {
 | 
					 | 
				
			||||||
  position: 'absolute',
 | 
					 | 
				
			||||||
  background: 'rgba(0, 0, 0, 0.5)',
 | 
					 | 
				
			||||||
  padding: '30px 15px',
 | 
					 | 
				
			||||||
  cursor: 'pointer',
 | 
					 | 
				
			||||||
  fontSize: '24px',
 | 
					 | 
				
			||||||
  top: '0',
 | 
					 | 
				
			||||||
  right: '-61px',
 | 
					 | 
				
			||||||
  boxSizing: 'border-box',
 | 
					 | 
				
			||||||
  height: '100%',
 | 
					 | 
				
			||||||
  display: 'flex',
 | 
					 | 
				
			||||||
  alignItems: 'center'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Modal = React.createClass({
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  propTypes: {
 | 
					 | 
				
			||||||
    media: ImmutablePropTypes.list,
 | 
					 | 
				
			||||||
    index: React.PropTypes.number.isRequired,
 | 
					 | 
				
			||||||
    isVisible: React.PropTypes.bool,
 | 
					 | 
				
			||||||
    onCloseClicked: React.PropTypes.func,
 | 
					 | 
				
			||||||
    onOverlayClicked: React.PropTypes.func,
 | 
					 | 
				
			||||||
    onNextClicked: React.PropTypes.func,
 | 
					 | 
				
			||||||
    onPrevClicked: React.PropTypes.func
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleNextClick () {
 | 
					 | 
				
			||||||
    this.props.onNextClicked();
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handlePrevClick () {
 | 
					 | 
				
			||||||
    this.props.onPrevClicked();
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentDidMount () {
 | 
					 | 
				
			||||||
    this._listener = e => {
 | 
					 | 
				
			||||||
      if (!this.props.isVisible) {
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      switch(e.key) {
 | 
					 | 
				
			||||||
      case 'ArrowLeft':
 | 
					 | 
				
			||||||
        this.props.onPrevClicked();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case 'ArrowRight':
 | 
					 | 
				
			||||||
        this.props.onNextClicked();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    window.addEventListener('keyup', this._listener);
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentWillUnmount () {
 | 
					 | 
				
			||||||
    window.removeEventListener('keyup', this._listener);
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					 | 
				
			||||||
    const { media, index, ...other } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!media) {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const attachment = media.get(index);
 | 
					 | 
				
			||||||
    const url = attachment.get('url');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let leftNav, rightNav, content;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    leftNav = rightNav = content = '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (media.size > 1) {
 | 
					 | 
				
			||||||
      leftNav  = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
 | 
					 | 
				
			||||||
      rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (attachment.get('type') === 'image') {
 | 
					 | 
				
			||||||
      content = (
 | 
					 | 
				
			||||||
        <ImageLoader
 | 
					 | 
				
			||||||
          src={url}
 | 
					 | 
				
			||||||
          preloader={preloader}
 | 
					 | 
				
			||||||
          imgProps={{ style: imageStyle }}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } else if (attachment.get('type') === 'gifv') {
 | 
					 | 
				
			||||||
      content = <ExtendedVideoPlayer src={url} />;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <Lightbox {...other}>
 | 
					 | 
				
			||||||
        {leftNav}
 | 
					 | 
				
			||||||
        {content}
 | 
					 | 
				
			||||||
        {rightNav}
 | 
					 | 
				
			||||||
      </Lightbox>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default connect(mapStateToProps, mapDispatchToProps)(Modal);
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,15 +36,33 @@ const UI = React.createClass({
 | 
				
			||||||
    this.setState({ width: window.innerWidth });
 | 
					    this.setState({ width: window.innerWidth });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleDragEnter (e) {
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!this.dragTargets) {
 | 
				
			||||||
 | 
					      this.dragTargets = [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.dragTargets.indexOf(e.target) === -1) {
 | 
				
			||||||
 | 
					      this.dragTargets.push(e.target);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (e.dataTransfer && e.dataTransfer.files.length > 0) {
 | 
				
			||||||
 | 
					      this.setState({ draggingOver: true });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleDragOver (e) {
 | 
					  handleDragOver (e) {
 | 
				
			||||||
    e.preventDefault();
 | 
					    e.preventDefault();
 | 
				
			||||||
    e.stopPropagation();
 | 
					    e.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    e.dataTransfer.dropEffect = 'copy';
 | 
					    try {
 | 
				
			||||||
 | 
					      e.dataTransfer.dropEffect = 'copy';
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (e.dataTransfer.effectAllowed === 'all' || e.dataTransfer.effectAllowed === 'uninitialized') {
 | 
					 | 
				
			||||||
      this.setState({ draggingOver: true });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleDrop (e) {
 | 
					  handleDrop (e) {
 | 
				
			||||||
| 
						 | 
					@ -57,14 +75,25 @@ const UI = React.createClass({
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleDragLeave () {
 | 
					  handleDragLeave (e) {
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					    e.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.dragTargets.length > 0) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.setState({ draggingOver: false });
 | 
					    this.setState({ draggingOver: false });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillMount () {
 | 
					  componentWillMount () {
 | 
				
			||||||
    window.addEventListener('resize', this.handleResize, { passive: true });
 | 
					    window.addEventListener('resize', this.handleResize, { passive: true });
 | 
				
			||||||
    window.addEventListener('dragover', this.handleDragOver);
 | 
					    document.addEventListener('dragenter', this.handleDragEnter, false);
 | 
				
			||||||
    window.addEventListener('drop', this.handleDrop);
 | 
					    document.addEventListener('dragover', this.handleDragOver, false);
 | 
				
			||||||
 | 
					    document.addEventListener('drop', this.handleDrop, false);
 | 
				
			||||||
 | 
					    document.addEventListener('dragleave', this.handleDragLeave, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.props.dispatch(refreshTimeline('home'));
 | 
					    this.props.dispatch(refreshTimeline('home'));
 | 
				
			||||||
    this.props.dispatch(refreshNotifications());
 | 
					    this.props.dispatch(refreshNotifications());
 | 
				
			||||||
| 
						 | 
					@ -72,8 +101,14 @@ const UI = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillUnmount () {
 | 
					  componentWillUnmount () {
 | 
				
			||||||
    window.removeEventListener('resize', this.handleResize);
 | 
					    window.removeEventListener('resize', this.handleResize);
 | 
				
			||||||
    window.removeEventListener('dragover', this.handleDragOver);
 | 
					    document.removeEventListener('dragenter', this.handleDragEnter);
 | 
				
			||||||
    window.removeEventListener('drop', this.handleDrop);
 | 
					    document.removeEventListener('dragover', this.handleDragOver);
 | 
				
			||||||
 | 
					    document.removeEventListener('drop', this.handleDrop);
 | 
				
			||||||
 | 
					    document.removeEventListener('dragleave', this.handleDragLeave);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setRef (c) {
 | 
				
			||||||
 | 
					    this.node = c;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
| 
						 | 
					@ -100,7 +135,7 @@ const UI = React.createClass({
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className='ui' onDragLeave={this.handleDragLeave}>
 | 
					      <div className='ui' ref={this.setRef}>
 | 
				
			||||||
        <TabsBar />
 | 
					        <TabsBar />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {mountedColumns}
 | 
					        {mountedColumns}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,7 @@ const en = {
 | 
				
			||||||
  "getting_started.heading": "Getting started",
 | 
					  "getting_started.heading": "Getting started",
 | 
				
			||||||
  "getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.",
 | 
					  "getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.",
 | 
				
			||||||
  "getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.",
 | 
					  "getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.",
 | 
				
			||||||
  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on github at {github}. {apps}.",
 | 
					  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.",
 | 
				
			||||||
  "column.home": "Home",
 | 
					  "column.home": "Home",
 | 
				
			||||||
  "column.community": "Local timeline",
 | 
					  "column.community": "Local timeline",
 | 
				
			||||||
  "column.public": "Federated timeline",
 | 
					  "column.public": "Federated timeline",
 | 
				
			||||||
| 
						 | 
					@ -40,7 +40,7 @@ const en = {
 | 
				
			||||||
  "compose_form.sensitive": "Mark media as sensitive",
 | 
					  "compose_form.sensitive": "Mark media as sensitive",
 | 
				
			||||||
  "compose_form.spoiler": "Hide text behind warning",
 | 
					  "compose_form.spoiler": "Hide text behind warning",
 | 
				
			||||||
  "compose_form.private": "Mark as private",
 | 
					  "compose_form.private": "Mark as private",
 | 
				
			||||||
  "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}} to not leak your status?",
 | 
					  "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
 | 
				
			||||||
  "compose_form.unlisted": "Do not display on public timelines",
 | 
					  "compose_form.unlisted": "Do not display on public timelines",
 | 
				
			||||||
  "navigation_bar.edit_profile": "Edit profile",
 | 
					  "navigation_bar.edit_profile": "Edit profile",
 | 
				
			||||||
  "navigation_bar.preferences": "Preferences",
 | 
					  "navigation_bar.preferences": "Preferences",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,68 @@
 | 
				
			||||||
 | 
					const fi = {
 | 
				
			||||||
 | 
					  "column_back_button.label": "Takaisin",
 | 
				
			||||||
 | 
					  "lightbox.close": "Sulje",
 | 
				
			||||||
 | 
					  "loading_indicator.label": "Ladataan...",
 | 
				
			||||||
 | 
					  "status.mention": "Mainitse @{name}",
 | 
				
			||||||
 | 
					  "status.delete": "Poista",
 | 
				
			||||||
 | 
					  "status.reply": "Vastaa",
 | 
				
			||||||
 | 
					  "status.reblog": "Boostaa",
 | 
				
			||||||
 | 
					  "status.favourite": "Tykkää",
 | 
				
			||||||
 | 
					  "status.reblogged_by": "{name} boostattu",
 | 
				
			||||||
 | 
					  "status.sensitive_warning": "Arkaluontoista sisältöä",
 | 
				
			||||||
 | 
					  "status.sensitive_toggle": "Klikkaa nähdäksesi",
 | 
				
			||||||
 | 
					  "video_player.toggle_sound": "Äänet päälle/pois",
 | 
				
			||||||
 | 
					  "account.mention": "Mainitse @{name}",
 | 
				
			||||||
 | 
					  "account.edit_profile": "Muokkaa",
 | 
				
			||||||
 | 
					  "account.unblock": "Salli @{name}",
 | 
				
			||||||
 | 
					  "account.unfollow": "Lopeta seuraaminen",
 | 
				
			||||||
 | 
					  "account.block": "Estä @{name}",
 | 
				
			||||||
 | 
					  "account.follow": "Seuraa",
 | 
				
			||||||
 | 
					  "account.posts": "Postit",
 | 
				
			||||||
 | 
					  "account.follows": "Seuraa",
 | 
				
			||||||
 | 
					  "account.followers": "Seuraajia",
 | 
				
			||||||
 | 
					  "account.follows_you": "Seuraa sinua",
 | 
				
			||||||
 | 
					  "account.requested": "Odottaa hyväksyntää",
 | 
				
			||||||
 | 
					  "getting_started.heading": "Päästä alkuun",
 | 
				
			||||||
 | 
					  "getting_started.about_addressing": "Voit seurata ihmisiä jos tiedät heidän käyttäjänimensä ja domainin missä he ovat syöttämällä e-mail-esque osoitteen Etsi kenttään.",
 | 
				
			||||||
 | 
					  "getting_started.about_shortcuts": "Jos etsimäsi henkilö on samassa domainissa kuin sinä, pelkkä käyttäjänimi kelpaa. Sama pätee kun mainitset ihmisiä statuksessasi",
 | 
				
			||||||
 | 
					  "getting_started.open_source_notice": "Mastodon Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia githubissa {github}. {apps}.",
 | 
				
			||||||
 | 
					  "column.home": "Koti",
 | 
				
			||||||
 | 
					  "column.community": "Paikallinen aikajana",
 | 
				
			||||||
 | 
					  "column.public": "Yhdistetty aikajana",
 | 
				
			||||||
 | 
					  "column.notifications": "Ilmoitukset",
 | 
				
			||||||
 | 
					  "tabs_bar.compose": "Luo",
 | 
				
			||||||
 | 
					  "tabs_bar.home": "Koti",
 | 
				
			||||||
 | 
					  "tabs_bar.mentions": "Maininnat",
 | 
				
			||||||
 | 
					  "tabs_bar.public": "Yleinen aikajana",
 | 
				
			||||||
 | 
					  "tabs_bar.notifications": "Ilmoitukset",
 | 
				
			||||||
 | 
					  "compose_form.placeholder": "Mitä sinulla on mielessä?",
 | 
				
			||||||
 | 
					  "compose_form.publish": "Toot",
 | 
				
			||||||
 | 
					  "compose_form.sensitive": "Merkitse media herkäksi",
 | 
				
			||||||
 | 
					  "compose_form.spoiler": "Piiloita teksti varoituksen taakse",
 | 
				
			||||||
 | 
					  "compose_form.private": "Merkitse yksityiseksi",
 | 
				
			||||||
 | 
					  "compose_form.privacy_disclaimer": "Sinun yksityinen status toimitetaan mainitsemallesi käyttäjille domaineissa {domains}. Luotatko {domainsCount, plural, one {tähän palvelimeen} other {näihin palvelimiin}}? Postauksen yksityisyys toimii van Mastodon palvelimilla. Jos {domains} {domainsCount, plural, one {ei ole Mastodon palvelin} other {eivät ole Mastodon palvelin}}, viestiin ei tule Yksityinen-merkintää, ja sitä voidaan boostata tai muuten tehdä näkyväksi muille vastaanottajille.",
 | 
				
			||||||
 | 
					  "compose_form.unlisted": "Älä näytä julkisilla aikajanoilla",
 | 
				
			||||||
 | 
					  "navigation_bar.edit_profile": "Muokkaa profiilia",
 | 
				
			||||||
 | 
					  "navigation_bar.preferences": "Ominaisuudet",
 | 
				
			||||||
 | 
					  "navigation_bar.community_timeline": "Paikallinen aikajana",
 | 
				
			||||||
 | 
					  "navigation_bar.public_timeline": "Yleinen aikajana",
 | 
				
			||||||
 | 
					  "navigation_bar.logout": "Kirjaudu ulos",
 | 
				
			||||||
 | 
					  "reply_indicator.cancel": "Peruuta",
 | 
				
			||||||
 | 
					  "search.placeholder": "Hae",
 | 
				
			||||||
 | 
					  "search.account": "Tili",
 | 
				
			||||||
 | 
					  "search.hashtag": "Hashtag",
 | 
				
			||||||
 | 
					  "upload_button.label": "Lisää mediaa",
 | 
				
			||||||
 | 
					  "upload_form.undo": "Peru",
 | 
				
			||||||
 | 
					  "notification.follow": "{name} seurasi sinua",
 | 
				
			||||||
 | 
					  "notification.favourite": "{name} tykkäsi statuksestasi",
 | 
				
			||||||
 | 
					  "notification.reblog": "{name} boostasi statustasi",
 | 
				
			||||||
 | 
					  "notification.mention": "{name} mainitsi sinut",
 | 
				
			||||||
 | 
					  "notifications.column_settings.alert": "Työpöytä ilmoitukset",
 | 
				
			||||||
 | 
					  "notifications.column_settings.show": "Näytä sarakkeessa",
 | 
				
			||||||
 | 
					  "notifications.column_settings.follow": "Uusia seuraajia:",
 | 
				
			||||||
 | 
					  "notifications.column_settings.favourite": "Tykkäyksiä:",
 | 
				
			||||||
 | 
					  "notifications.column_settings.mention": "Mainintoja:",
 | 
				
			||||||
 | 
					  "notifications.column_settings.reblog": "Boosteja:",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default fi;
 | 
				
			||||||
| 
						 | 
					@ -1,68 +1,91 @@
 | 
				
			||||||
const fr = {
 | 
					const fr = {
 | 
				
			||||||
  "account.block": "Bloquer",
 | 
					 | 
				
			||||||
  "account.edit_profile": "Modifier le profil",
 | 
					 | 
				
			||||||
  "account.followers": "Abonnés",
 | 
					 | 
				
			||||||
  "account.follows": "Abonnements",
 | 
					 | 
				
			||||||
  "account.follow": "Suivre",
 | 
					 | 
				
			||||||
  "account.follows_you": "Vous suit",
 | 
					 | 
				
			||||||
  "account.mention": "Mentionner",
 | 
					 | 
				
			||||||
  "account.posts": "Statuts",
 | 
					 | 
				
			||||||
  "account.requested": "Invitation envoyée",
 | 
					 | 
				
			||||||
  "account.unblock": "Débloquer",
 | 
					 | 
				
			||||||
  "account.unfollow": "Ne plus suivre",
 | 
					 | 
				
			||||||
  "column_back_button.label": "Retour",
 | 
					  "column_back_button.label": "Retour",
 | 
				
			||||||
  "column.home": "Accueil",
 | 
					 | 
				
			||||||
  "column.mentions": "Mentions",
 | 
					 | 
				
			||||||
  "column.notifications": "Notifications",
 | 
					 | 
				
			||||||
  "column.public": "Fil public",
 | 
					 | 
				
			||||||
  "compose_form.placeholder": "Qu’avez-vous en tête ?",
 | 
					 | 
				
			||||||
  "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ?",
 | 
					 | 
				
			||||||
  "compose_form.private": "Rendre privé",
 | 
					 | 
				
			||||||
  "compose_form.publish": "Pouet ",
 | 
					 | 
				
			||||||
  "compose_form.sensitive": "Marquer le média comme délicat",
 | 
					 | 
				
			||||||
  "compose_form.spoiler": "Masque le texte par un avertissement",
 | 
					 | 
				
			||||||
  "compose_form.unlisted": "Ne pas afficher dans le fil public",
 | 
					 | 
				
			||||||
  "getting_started.about_addressing": "Vous pouvez vous suivre les statuts de quelqu’un en entrant dans le champs de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.",
 | 
					 | 
				
			||||||
  "getting_started.about_developer": "Pour suivre le développeur de ce projet, c’est Gargron@mastodon.social",
 | 
					 | 
				
			||||||
  "getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, l’identifiant suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.",
 | 
					 | 
				
			||||||
  "getting_started.heading": "Pour commencer",
 | 
					 | 
				
			||||||
  "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.",
 | 
					 | 
				
			||||||
  "lightbox.close": "Fermer",
 | 
					  "lightbox.close": "Fermer",
 | 
				
			||||||
  "loading_indicator.label": "Chargement…",
 | 
					  "loading_indicator.label": "Chargement…",
 | 
				
			||||||
  "navigation_bar.edit_profile": "Modifier le profil",
 | 
					 | 
				
			||||||
  "navigation_bar.logout": "Déconnexion",
 | 
					 | 
				
			||||||
  "navigation_bar.preferences": "Préférences",
 | 
					 | 
				
			||||||
  "navigation_bar.public_timeline": "Fil public",
 | 
					 | 
				
			||||||
  "notification.favourite": "{name} a ajouté à ses favoris :",
 | 
					 | 
				
			||||||
  "notification.follow": "{name} vous suit.",
 | 
					 | 
				
			||||||
  "notification.mention": "{name} vous a mentionné⋅e :",
 | 
					 | 
				
			||||||
  "notification.reblog": "{name} a partagé votre statut :",
 | 
					 | 
				
			||||||
  "notifications.column_settings.alert": "Notifications locales",
 | 
					 | 
				
			||||||
  "notifications.column_settings.favourite": "Favoris :",
 | 
					 | 
				
			||||||
  "notifications.column_settings.follow": "Nouveaux abonnés :",
 | 
					 | 
				
			||||||
  "notifications.column_settings.mention": "Mentions :",
 | 
					 | 
				
			||||||
  "notifications.column_settings.reblog": "Partages :",
 | 
					 | 
				
			||||||
  "notifications.column_settings.show": "Afficher dans la colonne",
 | 
					 | 
				
			||||||
  "reply_indicator.cancel": "Annuler",
 | 
					 | 
				
			||||||
  "search.account": "Compte",
 | 
					 | 
				
			||||||
  "search.hashtag": "Mot-clé",
 | 
					 | 
				
			||||||
  "search.placeholder": "Chercher",
 | 
					 | 
				
			||||||
  "status.delete": "Effacer",
 | 
					 | 
				
			||||||
  "status.favourite": "Ajouter aux favoris",
 | 
					 | 
				
			||||||
  "status.mention": "Mentionner",
 | 
					  "status.mention": "Mentionner",
 | 
				
			||||||
  "status.reblogged_by": "{name} a partagé :",
 | 
					  "status.delete": "Effacer",
 | 
				
			||||||
  "status.reblog": "Partager",
 | 
					 | 
				
			||||||
  "status.reply": "Répondre",
 | 
					  "status.reply": "Répondre",
 | 
				
			||||||
  "status.sensitive_toggle": "Cliquer pour dévoiler",
 | 
					  "status.reblog": "Partager",
 | 
				
			||||||
 | 
					  "status.favourite": "Ajouter aux favoris",
 | 
				
			||||||
 | 
					  "status.reblogged_by": "{name} a partagé :",
 | 
				
			||||||
  "status.sensitive_warning": "Contenu délicat",
 | 
					  "status.sensitive_warning": "Contenu délicat",
 | 
				
			||||||
 | 
					  "status.sensitive_toggle": "Cliquer pour dévoiler",
 | 
				
			||||||
 | 
					  "video_player.toggle_sound": "Mettre/Couper le son",
 | 
				
			||||||
 | 
					  "account.mention": "Mentionner",
 | 
				
			||||||
 | 
					  "account.edit_profile": "Modifier le profil",
 | 
				
			||||||
 | 
					  "account.unblock": "Débloquer",
 | 
				
			||||||
 | 
					  "account.unfollow": "Ne plus suivre",
 | 
				
			||||||
 | 
					  "account.block": "Bloquer",
 | 
				
			||||||
 | 
					  "account.mute": "Masquer",
 | 
				
			||||||
 | 
					  "account.unmute": "Ne plus masquer",
 | 
				
			||||||
 | 
					  "account.follow": "Suivre",
 | 
				
			||||||
 | 
					  "account.posts": "Statuts",
 | 
				
			||||||
 | 
					  "account.follows": "Abonnements",
 | 
				
			||||||
 | 
					  "account.followers": "Abonnés",
 | 
				
			||||||
 | 
					  "account.follows_you": "Vous suit",
 | 
				
			||||||
 | 
					  "account.requested": "Invitation envoyée",
 | 
				
			||||||
 | 
					  "account.report": "Signaler",
 | 
				
			||||||
 | 
					  "account.disclaimer": "Ce compte est situé sur une autre instance. Les nombres peuvent être plus grands.",
 | 
				
			||||||
 | 
					  "getting_started.heading": "Pour commencer",
 | 
				
			||||||
 | 
					  "getting_started.about_addressing": "Vous pouvez suivre les statuts de quelqu’un en entrant dans le champs de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.",
 | 
				
			||||||
 | 
					  "getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, l’identifiant suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.",
 | 
				
			||||||
 | 
					  "getting_started.about_developer": "Pour suivre le développeur de ce projet, c’est Gargron@mastodon.social",
 | 
				
			||||||
 | 
					  "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.",
 | 
				
			||||||
 | 
					  "column.home": "Accueil",
 | 
				
			||||||
 | 
					  "column.community": "Fil public local",
 | 
				
			||||||
 | 
					  "column.public": "Fil public global",
 | 
				
			||||||
 | 
					  "column.notifications": "Notifications",
 | 
				
			||||||
 | 
					  "column.public": "Fil public",
 | 
				
			||||||
 | 
					  "column.blocks": "Utilisateurs bloqués",
 | 
				
			||||||
 | 
					  "column.favourites": "Favoris",
 | 
				
			||||||
  "tabs_bar.compose": "Composer",
 | 
					  "tabs_bar.compose": "Composer",
 | 
				
			||||||
  "tabs_bar.home": "Accueil",
 | 
					  "tabs_bar.home": "Accueil",
 | 
				
			||||||
  "tabs_bar.mentions": "Mentions",
 | 
					  "tabs_bar.mentions": "Mentions",
 | 
				
			||||||
 | 
					  "tabs_bar.public": "Fil public global",
 | 
				
			||||||
  "tabs_bar.notifications": "Notifications",
 | 
					  "tabs_bar.notifications": "Notifications",
 | 
				
			||||||
  "tabs_bar.public": "Public",
 | 
					  "compose_form.placeholder": "Qu’avez-vous en tête ?",
 | 
				
			||||||
 | 
					  "compose_form.publish": "Pouet ",
 | 
				
			||||||
 | 
					  "compose_form.sensitive": "Marquer le média comme délicat",
 | 
				
			||||||
 | 
					  "compose_form.spoiler": "Masquer le texte par un avertissement",
 | 
				
			||||||
 | 
					  "compose_form.private": "Rendre privé",
 | 
				
			||||||
 | 
					  "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues",
 | 
				
			||||||
 | 
					  "compose_form.unlisted": "Ne pas afficher dans les fils publics",
 | 
				
			||||||
 | 
					  "emoji_button.label": "Insérer un emoji",
 | 
				
			||||||
 | 
					  "navigation_bar.edit_profile": "Modifier le profil",
 | 
				
			||||||
 | 
					  "navigation_bar.preferences": "Préférences",
 | 
				
			||||||
 | 
					  "navigation_bar.community_timeline": "Fil public local",
 | 
				
			||||||
 | 
					  "navigation_bar.public_timeline": "Fil public global",
 | 
				
			||||||
 | 
					  "navigation_bar.blocks": "Utilisateurs bloqués",
 | 
				
			||||||
 | 
					  "navigation_bar.favourites": "Favoris",
 | 
				
			||||||
 | 
					  "navigation_bar.info": "Plus d'informations",
 | 
				
			||||||
 | 
					  "notification.favourite": "{name} a ajouté à ses favoris :",
 | 
				
			||||||
 | 
					  "navigation_bar.logout": "Déconnexion",
 | 
				
			||||||
 | 
					  "reply_indicator.cancel": "Annuler",
 | 
				
			||||||
 | 
					  "search.placeholder": "Chercher",
 | 
				
			||||||
 | 
					  "search.account": "Compte",
 | 
				
			||||||
 | 
					  "search.hashtag": "Mot-clé",
 | 
				
			||||||
 | 
					  "search_results.total": "{count} {count, plural, one {résultat} other {résultats}}",
 | 
				
			||||||
  "upload_button.label": "Joindre un média",
 | 
					  "upload_button.label": "Joindre un média",
 | 
				
			||||||
  "upload_form.undo": "Annuler",
 | 
					  "upload_form.undo": "Annuler",
 | 
				
			||||||
  "video_player.toggle_sound": "Mettre/Couper le son",
 | 
					  "notification.follow": "{name} vous suit.",
 | 
				
			||||||
 | 
					  "notification.favourite": "{name} a ajouté à ses favoris :",
 | 
				
			||||||
 | 
					  "notification.reblog": "{name} a partagé votre statut :",
 | 
				
			||||||
 | 
					  "notification.mention": "{name} vous a mentionné⋅e :",
 | 
				
			||||||
 | 
					  "notifications.column_settings.alert": "Notifications locales",
 | 
				
			||||||
 | 
					  "notifications.column_settings.show": "Afficher dans la colonne",
 | 
				
			||||||
 | 
					  "notifications.column_settings.follow": "Nouveaux abonnés :",
 | 
				
			||||||
 | 
					  "notifications.column_settings.favourite": "Favoris :",
 | 
				
			||||||
 | 
					  "notifications.column_settings.mention": "Mentions :",
 | 
				
			||||||
 | 
					  "notifications.column_settings.reblog": "Partages :",
 | 
				
			||||||
 | 
					  "privacy.public.short": "Public",
 | 
				
			||||||
 | 
					  "privacy.public.long": "Afficher dans les fils publics",
 | 
				
			||||||
 | 
					  "privacy.unlisted.short": "Non-listé",
 | 
				
			||||||
 | 
					  "privacy.unlisted.long": "Ne pas afficher dans les fils publics",
 | 
				
			||||||
 | 
					  "privacy.private.short": "Privé",
 | 
				
			||||||
 | 
					  "privacy.private.long": "N’afficher que pour vos abonné⋅e⋅s",
 | 
				
			||||||
 | 
					  "privacy.direct.short": "Direct",
 | 
				
			||||||
 | 
					  "privacy.direct.long": "N’afficher que pour les personnes mentionné⋅e⋅s",
 | 
				
			||||||
 | 
					  "privacy.change": "Ajuster la confidentialité du message",
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default fr;
 | 
					export default fr;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ import hu from './hu';
 | 
				
			||||||
import fr from './fr';
 | 
					import fr from './fr';
 | 
				
			||||||
import pt from './pt';
 | 
					import pt from './pt';
 | 
				
			||||||
import uk from './uk';
 | 
					import uk from './uk';
 | 
				
			||||||
 | 
					import fi from './fi';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const locales = {
 | 
					const locales = {
 | 
				
			||||||
  en,
 | 
					  en,
 | 
				
			||||||
| 
						 | 
					@ -13,7 +14,8 @@ const locales = {
 | 
				
			||||||
  hu,
 | 
					  hu,
 | 
				
			||||||
  fr,
 | 
					  fr,
 | 
				
			||||||
  pt,
 | 
					  pt,
 | 
				
			||||||
  uk
 | 
					  uk,
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function getMessagesForLocale (locale) {
 | 
					export default function getMessagesForLocale (locale) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,7 @@ import {
 | 
				
			||||||
  STATUS_FETCH_SUCCESS,
 | 
					  STATUS_FETCH_SUCCESS,
 | 
				
			||||||
  CONTEXT_FETCH_SUCCESS
 | 
					  CONTEXT_FETCH_SUCCESS
 | 
				
			||||||
} from '../actions/statuses';
 | 
					} from '../actions/statuses';
 | 
				
			||||||
import { SEARCH_SUGGESTIONS_READY } from '../actions/search';
 | 
					import { SEARCH_FETCH_SUCCESS } from '../actions/search';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  NOTIFICATIONS_UPDATE,
 | 
					  NOTIFICATIONS_UPDATE,
 | 
				
			||||||
  NOTIFICATIONS_REFRESH_SUCCESS,
 | 
					  NOTIFICATIONS_REFRESH_SUCCESS,
 | 
				
			||||||
| 
						 | 
					@ -97,7 +97,7 @@ export default function accounts(state = initialState, action) {
 | 
				
			||||||
    return normalizeAccounts(state, action.accounts);
 | 
					    return normalizeAccounts(state, action.accounts);
 | 
				
			||||||
  case NOTIFICATIONS_REFRESH_SUCCESS:
 | 
					  case NOTIFICATIONS_REFRESH_SUCCESS:
 | 
				
			||||||
  case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
					  case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
				
			||||||
  case SEARCH_SUGGESTIONS_READY:
 | 
					  case SEARCH_FETCH_SUCCESS:
 | 
				
			||||||
    return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses);
 | 
					    return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses);
 | 
				
			||||||
  case TIMELINE_REFRESH_SUCCESS:
 | 
					  case TIMELINE_REFRESH_SUCCESS:
 | 
				
			||||||
  case TIMELINE_EXPAND_SUCCESS:
 | 
					  case TIMELINE_EXPAND_SUCCESS:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,31 +1,17 @@
 | 
				
			||||||
import {
 | 
					import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal';
 | 
				
			||||||
  MEDIA_OPEN,
 | 
					 | 
				
			||||||
  MODAL_CLOSE,
 | 
					 | 
				
			||||||
  MODAL_INDEX_DECREASE,
 | 
					 | 
				
			||||||
  MODAL_INDEX_INCREASE
 | 
					 | 
				
			||||||
} from '../actions/modal';
 | 
					 | 
				
			||||||
import Immutable from 'immutable';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const initialState = Immutable.Map({
 | 
					const initialState = {
 | 
				
			||||||
  media: null,
 | 
					  modalType: null,
 | 
				
			||||||
  index: 0,
 | 
					  modalProps: {}
 | 
				
			||||||
  open: false
 | 
					};
 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function modal(state = initialState, action) {
 | 
					export default function modal(state = initialState, action) {
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
  case MEDIA_OPEN:
 | 
					  case MODAL_OPEN:
 | 
				
			||||||
    return state.withMutations(map => {
 | 
					    return { modalType: action.modalType, modalProps: action.modalProps };
 | 
				
			||||||
      map.set('media', action.media);
 | 
					 | 
				
			||||||
      map.set('index', action.index);
 | 
					 | 
				
			||||||
      map.set('open', true);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  case MODAL_CLOSE:
 | 
					  case MODAL_CLOSE:
 | 
				
			||||||
    return state.set('open', false);
 | 
					    return initialState;
 | 
				
			||||||
  case MODAL_INDEX_DECREASE:
 | 
					 | 
				
			||||||
    return state.update('index', index => (index - 1) % state.get('media').size);
 | 
					 | 
				
			||||||
  case MODAL_INDEX_INCREASE:
 | 
					 | 
				
			||||||
    return state.update('index', index => (index + 1) % state.get('media').size);
 | 
					 | 
				
			||||||
  default:
 | 
					  default:
 | 
				
			||||||
    return state;
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,16 +23,16 @@ const initialState = Immutable.Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function relationships(state = initialState, action) {
 | 
					export default function relationships(state = initialState, action) {
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
    case ACCOUNT_FOLLOW_SUCCESS:
 | 
					  case ACCOUNT_FOLLOW_SUCCESS:
 | 
				
			||||||
    case ACCOUNT_UNFOLLOW_SUCCESS:
 | 
					  case ACCOUNT_UNFOLLOW_SUCCESS:
 | 
				
			||||||
    case ACCOUNT_BLOCK_SUCCESS:
 | 
					  case ACCOUNT_BLOCK_SUCCESS:
 | 
				
			||||||
    case ACCOUNT_UNBLOCK_SUCCESS:
 | 
					  case ACCOUNT_UNBLOCK_SUCCESS:
 | 
				
			||||||
    case ACCOUNT_MUTE_SUCCESS:
 | 
					  case ACCOUNT_MUTE_SUCCESS:
 | 
				
			||||||
    case ACCOUNT_UNMUTE_SUCCESS:
 | 
					  case ACCOUNT_UNMUTE_SUCCESS:
 | 
				
			||||||
      return normalizeRelationship(state, action.relationship);
 | 
					    return normalizeRelationship(state, action.relationship);
 | 
				
			||||||
    case RELATIONSHIPS_FETCH_SUCCESS:
 | 
					  case RELATIONSHIPS_FETCH_SUCCESS:
 | 
				
			||||||
      return normalizeRelationships(state, action.relationships);
 | 
					    return normalizeRelationships(state, action.relationships);
 | 
				
			||||||
    default:
 | 
					  default:
 | 
				
			||||||
      return state;
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,17 @@
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  SEARCH_CHANGE,
 | 
					  SEARCH_CHANGE,
 | 
				
			||||||
  SEARCH_SUGGESTIONS_READY,
 | 
					  SEARCH_CLEAR,
 | 
				
			||||||
  SEARCH_RESET
 | 
					  SEARCH_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					  SEARCH_SHOW
 | 
				
			||||||
} from '../actions/search';
 | 
					} from '../actions/search';
 | 
				
			||||||
 | 
					import { COMPOSE_MENTION, COMPOSE_REPLY } from '../actions/compose';
 | 
				
			||||||
import Immutable from 'immutable';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const initialState = Immutable.Map({
 | 
					const initialState = Immutable.Map({
 | 
				
			||||||
  value: '',
 | 
					  value: '',
 | 
				
			||||||
  loaded_value: '',
 | 
					  submitted: false,
 | 
				
			||||||
  suggestions: []
 | 
					  hidden: false,
 | 
				
			||||||
 | 
					  results: Immutable.Map()
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const normalizeSuggestions = (state, value, accounts, hashtags, statuses) => {
 | 
					const normalizeSuggestions = (state, value, accounts, hashtags, statuses) => {
 | 
				
			||||||
| 
						 | 
					@ -69,14 +72,24 @@ export default function search(state = initialState, action) {
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
  case SEARCH_CHANGE:
 | 
					  case SEARCH_CHANGE:
 | 
				
			||||||
    return state.set('value', action.value);
 | 
					    return state.set('value', action.value);
 | 
				
			||||||
  case SEARCH_SUGGESTIONS_READY:
 | 
					  case SEARCH_CLEAR:
 | 
				
			||||||
    return normalizeSuggestions(state, action.value, action.accounts, action.hashtags, action.statuses);
 | 
					 | 
				
			||||||
  case SEARCH_RESET:
 | 
					 | 
				
			||||||
    return state.withMutations(map => {
 | 
					    return state.withMutations(map => {
 | 
				
			||||||
      map.set('suggestions', []);
 | 
					 | 
				
			||||||
      map.set('value', '');
 | 
					      map.set('value', '');
 | 
				
			||||||
      map.set('loaded_value', '');
 | 
					      map.set('results', Immutable.Map());
 | 
				
			||||||
 | 
					      map.set('submitted', false);
 | 
				
			||||||
 | 
					      map.set('hidden', false);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  case SEARCH_SHOW:
 | 
				
			||||||
 | 
					    return state.set('hidden', false);
 | 
				
			||||||
 | 
					  case COMPOSE_REPLY:
 | 
				
			||||||
 | 
					  case COMPOSE_MENTION:
 | 
				
			||||||
 | 
					    return state.set('hidden', true);
 | 
				
			||||||
 | 
					  case SEARCH_FETCH_SUCCESS:
 | 
				
			||||||
 | 
					    return state.set('results', Immutable.Map({
 | 
				
			||||||
 | 
					      accounts: Immutable.List(action.results.accounts.map(item => item.id)),
 | 
				
			||||||
 | 
					      statuses: Immutable.List(action.results.statuses.map(item => item.id)),
 | 
				
			||||||
 | 
					      hashtags: Immutable.List(action.results.hashtags)
 | 
				
			||||||
 | 
					    })).set('submitted', true);
 | 
				
			||||||
  default:
 | 
					  default:
 | 
				
			||||||
    return state;
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,7 +32,7 @@ import {
 | 
				
			||||||
  FAVOURITED_STATUSES_FETCH_SUCCESS,
 | 
					  FAVOURITED_STATUSES_FETCH_SUCCESS,
 | 
				
			||||||
  FAVOURITED_STATUSES_EXPAND_SUCCESS
 | 
					  FAVOURITED_STATUSES_EXPAND_SUCCESS
 | 
				
			||||||
} from '../actions/favourites';
 | 
					} from '../actions/favourites';
 | 
				
			||||||
import { SEARCH_SUGGESTIONS_READY } from '../actions/search';
 | 
					import { SEARCH_FETCH_SUCCESS } from '../actions/search';
 | 
				
			||||||
import Immutable from 'immutable';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const normalizeStatus = (state, status) => {
 | 
					const normalizeStatus = (state, status) => {
 | 
				
			||||||
| 
						 | 
					@ -109,7 +109,7 @@ export default function statuses(state = initialState, action) {
 | 
				
			||||||
  case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
					  case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
				
			||||||
  case FAVOURITED_STATUSES_FETCH_SUCCESS:
 | 
					  case FAVOURITED_STATUSES_FETCH_SUCCESS:
 | 
				
			||||||
  case FAVOURITED_STATUSES_EXPAND_SUCCESS:
 | 
					  case FAVOURITED_STATUSES_EXPAND_SUCCESS:
 | 
				
			||||||
  case SEARCH_SUGGESTIONS_READY:
 | 
					  case SEARCH_FETCH_SUCCESS:
 | 
				
			||||||
    return normalizeStatuses(state, action.statuses);
 | 
					    return normalizeStatuses(state, action.statuses);
 | 
				
			||||||
  case TIMELINE_DELETE:
 | 
					  case TIMELINE_DELETE:
 | 
				
			||||||
    return deleteStatus(state, action.id, action.references);
 | 
					    return deleteStatus(state, action.id, action.references);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,9 @@ import {
 | 
				
			||||||
  TIMELINE_EXPAND_SUCCESS,
 | 
					  TIMELINE_EXPAND_SUCCESS,
 | 
				
			||||||
  TIMELINE_EXPAND_REQUEST,
 | 
					  TIMELINE_EXPAND_REQUEST,
 | 
				
			||||||
  TIMELINE_EXPAND_FAIL,
 | 
					  TIMELINE_EXPAND_FAIL,
 | 
				
			||||||
  TIMELINE_SCROLL_TOP
 | 
					  TIMELINE_SCROLL_TOP,
 | 
				
			||||||
 | 
					  TIMELINE_CONNECT,
 | 
				
			||||||
 | 
					  TIMELINE_DISCONNECT
 | 
				
			||||||
} from '../actions/timelines';
 | 
					} from '../actions/timelines';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  REBLOG_SUCCESS,
 | 
					  REBLOG_SUCCESS,
 | 
				
			||||||
| 
						 | 
					@ -35,6 +37,7 @@ const initialState = Immutable.Map({
 | 
				
			||||||
    path: () => '/api/v1/timelines/home',
 | 
					    path: () => '/api/v1/timelines/home',
 | 
				
			||||||
    next: null,
 | 
					    next: null,
 | 
				
			||||||
    isLoading: false,
 | 
					    isLoading: false,
 | 
				
			||||||
 | 
					    online: false,
 | 
				
			||||||
    loaded: false,
 | 
					    loaded: false,
 | 
				
			||||||
    top: true,
 | 
					    top: true,
 | 
				
			||||||
    unread: 0,
 | 
					    unread: 0,
 | 
				
			||||||
| 
						 | 
					@ -45,6 +48,7 @@ const initialState = Immutable.Map({
 | 
				
			||||||
    path: () => '/api/v1/timelines/public',
 | 
					    path: () => '/api/v1/timelines/public',
 | 
				
			||||||
    next: null,
 | 
					    next: null,
 | 
				
			||||||
    isLoading: false,
 | 
					    isLoading: false,
 | 
				
			||||||
 | 
					    online: false,
 | 
				
			||||||
    loaded: false,
 | 
					    loaded: false,
 | 
				
			||||||
    top: true,
 | 
					    top: true,
 | 
				
			||||||
    unread: 0,
 | 
					    unread: 0,
 | 
				
			||||||
| 
						 | 
					@ -56,6 +60,7 @@ const initialState = Immutable.Map({
 | 
				
			||||||
    next: null,
 | 
					    next: null,
 | 
				
			||||||
    params: { local: true },
 | 
					    params: { local: true },
 | 
				
			||||||
    isLoading: false,
 | 
					    isLoading: false,
 | 
				
			||||||
 | 
					    online: false,
 | 
				
			||||||
    loaded: false,
 | 
					    loaded: false,
 | 
				
			||||||
    top: true,
 | 
					    top: true,
 | 
				
			||||||
    unread: 0,
 | 
					    unread: 0,
 | 
				
			||||||
| 
						 | 
					@ -300,6 +305,10 @@ export default function timelines(state = initialState, action) {
 | 
				
			||||||
    return filterTimelines(state, action.relationship, action.statuses);
 | 
					    return filterTimelines(state, action.relationship, action.statuses);
 | 
				
			||||||
  case TIMELINE_SCROLL_TOP:
 | 
					  case TIMELINE_SCROLL_TOP:
 | 
				
			||||||
    return updateTop(state, action.timeline, action.top);
 | 
					    return updateTop(state, action.timeline, action.top);
 | 
				
			||||||
 | 
					  case TIMELINE_CONNECT:
 | 
				
			||||||
 | 
					    return state.setIn([action.timeline, 'online'], true);
 | 
				
			||||||
 | 
					  case TIMELINE_DISCONNECT:
 | 
				
			||||||
 | 
					    return state.setIn([action.timeline, 'online'], false);
 | 
				
			||||||
  default:
 | 
					  default:
 | 
				
			||||||
    return state;
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ const getStatuses = state => state.get('statuses');
 | 
				
			||||||
const getAccounts = state => state.get('accounts');
 | 
					const getAccounts = state => state.get('accounts');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getAccountBase         = (state, id) => state.getIn(['accounts', id], null);
 | 
					const getAccountBase         = (state, id) => state.getIn(['accounts', id], null);
 | 
				
			||||||
const getAccountRelationship = (state, id) => state.getIn(['relationships', id]);
 | 
					const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const makeGetAccount = () => {
 | 
					export const makeGetAccount = () => {
 | 
				
			||||||
  return createSelector([getAccountBase, getAccountRelationship], (base, relationship) => {
 | 
					  return createSelector([getAccountBase, getAccountRelationship], (base, relationship) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,4 +24,17 @@ $(() => {
 | 
				
			||||||
      window.location.href = $(e.target).attr('href');
 | 
					      window.location.href = $(e.target).attr('href');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $('.status__content__spoiler-link').on('click', e => {
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					    const contentEl = $(e.target).parent().parent().find('div');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (contentEl.is(':visible')) {
 | 
				
			||||||
 | 
					      contentEl.hide();
 | 
				
			||||||
 | 
					      $(e.target).parent().attr('style', 'margin-bottom: 0');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      contentEl.show();
 | 
				
			||||||
 | 
					      $(e.target).parent().attr('style', null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -311,6 +311,7 @@
 | 
				
			||||||
      padding: 10px;
 | 
					      padding: 10px;
 | 
				
			||||||
      padding-top: 15px;
 | 
					      padding-top: 15px;
 | 
				
			||||||
      color: $color3;
 | 
					      color: $color3;
 | 
				
			||||||
 | 
					      word-wrap: break-word;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@
 | 
				
			||||||
  text-decoration: none;
 | 
					  text-decoration: none;
 | 
				
			||||||
  transition: all 100ms ease-in;
 | 
					  transition: all 100ms ease-in;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover, &:active, &:focus {
 | 
				
			||||||
    background-color: lighten($color4, 7%);
 | 
					    background-color: lighten($color4, 7%);
 | 
				
			||||||
    transition: all 200ms ease-out;
 | 
					    transition: all 200ms ease-out;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -54,7 +54,7 @@
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  transition: all 100ms ease-in;
 | 
					  transition: all 100ms ease-in;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover, &:active, &:focus {
 | 
				
			||||||
    color: lighten($color1, 33%);
 | 
					    color: lighten($color1, 33%);
 | 
				
			||||||
    transition: all 200ms ease-out;
 | 
					    transition: all 200ms ease-out;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -79,7 +79,7 @@
 | 
				
			||||||
  &.inverted {
 | 
					  &.inverted {
 | 
				
			||||||
    color: lighten($color1, 33%);
 | 
					    color: lighten($color1, 33%);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &:hover {
 | 
					    &:hover, &:active, &:focus {
 | 
				
			||||||
      color: lighten($color1, 26%);
 | 
					      color: lighten($color1, 26%);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,7 +105,7 @@
 | 
				
			||||||
  outline: 0;
 | 
					  outline: 0;
 | 
				
			||||||
  transition: all 100ms ease-in;
 | 
					  transition: all 100ms ease-in;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover, &:active, &:focus {
 | 
				
			||||||
    color: lighten($color1, 26%);
 | 
					    color: lighten($color1, 26%);
 | 
				
			||||||
    transition: all 200ms ease-out;
 | 
					    transition: all 200ms ease-out;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -424,6 +424,7 @@ a.status__content__spoiler-link {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.account__header__content {
 | 
					.account__header__content {
 | 
				
			||||||
  word-wrap: break-word;
 | 
					  word-wrap: break-word;
 | 
				
			||||||
 | 
					  word-break: normal;
 | 
				
			||||||
  font-weight: 400;
 | 
					  font-weight: 400;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
  color: $color3;
 | 
					  color: $color3;
 | 
				
			||||||
| 
						 | 
					@ -764,8 +765,19 @@ a.status__content__spoiler-link {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.drawer__pager {
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  flex-grow: 1;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.drawer__inner {
 | 
					.drawer__inner {
 | 
				
			||||||
  //background: linear-gradient(rgba(lighten($color1, 13%), 1), rgba(lighten($color1, 13%), 0.65));
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
  background: lighten($color1, 13%);
 | 
					  background: lighten($color1, 13%);
 | 
				
			||||||
  box-sizing: border-box;
 | 
					  box-sizing: border-box;
 | 
				
			||||||
  padding: 0;
 | 
					  padding: 0;
 | 
				
			||||||
| 
						 | 
					@ -773,7 +785,12 @@ a.status__content__spoiler-link {
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
  overflow-y: auto;
 | 
					  overflow-y: auto;
 | 
				
			||||||
  flex-grow: 1;
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.darker {
 | 
				
			||||||
 | 
					    background: $color1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.drawer__header {
 | 
					.drawer__header {
 | 
				
			||||||
| 
						 | 
					@ -842,11 +859,25 @@ a.status__content__spoiler-link {
 | 
				
			||||||
  font-size:12px;
 | 
					  font-size:12px;
 | 
				
			||||||
  font-weight: 500;
 | 
					  font-weight: 500;
 | 
				
			||||||
  border-bottom: 2px solid lighten($color1, 8%);
 | 
					  border-bottom: 2px solid lighten($color1, 8%);
 | 
				
			||||||
 | 
					  transition: all 200ms linear;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .fa {
 | 
				
			||||||
 | 
					    font-weight: 400;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.active {
 | 
					  &.active {
 | 
				
			||||||
    border-bottom: 2px solid $color4;
 | 
					    border-bottom: 2px solid $color4;
 | 
				
			||||||
    color: $color4;
 | 
					    color: $color4;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover, &:focus, &:active {
 | 
				
			||||||
 | 
					    background: lighten($color1, 14%);
 | 
				
			||||||
 | 
					    transition: all 100ms linear;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  span {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media screen and (min-width: 360px) {
 | 
					@media screen and (min-width: 360px) {
 | 
				
			||||||
| 
						 | 
					@ -854,6 +885,22 @@ a.status__content__spoiler-link {
 | 
				
			||||||
    margin: 10px;
 | 
					    margin: 10px;
 | 
				
			||||||
    margin-bottom: 0;
 | 
					    margin-bottom: 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .search {
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media screen and (min-width: 600px) {
 | 
				
			||||||
 | 
					  .tabs-bar__link {
 | 
				
			||||||
 | 
					    .fa {
 | 
				
			||||||
 | 
					      margin-right: 5px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    span {
 | 
				
			||||||
 | 
					      display: inline;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media screen and (min-width: 1025px) {
 | 
					@media screen and (min-width: 1025px) {
 | 
				
			||||||
| 
						 | 
					@ -1102,11 +1149,9 @@ a.status__content__spoiler-link {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.getting-started {
 | 
					.getting-started {
 | 
				
			||||||
  box-sizing: border-box;
 | 
					  box-sizing: border-box;
 | 
				
			||||||
  overflow-y: auto;
 | 
					 | 
				
			||||||
  padding-bottom: 235px;
 | 
					  padding-bottom: 235px;
 | 
				
			||||||
  background: image-url('mastodon-getting-started.png') no-repeat bottom left;
 | 
					  background: image-url('mastodon-getting-started.png') no-repeat 0 100% local;
 | 
				
			||||||
  height: auto;
 | 
					  flex: 1 0 auto;
 | 
				
			||||||
  min-height: 100%;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  p {
 | 
					  p {
 | 
				
			||||||
    color: $color2;
 | 
					    color: $color2;
 | 
				
			||||||
| 
						 | 
					@ -1224,26 +1269,6 @@ button.active i.fa-retweet {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.search {
 | 
					 | 
				
			||||||
  .fa {
 | 
					 | 
				
			||||||
    color: $color3;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.search__input {
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  border: none;
 | 
					 | 
				
			||||||
  padding: 10px;
 | 
					 | 
				
			||||||
  padding-right: 30px;
 | 
					 | 
				
			||||||
  font-family: inherit;
 | 
					 | 
				
			||||||
  background: $color1;
 | 
					 | 
				
			||||||
  color: $color3;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  margin: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.loading-indicator {
 | 
					.loading-indicator {
 | 
				
			||||||
  color: $color2;
 | 
					  color: $color2;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1286,7 +1311,7 @@ button.active i.fa-retweet {
 | 
				
			||||||
  color: $color3;
 | 
					  color: $color3;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.modal-container--nav {
 | 
					.modal-container__nav {
 | 
				
			||||||
  color: $color5;
 | 
					  color: $color5;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1640,7 +1665,7 @@ button.active i.fa-retweet {
 | 
				
			||||||
    margin-top: 2px;
 | 
					    margin-top: 2px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover, &:active, &:focus {
 | 
				
			||||||
    img {
 | 
					    img {
 | 
				
			||||||
      opacity: 1;
 | 
					      opacity: 1;
 | 
				
			||||||
      filter: none;
 | 
					      filter: none;
 | 
				
			||||||
| 
						 | 
					@ -1723,3 +1748,147 @@ button.active i.fa-retweet {
 | 
				
			||||||
    box-shadow: 2px 4px 6px rgba($color8, 0.1);
 | 
					    box-shadow: 2px 4px 6px rgba($color8, 0.1);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.search {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.search__input {
 | 
				
			||||||
 | 
					  padding-right: 30px;
 | 
				
			||||||
 | 
					  color: $color2;
 | 
				
			||||||
 | 
					  outline: 0;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					  padding-right: 30px;
 | 
				
			||||||
 | 
					  font-family: inherit;
 | 
				
			||||||
 | 
					  background: $color1;
 | 
				
			||||||
 | 
					  color: $color3;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &::-moz-focus-inner {
 | 
				
			||||||
 | 
					    border: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &::-moz-focus-inner, &:focus, &:active {
 | 
				
			||||||
 | 
					    outline: 0 !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:focus {
 | 
				
			||||||
 | 
					    background: lighten($color1, 4%);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.search__icon {
 | 
				
			||||||
 | 
					  .fa {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 10px;
 | 
				
			||||||
 | 
					    right: 10px;
 | 
				
			||||||
 | 
					    z-index: 2;
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    transition: all 100ms linear;
 | 
				
			||||||
 | 
					    font-size: 18px;
 | 
				
			||||||
 | 
					    width: 18px;
 | 
				
			||||||
 | 
					    height: 18px;
 | 
				
			||||||
 | 
					    color: $color2;
 | 
				
			||||||
 | 
					    cursor: default;
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.active {
 | 
				
			||||||
 | 
					      pointer-events: auto;
 | 
				
			||||||
 | 
					      opacity: 0.3;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .fa-search {
 | 
				
			||||||
 | 
					    transform: translateZ(0) rotate(90deg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.active {
 | 
				
			||||||
 | 
					      pointer-events: none;
 | 
				
			||||||
 | 
					      transform: translateZ(0) rotate(0deg);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .fa-times-circle {
 | 
				
			||||||
 | 
					    top: 11px;
 | 
				
			||||||
 | 
					    transform: translateZ(0) rotate(0deg);
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.active {
 | 
				
			||||||
 | 
					      transform: translateZ(0) rotate(90deg);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      color: $color5;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.search-results__header {
 | 
				
			||||||
 | 
					  color: lighten($color1, 26%);
 | 
				
			||||||
 | 
					  background: lighten($color1, 2%);
 | 
				
			||||||
 | 
					  border-bottom: 1px solid darken($color1, 4%);
 | 
				
			||||||
 | 
					  padding: 15px 10px;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.search-results__hashtag {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					  color: $color2;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover, &:active, &:focus {
 | 
				
			||||||
 | 
					    color: lighten($color2, 4%);
 | 
				
			||||||
 | 
					    text-decoration: underline;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.modal-root__overlay {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  bottom: 0;
 | 
				
			||||||
 | 
					  z-index: 9999;
 | 
				
			||||||
 | 
					  opacity: 0;
 | 
				
			||||||
 | 
					  background: rgba($color8, 0.7);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.modal-root__container {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-content: space-around;
 | 
				
			||||||
 | 
					  z-index: 9999;
 | 
				
			||||||
 | 
					  opacity: 0;
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					  user-select: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.modal-root__modal {
 | 
				
			||||||
 | 
					  pointer-events: auto;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.media-modal {
 | 
				
			||||||
 | 
					  max-width: 80vw;
 | 
				
			||||||
 | 
					  max-height: 80vh;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  img, video {
 | 
				
			||||||
 | 
					    max-width: 80vw;
 | 
				
			||||||
 | 
					    max-height: 80vh;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -97,6 +97,15 @@
 | 
				
			||||||
      a {
 | 
					      a {
 | 
				
			||||||
        color: $color4;
 | 
					        color: $color4;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      a.status__content__spoiler-link {
 | 
				
			||||||
 | 
					        color: $color5;
 | 
				
			||||||
 | 
					        background: $color3;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          background: lighten($color3, 8%);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .status__attachments {
 | 
					    .status__attachments {
 | 
				
			||||||
| 
						 | 
					@ -163,6 +172,15 @@
 | 
				
			||||||
      a {
 | 
					      a {
 | 
				
			||||||
        color: $color4;
 | 
					        color: $color4;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      a.status__content__spoiler-link {
 | 
				
			||||||
 | 
					        color: $color5;
 | 
				
			||||||
 | 
					        background: $color3;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          background: lighten($color3, 8%);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .detailed-status__meta {
 | 
					    .detailed-status__meta {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,9 @@ class AboutController < ApplicationController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
    @description = Setting.site_description
 | 
					    @description = Setting.site_description
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @user = User.new
 | 
				
			||||||
 | 
					    @user.build_account
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def more
 | 
					  def more
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,24 @@ class Admin::DomainBlocksController < ApplicationController
 | 
				
			||||||
    @blocks = DomainBlock.paginate(page: params[:page], per_page: 40)
 | 
					    @blocks = DomainBlock.paginate(page: params[:page], per_page: 40)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def new
 | 
				
			||||||
 | 
					    @domain_block = DomainBlock.new
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
 | 
					    @domain_block = DomainBlock.new(resource_params)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if @domain_block.save
 | 
				
			||||||
 | 
					      DomainBlockWorker.perform_async(@domain_block.id)
 | 
				
			||||||
 | 
					      redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      render action: :new
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def resource_params
 | 
				
			||||||
 | 
					    params.require(:domain_block).permit(:domain, :severity)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ class Admin::ReportsController < ApplicationController
 | 
				
			||||||
  layout 'admin'
 | 
					  layout 'admin'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
    @reports = Report.includes(:account, :target_account).paginate(page: params[:page], per_page: 40)
 | 
					    @reports = Report.includes(:account, :target_account).order('id desc').paginate(page: params[:page], per_page: 40)
 | 
				
			||||||
    @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
 | 
					    @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,19 +16,19 @@ class Admin::ReportsController < ApplicationController
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def resolve
 | 
					  def resolve
 | 
				
			||||||
    @report.update(action_taken: true)
 | 
					    @report.update(action_taken: true, action_taken_by_account_id: current_account.id)
 | 
				
			||||||
    redirect_to admin_report_path(@report)
 | 
					    redirect_to admin_report_path(@report)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def suspend
 | 
					  def suspend
 | 
				
			||||||
    Admin::SuspensionWorker.perform_async(@report.target_account.id)
 | 
					    Admin::SuspensionWorker.perform_async(@report.target_account.id)
 | 
				
			||||||
    @report.update(action_taken: true)
 | 
					    Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
 | 
				
			||||||
    redirect_to admin_report_path(@report)
 | 
					    redirect_to admin_report_path(@report)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def silence
 | 
					  def silence
 | 
				
			||||||
    @report.target_account.update(silenced: true)
 | 
					    @report.target_account.update(silenced: true)
 | 
				
			||||||
    @report.update(action_taken: true)
 | 
					    Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
 | 
				
			||||||
    redirect_to admin_report_path(@report)
 | 
					    redirect_to admin_report_path(@report)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,12 @@ class Api::V1::AppsController < ApiController
 | 
				
			||||||
  respond_to :json
 | 
					  respond_to :json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
    @app = Doorkeeper::Application.create!(name: params[:client_name], redirect_uri: params[:redirect_uris], scopes: (params[:scopes] || Doorkeeper.configuration.default_scopes), website: params[:website])
 | 
					    @app = Doorkeeper::Application.create!(name: app_params[:client_name], redirect_uri: app_params[:redirect_uris], scopes: (app_params[:scopes] || Doorkeeper.configuration.default_scopes), website: app_params[:website])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def app_params
 | 
				
			||||||
 | 
					    params.permit(:client_name, :redirect_uris, :scopes, :website)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ class Api::V1::FollowsController < ApiController
 | 
				
			||||||
  respond_to :json
 | 
					  respond_to :json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
    raise ActiveRecord::RecordNotFound if params[:uri].blank?
 | 
					    raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
 | 
					    @account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
 | 
				
			||||||
    render action: :show
 | 
					    render action: :show
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,10 @@ class Api::V1::FollowsController < ApiController
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def target_uri
 | 
					  def target_uri
 | 
				
			||||||
    params[:uri].strip.gsub(/\A@/, '')
 | 
					    follow_params[:uri].strip.gsub(/\A@/, '')
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def follow_params
 | 
				
			||||||
 | 
					    params.permit(:uri)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,10 +10,16 @@ class Api::V1::MediaController < ApiController
 | 
				
			||||||
  respond_to :json
 | 
					  respond_to :json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
    @media = MediaAttachment.create!(account: current_user.account, file: params[:file])
 | 
					    @media = MediaAttachment.create!(account: current_user.account, file: media_params[:file])
 | 
				
			||||||
  rescue Paperclip::Errors::NotIdentifiedByImageMagickError
 | 
					  rescue Paperclip::Errors::NotIdentifiedByImageMagickError
 | 
				
			||||||
    render json: { error: 'File type of uploaded media could not be verified' }, status: 422
 | 
					    render json: { error: 'File type of uploaded media could not be verified' }, status: 422
 | 
				
			||||||
  rescue Paperclip::Error
 | 
					  rescue Paperclip::Error
 | 
				
			||||||
    render json: { error: 'Error processing thumbnail for uploaded media' }, status: 500
 | 
					    render json: { error: 'Error processing thumbnail for uploaded media' }, status: 500
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def media_params
 | 
				
			||||||
 | 
					    params.permit(:file)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,13 +12,19 @@ class Api::V1::ReportsController < ApiController
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
    status_ids = params[:status_ids].is_a?(Enumerable) ? params[:status_ids] : [params[:status_ids]]
 | 
					    status_ids = report_params[:status_ids].is_a?(Enumerable) ? report_params[:status_ids] : [report_params[:status_ids]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @report = Report.create!(account: current_account,
 | 
					    @report = Report.create!(account: current_account,
 | 
				
			||||||
                             target_account: Account.find(params[:account_id]),
 | 
					                             target_account: Account.find(report_params[:account_id]),
 | 
				
			||||||
                             status_ids: Status.find(status_ids).pluck(:id),
 | 
					                             status_ids: Status.find(status_ids).pluck(:id),
 | 
				
			||||||
                             comment: params[:comment])
 | 
					                             comment: report_params[:comment])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    render :show
 | 
					    render :show
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def report_params
 | 
				
			||||||
 | 
					    params.permit(:account_id, :comment, status_ids: [])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -62,11 +62,11 @@ class Api::V1::StatusesController < ApiController
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
    @status = PostStatusService.new.call(current_user.account, params[:status], params[:in_reply_to_id].blank? ? nil : Status.find(params[:in_reply_to_id]), media_ids: params[:media_ids],
 | 
					    @status = PostStatusService.new.call(current_user.account, status_params[:status], status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]), media_ids: status_params[:media_ids],
 | 
				
			||||||
                                                                                                                                                             sensitive: params[:sensitive],
 | 
					                                                                                                                                                                                  sensitive: status_params[:sensitive],
 | 
				
			||||||
                                                                                                                                                             spoiler_text: params[:spoiler_text],
 | 
					                                                                                                                                                                                  spoiler_text: status_params[:spoiler_text],
 | 
				
			||||||
                                                                                                                                                             visibility: params[:visibility],
 | 
					                                                                                                                                                                                  visibility: status_params[:visibility],
 | 
				
			||||||
                                                                                                                                                             application: doorkeeper_token.application)
 | 
					                                                                                                                                                                                  application: doorkeeper_token.application)
 | 
				
			||||||
    render action: :show
 | 
					    render action: :show
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -111,4 +111,8 @@ class Api::V1::StatusesController < ApiController
 | 
				
			||||||
    @status = Status.find(params[:id])
 | 
					    @status = Status.find(params[:id])
 | 
				
			||||||
    raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account)
 | 
					    raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def status_params
 | 
				
			||||||
 | 
					    params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, media_ids: [])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,8 +11,8 @@ class Api::V1::TimelinesController < ApiController
 | 
				
			||||||
    @statuses = cache_collection(@statuses)
 | 
					    @statuses = cache_collection(@statuses)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    set_maps(@statuses)
 | 
					    set_maps(@statuses)
 | 
				
			||||||
    set_counters_maps(@statuses)
 | 
					    # set_counters_maps(@statuses)
 | 
				
			||||||
    set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
 | 
					    # set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    next_path = api_v1_home_timeline_url(max_id: @statuses.last.id)    unless @statuses.empty?
 | 
					    next_path = api_v1_home_timeline_url(max_id: @statuses.last.id)    unless @statuses.empty?
 | 
				
			||||||
    prev_path = api_v1_home_timeline_url(since_id: @statuses.first.id) unless @statuses.empty?
 | 
					    prev_path = api_v1_home_timeline_url(since_id: @statuses.first.id) unless @statuses.empty?
 | 
				
			||||||
| 
						 | 
					@ -27,8 +27,8 @@ class Api::V1::TimelinesController < ApiController
 | 
				
			||||||
    @statuses = cache_collection(@statuses)
 | 
					    @statuses = cache_collection(@statuses)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    set_maps(@statuses)
 | 
					    set_maps(@statuses)
 | 
				
			||||||
    set_counters_maps(@statuses)
 | 
					    # set_counters_maps(@statuses)
 | 
				
			||||||
    set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
 | 
					    # set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    next_path = api_v1_public_timeline_url(max_id: @statuses.last.id)    unless @statuses.empty?
 | 
					    next_path = api_v1_public_timeline_url(max_id: @statuses.last.id)    unless @statuses.empty?
 | 
				
			||||||
    prev_path = api_v1_public_timeline_url(since_id: @statuses.first.id) unless @statuses.empty?
 | 
					    prev_path = api_v1_public_timeline_url(since_id: @statuses.first.id) unless @statuses.empty?
 | 
				
			||||||
| 
						 | 
					@ -44,8 +44,8 @@ class Api::V1::TimelinesController < ApiController
 | 
				
			||||||
    @statuses = cache_collection(@statuses)
 | 
					    @statuses = cache_collection(@statuses)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    set_maps(@statuses)
 | 
					    set_maps(@statuses)
 | 
				
			||||||
    set_counters_maps(@statuses)
 | 
					    # set_counters_maps(@statuses)
 | 
				
			||||||
    set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
 | 
					    # set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    next_path = api_v1_hashtag_timeline_url(params[:id], max_id: @statuses.last.id)    unless @statuses.empty?
 | 
					    next_path = api_v1_hashtag_timeline_url(params[:id], max_id: @statuses.last.id)    unless @statuses.empty?
 | 
				
			||||||
    prev_path = api_v1_hashtag_timeline_url(params[:id], since_id: @statuses.first.id) unless @statuses.empty?
 | 
					    prev_path = api_v1_hashtag_timeline_url(params[:id], since_id: @statuses.first.id) unless @statuses.empty?
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,7 +39,14 @@ class ApplicationController < ActionController::Base
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_user_activity
 | 
					  def set_user_activity
 | 
				
			||||||
    current_user.touch(:current_sign_in_at) if !current_user.nil? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < 24.hours.ago)
 | 
					    return unless !current_user.nil? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < 24.hours.ago)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Mark user as signed-in today
 | 
				
			||||||
 | 
					    current_user.update_tracked_fields(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # If the sign in is after a two week break, we need to regenerate their feed
 | 
				
			||||||
 | 
					    RegenerationWorker.perform_async(current_user.account_id) if current_user.last_sign_in_at < 14.days.ago
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def check_suspension
 | 
					  def check_suspension
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@
 | 
				
			||||||
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
 | 
					class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
 | 
				
			||||||
  skip_before_action :authenticate_resource_owner!
 | 
					  skip_before_action :authenticate_resource_owner!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_action :set_locale
 | 
				
			||||||
  before_action :store_current_location
 | 
					  before_action :store_current_location
 | 
				
			||||||
  before_action :authenticate_resource_owner!
 | 
					  before_action :authenticate_resource_owner!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,4 +12,10 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
 | 
				
			||||||
  def store_current_location
 | 
					  def store_current_location
 | 
				
			||||||
    store_location_for(:user, request.url)
 | 
					    store_location_for(:user, request.url)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_locale
 | 
				
			||||||
 | 
					    I18n.locale = current_user.try(:locale) || I18n.default_locale
 | 
				
			||||||
 | 
					  rescue I18n::InvalidLocale
 | 
				
			||||||
 | 
					    I18n.locale = I18n.default_locale
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Settings::ImportsController < ApplicationController
 | 
				
			||||||
 | 
					  layout 'admin'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_action :authenticate_user!
 | 
				
			||||||
 | 
					  before_action :set_account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def show
 | 
				
			||||||
 | 
					    @import = Import.new
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create
 | 
				
			||||||
 | 
					    @import = Import.new(import_params)
 | 
				
			||||||
 | 
					    @import.account = @account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if @import.save
 | 
				
			||||||
 | 
					      ImportWorker.perform_async(@import.id)
 | 
				
			||||||
 | 
					      redirect_to settings_import_path, notice: I18n.t('imports.success')
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      render action: :show
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_account
 | 
				
			||||||
 | 
					    @account = current_user.account
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def import_params
 | 
				
			||||||
 | 
					    params.require(:import).permit(:data, :type)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,7 @@ module SettingsHelper
 | 
				
			||||||
    hu: 'Magyar',
 | 
					    hu: 'Magyar',
 | 
				
			||||||
    uk: 'Українська',
 | 
					    uk: 'Українська',
 | 
				
			||||||
    'zh-CN': '简体中文',
 | 
					    'zh-CN': '简体中文',
 | 
				
			||||||
 | 
					    fi: 'Suomi',
 | 
				
			||||||
  }.freeze
 | 
					  }.freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def human_locale(locale)
 | 
					  def human_locale(locale)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,4 +4,5 @@ module Mastodon
 | 
				
			||||||
  class Error < StandardError; end
 | 
					  class Error < StandardError; end
 | 
				
			||||||
  class NotPermittedError < Error; end
 | 
					  class NotPermittedError < Error; end
 | 
				
			||||||
  class ValidationError < Error; end
 | 
					  class ValidationError < Error; end
 | 
				
			||||||
 | 
					  class RaceConditionError < Error; end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,7 +52,7 @@ class FeedManager
 | 
				
			||||||
    timeline_key = key(:home, into_account.id)
 | 
					    timeline_key = key(:home, into_account.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from_account.statuses.limit(MAX_ITEMS).each do |status|
 | 
					    from_account.statuses.limit(MAX_ITEMS).each do |status|
 | 
				
			||||||
      next if filter?(:home, status, into_account)
 | 
					      next if status.direct_visibility? || filter?(:home, status, into_account)
 | 
				
			||||||
      redis.zadd(timeline_key, status.id, status.id)
 | 
					      redis.zadd(timeline_key, status.id, status.id)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,17 +10,9 @@ class Feed
 | 
				
			||||||
    max_id     = '+inf' if max_id.blank?
 | 
					    max_id     = '+inf' if max_id.blank?
 | 
				
			||||||
    since_id   = '-inf' if since_id.blank?
 | 
					    since_id   = '-inf' if since_id.blank?
 | 
				
			||||||
    unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i)
 | 
					    unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i)
 | 
				
			||||||
 | 
					    status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # If we're after most recent items and none are there, we need to precompute the feed
 | 
					    unhydrated.map { |id| status_map[id] }.compact
 | 
				
			||||||
    if unhydrated.empty? && max_id == '+inf' && since_id == '-inf'
 | 
					 | 
				
			||||||
      RegenerationWorker.perform_async(@account.id, @type)
 | 
					 | 
				
			||||||
      @statuses = Status.send("as_#{@type}_timeline", @account).cache_ids.paginate_by_max_id(limit, nil, nil)
 | 
					 | 
				
			||||||
    else
 | 
					 | 
				
			||||||
      status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h
 | 
					 | 
				
			||||||
      @statuses  = unhydrated.map { |id| status_map[id] }.compact
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @statuses
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Import < ApplicationRecord
 | 
				
			||||||
 | 
					  self.inheritance_column = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  enum type: [:following, :blocking]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  belongs_to :account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  FILE_TYPES = ['text/plain', 'text/csv'].freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET']
 | 
				
			||||||
 | 
					  validates_attachment_content_type :data, content_type: FILE_TYPES
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@
 | 
				
			||||||
class Report < ApplicationRecord
 | 
					class Report < ApplicationRecord
 | 
				
			||||||
  belongs_to :account
 | 
					  belongs_to :account
 | 
				
			||||||
  belongs_to :target_account, class_name: 'Account'
 | 
					  belongs_to :target_account, class_name: 'Account'
 | 
				
			||||||
 | 
					  belongs_to :action_taken_by_account, class_name: 'Account'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  scope :unresolved, -> { where(action_taken: false) }
 | 
					  scope :unresolved, -> { where(action_taken: false) }
 | 
				
			||||||
  scope :resolved,   -> { where(action_taken: true) }
 | 
					  scope :resolved,   -> { where(action_taken: true) }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -188,7 +188,7 @@ class Status < ApplicationRecord
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  before_validation do
 | 
					  before_validation do
 | 
				
			||||||
    text.strip!
 | 
					    text&.strip!
 | 
				
			||||||
    spoiler_text&.strip!
 | 
					    spoiler_text&.strip!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    self.reply                  = !(in_reply_to_id.nil? && thread.nil?) unless reply
 | 
					    self.reply                  = !(in_reply_to_id.nil? && thread.nil?) unless reply
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,11 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BlockDomainService < BaseService
 | 
					class BlockDomainService < BaseService
 | 
				
			||||||
  def call(domain, severity)
 | 
					  def call(domain_block)
 | 
				
			||||||
    DomainBlock.where(domain: domain).first_or_create!(domain: domain, severity: severity)
 | 
					    if domain_block.silence?
 | 
				
			||||||
 | 
					      Account.where(domain: domain_block.domain).update_all(silenced: true)
 | 
				
			||||||
    if severity == :silence
 | 
					 | 
				
			||||||
      Account.where(domain: domain).update_all(silenced: true)
 | 
					 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      Account.where(domain: domain).find_each do |account|
 | 
					      Account.where(domain: domain_block.domain).find_each do |account|
 | 
				
			||||||
        account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed?
 | 
					        account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed?
 | 
				
			||||||
        SuspendAccountService.new.call(account)
 | 
					        SuspendAccountService.new.call(account)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,9 +4,15 @@ class FanOutOnWriteService < BaseService
 | 
				
			||||||
  # Push a status into home and mentions feeds
 | 
					  # Push a status into home and mentions feeds
 | 
				
			||||||
  # @param [Status] status
 | 
					  # @param [Status] status
 | 
				
			||||||
  def call(status)
 | 
					  def call(status)
 | 
				
			||||||
 | 
					    raise Mastodon::RaceConditionError if status.visibility.nil?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    deliver_to_self(status) if status.account.local?
 | 
					    deliver_to_self(status) if status.account.local?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    status.direct_visibility? ? deliver_to_mentioned_followers(status) : deliver_to_followers(status)
 | 
					    if status.direct_visibility?
 | 
				
			||||||
 | 
					      deliver_to_mentioned_followers(status)
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      deliver_to_followers(status)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return if status.account.silenced? || !status.public_visibility? || status.reblog?
 | 
					    return if status.account.silenced? || !status.public_visibility? || status.reblog?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,10 +4,10 @@ class PrecomputeFeedService < BaseService
 | 
				
			||||||
  # Fill up a user's home/mentions feed from DB and return a subset
 | 
					  # Fill up a user's home/mentions feed from DB and return a subset
 | 
				
			||||||
  # @param [Symbol] type :home or :mentions
 | 
					  # @param [Symbol] type :home or :mentions
 | 
				
			||||||
  # @param [Account] account
 | 
					  # @param [Account] account
 | 
				
			||||||
  def call(type, account)
 | 
					  def call(_, account)
 | 
				
			||||||
    Status.send("as_#{type}_timeline", account).limit(FeedManager::MAX_ITEMS).each do |status|
 | 
					    Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS).each do |status|
 | 
				
			||||||
      next if FeedManager.instance.filter?(type, status, account)
 | 
					      next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account)
 | 
				
			||||||
      redis.zadd(FeedManager.instance.key(type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
 | 
					      redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,10 +2,10 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SearchService < BaseService
 | 
					class SearchService < BaseService
 | 
				
			||||||
  def call(query, limit, resolve = false, account = nil)
 | 
					  def call(query, limit, resolve = false, account = nil)
 | 
				
			||||||
    return if query.blank?
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    results = { accounts: [], hashtags: [], statuses: [] }
 | 
					    results = { accounts: [], hashtags: [], statuses: [] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return results if query.blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if query =~ /\Ahttps?:\/\//
 | 
					    if query =~ /\Ahttps?:\/\//
 | 
				
			||||||
      resource = FetchRemoteResourceService.new.call(query)
 | 
					      resource = FetchRemoteResourceService.new.call(query)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,7 @@
 | 
				
			||||||
  .screenshot-with-signup
 | 
					  .screenshot-with-signup
 | 
				
			||||||
    .mascot= image_tag 'fluffy-elephant-friend.png'
 | 
					    .mascot= image_tag 'fluffy-elephant-friend.png'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    = simple_form_for(:user, url: user_registration_path) do |f|
 | 
					    = simple_form_for(@user, url: user_registration_path) do |f|
 | 
				
			||||||
      = f.simple_fields_for :account do |ff|
 | 
					      = f.simple_fields_for :account do |ff|
 | 
				
			||||||
        = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
 | 
					        = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,12 +23,12 @@
 | 
				
			||||||
      .counter{ class: active_nav_class(short_account_url(@account)) }
 | 
					      .counter{ class: active_nav_class(short_account_url(@account)) }
 | 
				
			||||||
        = link_to short_account_url(@account), class: 'u-url u-uid' do
 | 
					        = link_to short_account_url(@account), class: 'u-url u-uid' do
 | 
				
			||||||
          %span.counter-label= t('accounts.posts')
 | 
					          %span.counter-label= t('accounts.posts')
 | 
				
			||||||
          %span.counter-number= number_with_delimiter @account.statuses.count
 | 
					          %span.counter-number= number_with_delimiter @account.statuses_count
 | 
				
			||||||
      .counter{ class: active_nav_class(following_account_url(@account)) }
 | 
					      .counter{ class: active_nav_class(following_account_url(@account)) }
 | 
				
			||||||
        = link_to following_account_url(@account) do
 | 
					        = link_to following_account_url(@account) do
 | 
				
			||||||
          %span.counter-label= t('accounts.following')
 | 
					          %span.counter-label= t('accounts.following')
 | 
				
			||||||
          %span.counter-number= number_with_delimiter @account.following.count
 | 
					          %span.counter-number= number_with_delimiter @account.following_count
 | 
				
			||||||
      .counter{ class: active_nav_class(followers_account_url(@account)) }
 | 
					      .counter{ class: active_nav_class(followers_account_url(@account)) }
 | 
				
			||||||
        = link_to followers_account_url(@account) do
 | 
					        = link_to followers_account_url(@account) do
 | 
				
			||||||
          %span.counter-label= t('accounts.followers')
 | 
					          %span.counter-label= t('accounts.followers')
 | 
				
			||||||
          %span.counter-number= number_with_delimiter @account.followers.count
 | 
					          %span.counter-number= number_with_delimiter @account.followers_count
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,13 +47,13 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    %tr
 | 
					    %tr
 | 
				
			||||||
      %th Follows
 | 
					      %th Follows
 | 
				
			||||||
      %td= @account.following.count
 | 
					      %td= @account.following_count
 | 
				
			||||||
    %tr
 | 
					    %tr
 | 
				
			||||||
      %th Followers
 | 
					      %th Followers
 | 
				
			||||||
      %td= @account.followers.count
 | 
					      %td= @account.followers_count
 | 
				
			||||||
    %tr
 | 
					    %tr
 | 
				
			||||||
      %th Statuses
 | 
					      %th Statuses
 | 
				
			||||||
      %td= @account.statuses.count
 | 
					      %td= @account.statuses_count
 | 
				
			||||||
    %tr
 | 
					    %tr
 | 
				
			||||||
      %th Media attachments
 | 
					      %th Media attachments
 | 
				
			||||||
      %td
 | 
					      %td
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,3 +14,4 @@
 | 
				
			||||||
        %td= block.severity
 | 
					        %td= block.severity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
= will_paginate @blocks, pagination_options
 | 
					= will_paginate @blocks, pagination_options
 | 
				
			||||||
 | 
					= link_to 'Add new', new_admin_domain_block_path, class: 'button'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					- content_for :page_title do
 | 
				
			||||||
 | 
					  New domain block
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					= simple_form_for @domain_block, url: admin_domain_blocks_path do |f|
 | 
				
			||||||
 | 
					  = render 'shared/error_messages', object: @domain_block
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  %p.hint The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  = f.input :domain, placeholder: 'Domain'
 | 
				
			||||||
 | 
					  = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  %p.hint
 | 
				
			||||||
 | 
					    %strong Silence
 | 
				
			||||||
 | 
					    will make the account's posts invisible to anyone who isn't following them.
 | 
				
			||||||
 | 
					    %strong Suspend
 | 
				
			||||||
 | 
					    will remove all of the account's content, media, and profile data.
 | 
				
			||||||
 | 
					  .actions
 | 
				
			||||||
 | 
					    = f.button :button, 'Create block', type: :submit
 | 
				
			||||||
| 
						 | 
					@ -8,20 +8,25 @@
 | 
				
			||||||
      %li= filter_link_to 'Unresolved', action_taken: nil
 | 
					      %li= filter_link_to 'Unresolved', action_taken: nil
 | 
				
			||||||
      %li= filter_link_to 'Resolved', action_taken: '1'
 | 
					      %li= filter_link_to 'Resolved', action_taken: '1'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
%table.table
 | 
					= form_tag do
 | 
				
			||||||
  %thead
 | 
					
 | 
				
			||||||
    %tr
 | 
					  %table.table
 | 
				
			||||||
      %th ID
 | 
					    %thead
 | 
				
			||||||
      %th Target
 | 
					 | 
				
			||||||
      %th Reported by
 | 
					 | 
				
			||||||
      %th Comment
 | 
					 | 
				
			||||||
      %th
 | 
					 | 
				
			||||||
  %tbody
 | 
					 | 
				
			||||||
    - @reports.each do |report|
 | 
					 | 
				
			||||||
      %tr
 | 
					      %tr
 | 
				
			||||||
        %td= "##{report.id}"
 | 
					        %th
 | 
				
			||||||
        %td= link_to report.target_account.acct, admin_account_path(report.target_account.id)
 | 
					        %th ID
 | 
				
			||||||
        %td= link_to report.account.acct, admin_account_path(report.account.id)
 | 
					        %th Target
 | 
				
			||||||
        %td= truncate(report.comment, length: 30, separator: ' ')
 | 
					        %th Reported by
 | 
				
			||||||
        %td= table_link_to 'circle', 'View', admin_report_path(report)
 | 
					        %th Comment
 | 
				
			||||||
 | 
					        %th
 | 
				
			||||||
 | 
					    %tbody
 | 
				
			||||||
 | 
					      - @reports.each do |report|
 | 
				
			||||||
 | 
					        %tr
 | 
				
			||||||
 | 
					          %td= check_box_tag 'select', report.id
 | 
				
			||||||
 | 
					          %td= "##{report.id}"
 | 
				
			||||||
 | 
					          %td= link_to report.target_account.acct, admin_account_path(report.target_account.id)
 | 
				
			||||||
 | 
					          %td= link_to report.account.acct, admin_account_path(report.account.id)
 | 
				
			||||||
 | 
					          %td= truncate(report.comment, length: 30, separator: ' ')
 | 
				
			||||||
 | 
					          %td= table_link_to 'circle', 'View', admin_report_path(report)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
= will_paginate @reports, pagination_options
 | 
					= will_paginate @reports, pagination_options
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,7 @@
 | 
				
			||||||
        = link_to remove_admin_report_path(@report, status_id: status.id), method: :post, class: 'icon-button', style: 'font-size: 24px; width: 24px; height: 24px', title: 'Delete' do
 | 
					        = link_to remove_admin_report_path(@report, status_id: status.id), method: :post, class: 'icon-button', style: 'font-size: 24px; width: 24px; height: 24px', title: 'Delete' do
 | 
				
			||||||
          = fa_icon 'trash'
 | 
					          = fa_icon 'trash'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- unless @report.action_taken?
 | 
					- if !@report.action_taken?
 | 
				
			||||||
  %hr/
 | 
					  %hr/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  %div{ style: 'overflow: hidden' }
 | 
					  %div{ style: 'overflow: hidden' }
 | 
				
			||||||
| 
						 | 
					@ -36,3 +36,9 @@
 | 
				
			||||||
      = link_to 'Suspend account', suspend_admin_report_path(@report), method: :post, class: 'button'
 | 
					      = link_to 'Suspend account', suspend_admin_report_path(@report), method: :post, class: 'button'
 | 
				
			||||||
    %div{ style: 'float: left' }
 | 
					    %div{ style: 'float: left' }
 | 
				
			||||||
      = link_to 'Mark as resolved', resolve_admin_report_path(@report), method: :post, class: 'button'
 | 
					      = link_to 'Mark as resolved', resolve_admin_report_path(@report), method: :post, class: 'button'
 | 
				
			||||||
 | 
					- elsif !@report.action_taken_by_account.nil?
 | 
				
			||||||
 | 
					  %hr/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  %p
 | 
				
			||||||
 | 
					    %strong Action taken by:
 | 
				
			||||||
 | 
					    = @report.action_taken_by_account.acct
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,6 @@ node(:note)            { |account| Formatter.instance.simplified_format(account)
 | 
				
			||||||
node(:url)             { |account| TagManager.instance.url_for(account) }
 | 
					node(:url)             { |account| TagManager.instance.url_for(account) }
 | 
				
			||||||
node(:avatar)          { |account| full_asset_url(account.avatar.url(:original)) }
 | 
					node(:avatar)          { |account| full_asset_url(account.avatar.url(:original)) }
 | 
				
			||||||
node(:header)          { |account| full_asset_url(account.header.url(:original)) }
 | 
					node(:header)          { |account| full_asset_url(account.header.url(:original)) }
 | 
				
			||||||
node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : (account.try(:followers_count) || account.followers.count) }
 | 
					node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : account.followers_count }
 | 
				
			||||||
node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : (account.try(:following_count) || account.following.count) }
 | 
					node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : account.following_count }
 | 
				
			||||||
node(:statuses_count)  { |account| defined?(@statuses_counts_map)  ? (@statuses_counts_map[account.id]  || 0) : (account.try(:statuses_count)  || account.statuses.count) }
 | 
					node(:statuses_count)  { |account| defined?(@statuses_counts_map)  ? (@statuses_counts_map[account.id]  || 0) : account.statuses_count }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,8 +3,8 @@ attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, :sensitiv
 | 
				
			||||||
node(:uri)              { |status| TagManager.instance.uri_for(status) }
 | 
					node(:uri)              { |status| TagManager.instance.uri_for(status) }
 | 
				
			||||||
node(:content)          { |status| Formatter.instance.format(status) }
 | 
					node(:content)          { |status| Formatter.instance.format(status) }
 | 
				
			||||||
node(:url)              { |status| TagManager.instance.url_for(status) }
 | 
					node(:url)              { |status| TagManager.instance.url_for(status) }
 | 
				
			||||||
node(:reblogs_count)    { |status| defined?(@reblogs_counts_map)    ? (@reblogs_counts_map[status.id]    || 0) : (status.try(:reblogs_count) || status.reblogs.count) }
 | 
					node(:reblogs_count)    { |status| defined?(@reblogs_counts_map)    ? (@reblogs_counts_map[status.id]    || 0) : status.reblogs_count }
 | 
				
			||||||
node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : (status.try(:favourites_count) || status.favourites.count) }
 | 
					node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites_count }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
child :application do
 | 
					child :application do
 | 
				
			||||||
  extends 'api/v1/apps/show'
 | 
					  extends 'api/v1/apps/show'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,6 +12,15 @@
 | 
				
			||||||
    .content-wrapper
 | 
					    .content-wrapper
 | 
				
			||||||
      .content
 | 
					      .content
 | 
				
			||||||
        %h2= yield :page_title
 | 
					        %h2= yield :page_title
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        - if flash[:notice]
 | 
				
			||||||
 | 
					          .flash-message.notice
 | 
				
			||||||
 | 
					            %strong= flash[:notice]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        - if flash[:alert]
 | 
				
			||||||
 | 
					          .flash-message.alert
 | 
				
			||||||
 | 
					            %strong= flash[:alert]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        = yield
 | 
					        = yield
 | 
				
			||||||
 | 
					
 | 
				
			||||||
= render template: "layouts/application", locals: { body_classes: 'admin' }
 | 
					= render template: "layouts/application", locals: { body_classes: 'admin' }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					- content_for :page_title do
 | 
				
			||||||
 | 
					  = t('settings.import')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					%p.hint= t('imports.preface')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					= simple_form_for @import, url: settings_import_path do |f|
 | 
				
			||||||
 | 
					  = f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }
 | 
				
			||||||
 | 
					  = f.input :data, wrapper: :with_label, hint: t('simple_form.hints.imports.data')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .actions
 | 
				
			||||||
 | 
					    = f.button :button, t('imports.upload'), type: :submit
 | 
				
			||||||
| 
						 | 
					@ -9,8 +9,10 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .status__content.e-content.p-name.emojify<
 | 
					  .status__content.e-content.p-name.emojify<
 | 
				
			||||||
    - unless status.spoiler_text.blank?
 | 
					    - unless status.spoiler_text.blank?
 | 
				
			||||||
      %p= status.spoiler_text
 | 
					      %p{ style: 'margin-bottom: 0' }<
 | 
				
			||||||
    %div{ style: "direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)
 | 
					        %span>= "#{status.spoiler_text} "
 | 
				
			||||||
 | 
					        %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
 | 
				
			||||||
 | 
					    %div{ style: "display: #{status.spoiler_text.blank? ? 'block' : 'none'}; direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - unless status.media_attachments.empty?
 | 
					  - unless status.media_attachments.empty?
 | 
				
			||||||
    - if status.media_attachments.first.video?
 | 
					    - if status.media_attachments.first.video?
 | 
				
			||||||
| 
						 | 
					@ -39,11 +41,11 @@
 | 
				
			||||||
      ·
 | 
					      ·
 | 
				
			||||||
    %span<
 | 
					    %span<
 | 
				
			||||||
      = fa_icon('retweet')
 | 
					      = fa_icon('retweet')
 | 
				
			||||||
      %span= status.reblogs.count
 | 
					      %span= status.reblogs_count
 | 
				
			||||||
    ·
 | 
					    ·
 | 
				
			||||||
    %span<
 | 
					    %span<
 | 
				
			||||||
      = fa_icon('star')
 | 
					      = fa_icon('star')
 | 
				
			||||||
      %span= status.favourites.count
 | 
					      %span= status.favourites_count
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - if user_signed_in?
 | 
					    - if user_signed_in?
 | 
				
			||||||
      ·
 | 
					      ·
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,8 +14,10 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .status__content.e-content.p-name.emojify<
 | 
					  .status__content.e-content.p-name.emojify<
 | 
				
			||||||
    - unless status.spoiler_text.blank?
 | 
					    - unless status.spoiler_text.blank?
 | 
				
			||||||
      %p= status.spoiler_text
 | 
					      %p{ style: 'margin-bottom: 0' }<
 | 
				
			||||||
    %div{ style: "direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)
 | 
					        %span>= "#{status.spoiler_text} "
 | 
				
			||||||
 | 
					        %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
 | 
				
			||||||
 | 
					    %div{ style: "display: #{status.spoiler_text.blank? ? 'block' : 'none'}; direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - unless status.media_attachments.empty?
 | 
					  - unless status.media_attachments.empty?
 | 
				
			||||||
    .status__attachments
 | 
					    .status__attachments
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
class AfterRemoteFollowRequestWorker
 | 
					class AfterRemoteFollowRequestWorker
 | 
				
			||||||
  include Sidekiq::Worker
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  sidekiq_options retry: 5
 | 
					  sidekiq_options queue: 'pull', retry: 5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(follow_request_id)
 | 
					  def perform(follow_request_id)
 | 
				
			||||||
    follow_request  = FollowRequest.find(follow_request_id)
 | 
					    follow_request  = FollowRequest.find(follow_request_id)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
class AfterRemoteFollowWorker
 | 
					class AfterRemoteFollowWorker
 | 
				
			||||||
  include Sidekiq::Worker
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  sidekiq_options retry: 5
 | 
					  sidekiq_options queue: 'pull', retry: 5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(follow_id)
 | 
					  def perform(follow_id)
 | 
				
			||||||
    follow          = Follow.find(follow_id)
 | 
					    follow          = Follow.find(follow_id)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DomainBlockWorker
 | 
				
			||||||
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def perform(domain_block_id)
 | 
				
			||||||
 | 
					    BlockDomainService.new.call(DomainBlock.find(domain_block_id))
 | 
				
			||||||
 | 
					  rescue ActiveRecord::RecordNotFound
 | 
				
			||||||
 | 
					    true
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,54 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require 'csv'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ImportWorker
 | 
				
			||||||
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  sidekiq_options queue: 'pull', retry: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def perform(import_id)
 | 
				
			||||||
 | 
					    import = Import.find(import_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case import.type
 | 
				
			||||||
 | 
					    when 'blocking'
 | 
				
			||||||
 | 
					      process_blocks(import)
 | 
				
			||||||
 | 
					    when 'following'
 | 
				
			||||||
 | 
					      process_follows(import)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    import.destroy
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def process_blocks(import)
 | 
				
			||||||
 | 
					    from_account = import.account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    CSV.foreach(import.data.path) do |row|
 | 
				
			||||||
 | 
					      next if row.size != 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      begin
 | 
				
			||||||
 | 
					        target_account = FollowRemoteAccountService.new.call(row[0])
 | 
				
			||||||
 | 
					        next if target_account.nil?
 | 
				
			||||||
 | 
					        BlockService.new.call(from_account, target_account)
 | 
				
			||||||
 | 
					      rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
 | 
				
			||||||
 | 
					        next
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def process_follows(import)
 | 
				
			||||||
 | 
					    from_account = import.account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    CSV.foreach(import.data.path) do |row|
 | 
				
			||||||
 | 
					      next if row.size != 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      begin
 | 
				
			||||||
 | 
					        FollowService.new.call(from_account, row[0])
 | 
				
			||||||
 | 
					      rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
 | 
				
			||||||
 | 
					        next
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
class LinkCrawlWorker
 | 
					class LinkCrawlWorker
 | 
				
			||||||
  include Sidekiq::Worker
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  sidekiq_options retry: false
 | 
					  sidekiq_options queue: 'pull', retry: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(status_id)
 | 
					  def perform(status_id)
 | 
				
			||||||
    FetchLinkCardService.new.call(Status.find(status_id))
 | 
					    FetchLinkCardService.new.call(Status.find(status_id))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,8 @@
 | 
				
			||||||
class MergeWorker
 | 
					class MergeWorker
 | 
				
			||||||
  include Sidekiq::Worker
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  sidekiq_options queue: 'pull'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(from_account_id, into_account_id)
 | 
					  def perform(from_account_id, into_account_id)
 | 
				
			||||||
    FeedManager.instance.merge_into_timeline(Account.find(from_account_id), Account.find(into_account_id))
 | 
					    FeedManager.instance.merge_into_timeline(Account.find(from_account_id), Account.find(into_account_id))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
class NotificationWorker
 | 
					class NotificationWorker
 | 
				
			||||||
  include Sidekiq::Worker
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  sidekiq_options retry: 5
 | 
					  sidekiq_options queue: 'push', retry: 5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(xml, source_account_id, target_account_id)
 | 
					  def perform(xml, source_account_id, target_account_id)
 | 
				
			||||||
    SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id))
 | 
					    SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
class ProcessingWorker
 | 
					class ProcessingWorker
 | 
				
			||||||
  include Sidekiq::Worker
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  sidekiq_options backtrace: true
 | 
					  sidekiq_options queue: 'pull', backtrace: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(account_id, body)
 | 
					  def perform(account_id, body)
 | 
				
			||||||
    ProcessFeedService.new.call(body, Account.find(account_id))
 | 
					    ProcessFeedService.new.call(body, Account.find(account_id))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,9 @@
 | 
				
			||||||
class RegenerationWorker
 | 
					class RegenerationWorker
 | 
				
			||||||
  include Sidekiq::Worker
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(account_id, timeline_type)
 | 
					  sidekiq_options queue: 'pull', backtrace: true
 | 
				
			||||||
    PrecomputeFeedService.new.call(timeline_type, Account.find(account_id))
 | 
					
 | 
				
			||||||
 | 
					  def perform(account_id, _ = :home)
 | 
				
			||||||
 | 
					    PrecomputeFeedService.new.call(:home, Account.find(account_id))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
class SalmonWorker
 | 
					class SalmonWorker
 | 
				
			||||||
  include Sidekiq::Worker
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  sidekiq_options backtrace: true
 | 
					  sidekiq_options queue: 'pull', backtrace: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(account_id, body)
 | 
					  def perform(account_id, body)
 | 
				
			||||||
    ProcessInteractionService.new.call(body, Account.find(account_id))
 | 
					    ProcessInteractionService.new.call(body, Account.find(account_id))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
class ThreadResolveWorker
 | 
					class ThreadResolveWorker
 | 
				
			||||||
  include Sidekiq::Worker
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  sidekiq_options retry: false
 | 
					  sidekiq_options queue: 'pull', retry: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(child_status_id, parent_url)
 | 
					  def perform(child_status_id, parent_url)
 | 
				
			||||||
    child_status  = Status.find(child_status_id)
 | 
					    child_status  = Status.find(child_status_id)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,8 @@
 | 
				
			||||||
class UnmergeWorker
 | 
					class UnmergeWorker
 | 
				
			||||||
  include Sidekiq::Worker
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  sidekiq_options queue: 'pull'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(from_account_id, into_account_id)
 | 
					  def perform(from_account_id, into_account_id)
 | 
				
			||||||
    FeedManager.instance.unmerge_from_timeline(Account.find(from_account_id), Account.find(into_account_id))
 | 
					    FeedManager.instance.unmerge_from_timeline(Account.find(from_account_id), Account.find(into_account_id))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,7 @@ module Mastodon
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
 | 
					    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
 | 
				
			||||||
    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
 | 
					    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
 | 
				
			||||||
    config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN']
 | 
					    config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi]
 | 
				
			||||||
    config.i18n.default_locale    = :en
 | 
					    config.i18n.default_locale    = :en
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
 | 
					    # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,6 @@
 | 
				
			||||||
 | 
					Rack::Timeout::Logger.disable
 | 
				
			||||||
 | 
					Rack::Timeout.service_timeout = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if Rails.env.production?
 | 
					if Rails.env.production?
 | 
				
			||||||
  Rack::Timeout.service_timeout = 90
 | 
					  Rack::Timeout.service_timeout = 90
 | 
				
			||||||
  Rack::Timeout::Logger.disable
 | 
					 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,61 @@
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					fi:
 | 
				
			||||||
 | 
					  devise:
 | 
				
			||||||
 | 
					    confirmations:
 | 
				
			||||||
 | 
					      confirmed: Sähköpostisi on onnistuneesti vahvistettu.
 | 
				
			||||||
 | 
					      send_instructions: Saat kohta sähköpostiisi ohjeet kuinka voit aktivoida tilisi.
 | 
				
			||||||
 | 
					      send_paranoid_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet sen varmentamiseen.
 | 
				
			||||||
 | 
					    failure:
 | 
				
			||||||
 | 
					      already_authenticated: Olet jo kirjautunut sisään.
 | 
				
			||||||
 | 
					      inactive: Tiliäsi ei ole viellä aktivoitu.
 | 
				
			||||||
 | 
					      invalid: Virheellinen %{authentication_keys} tai salasana.
 | 
				
			||||||
 | 
					      last_attempt: Sinulla on yksi yritys jäljellä tai tili lukitaan.
 | 
				
			||||||
 | 
					      locked: Tili on lukittu.
 | 
				
			||||||
 | 
					      not_found_in_database: Virheellinen %{authentication_keys} tai salasana.
 | 
				
			||||||
 | 
					      timeout: Sessiosi on umpeutunut. Kirjaudu sisään jatkaaksesi.
 | 
				
			||||||
 | 
					      unauthenticated: Sinun tarvitsee kirjautua sisään tai rekisteröityä jatkaaksesi.
 | 
				
			||||||
 | 
					      unconfirmed: Sinun tarvitsee varmentaa sähköpostisi jatkaaksesi.
 | 
				
			||||||
 | 
					    mailer:
 | 
				
			||||||
 | 
					      confirmation_instructions:
 | 
				
			||||||
 | 
					        subject: 'Mastodon: Varmistus ohjeet'
 | 
				
			||||||
 | 
					      password_change:
 | 
				
			||||||
 | 
					        subject: 'Mastodon: Salasana vaihdettu'
 | 
				
			||||||
 | 
					      reset_password_instructions:
 | 
				
			||||||
 | 
					        subject: 'Mastodon: Salasanan vaihto ohjeet'
 | 
				
			||||||
 | 
					      unlock_instructions:
 | 
				
			||||||
 | 
					        subject: 'Mastodon: Avauksen ohjeet'
 | 
				
			||||||
 | 
					    omniauth_callbacks:
 | 
				
			||||||
 | 
					      failure: Varmennus %{kind} epäonnistui koska "%{reason}".
 | 
				
			||||||
 | 
					      success: Onnistuneesti varmennettu %{kind} tilillä.
 | 
				
			||||||
 | 
					    passwords:
 | 
				
			||||||
 | 
					      no_token: Et pääse tälle sivulle ilman salasanan vaihto sähköpostia. Jos tulet tämmöisestä postista, varmista että sinulla on täydellinen URL.
 | 
				
			||||||
 | 
					      send_instructions: Saat sähköpostitse ohjeet salasanan palautukseen muutaman minuutin kuluessa.
 | 
				
			||||||
 | 
					      send_paranoid_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet salasanan palautukseen.
 | 
				
			||||||
 | 
					      updated: Salasanasi vaihdettu onnistuneesti. Olet nyt kirjautunut sisään.
 | 
				
			||||||
 | 
					      updated_not_active: Salasanasi vaihdettu onnistuneesti.
 | 
				
			||||||
 | 
					    registrations:
 | 
				
			||||||
 | 
					      destroyed: Näkemiin! Tilisi on onnistuneesti peruttu. Toivottavasti näemme joskus uudestaan.
 | 
				
			||||||
 | 
					      signed_up: Tervetuloa! Rekisteröitymisesi onnistu.
 | 
				
			||||||
 | 
					      signed_up_but_inactive: Olet onnistuneesti rekisteröitynyt, mutta emme voi kirjata sinua sisään koska tiliäsi ei ole viellä aktivoitu.
 | 
				
			||||||
 | 
					      signed_up_but_locked: Olet onnistuneesti rekisteröitynyt, mutta emme voi kirjata sinua sisään koska tilisi on lukittu.
 | 
				
			||||||
 | 
					      signed_up_but_unconfirmed: Varmistuslinkki on lähetty sähköpostiisi. Seuraa sitä jotta tilisi voidaan aktivoida.
 | 
				
			||||||
 | 
					      update_needs_confirmation: Tilisi on onnistuneesti päivitetty, mutta meidän tarvitsee vahvistaa sinun uusi sähköpostisi. Tarkista sähköpostisi ja seuraa viestissä tullutta linkkiä varmistaaksesi uuden osoitteen..
 | 
				
			||||||
 | 
					      updated: Tilisi on onnistuneesti päivitetty.
 | 
				
			||||||
 | 
					    sessions:
 | 
				
			||||||
 | 
					      already_signed_out: Ulos kirjautuminen onnistui.
 | 
				
			||||||
 | 
					      signed_in: Sisäänkirjautuminen onnistui.
 | 
				
			||||||
 | 
					      signed_out: Ulos kirjautuminen onnistui.
 | 
				
			||||||
 | 
					    unlocks:
 | 
				
			||||||
 | 
					      send_instructions: Saat sähköpostiisi pian ohjeet, jolla voit avata tilisi uudestaan.
 | 
				
			||||||
 | 
					      send_paranoid_instructions: Jos tilisi on olemassa, saat sähköpostiisi pian ohjeet tilisi avaamiseen.
 | 
				
			||||||
 | 
					      unlocked: Tilisi on avattu onnistuneesti. Kirjaudu normaalisti sisään.
 | 
				
			||||||
 | 
					  errors:
 | 
				
			||||||
 | 
					    messages:
 | 
				
			||||||
 | 
					      already_confirmed: on jo varmistettu. Yritä kirjautua sisään
 | 
				
			||||||
 | 
					      confirmation_period_expired: pitää varmistaa %{period} sisällä, ole hyvä ja pyydä uusi
 | 
				
			||||||
 | 
					      expired: on erääntynyt, ole hyvä ja pyydä uusi
 | 
				
			||||||
 | 
					      not_found: ei löydy
 | 
				
			||||||
 | 
					      not_locked: ei ollut lukittu
 | 
				
			||||||
 | 
					      not_saved:
 | 
				
			||||||
 | 
					        one: '1 virhe esti %{resource} tallennuksen:'
 | 
				
			||||||
 | 
					        other: "%{count} virhettä esti %{resource} tallennuksen:"
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue