about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--Gemfile2
-rw-r--r--app/assets/javascripts/components/actions/accounts.jsx11
-rw-r--r--app/assets/javascripts/components/actions/timelines.jsx22
-rw-r--r--app/assets/javascripts/components/components/video_player.jsx4
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx13
-rw-r--r--app/assets/javascripts/components/features/community_timeline/index.jsx16
-rw-r--r--app/assets/javascripts/components/features/compose/index.jsx2
-rw-r--r--app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx4
-rw-r--r--app/assets/javascripts/components/features/public_timeline/index.jsx16
-rw-r--r--app/assets/javascripts/components/reducers/timelines.jsx11
-rw-r--r--app/assets/javascripts/components/selectors/index.jsx2
-rw-r--r--app/assets/stylesheets/accounts.scss1
-rw-r--r--app/assets/stylesheets/components.scss1
-rw-r--r--app/controllers/admin/reports_controller.rb2
-rw-r--r--app/lib/feed_manager.rb2
-rw-r--r--app/services/fan_out_on_write_service.rb6
-rw-r--r--app/services/precompute_feed_service.rb8
-rw-r--r--config/initializers/timeout.rb4
-rw-r--r--docs/Using-Mastodon/List-of-Mastodon-instances.md35
-rw-r--r--streaming/index.js7
21 files changed, 131 insertions, 40 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4210d1867..bfc771ab9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -13,7 +13,7 @@ Below are the guidelines for working on pull requests:
 
 ## General
 
-- 2 spaces indendation
+- 2 spaces indentation
 
 ## Documentation
 
diff --git a/Gemfile b/Gemfile
index 440f2e87b..764010d5d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -50,6 +50,7 @@ gem 'rails-settings-cached'
 gem 'simple-navigation'
 gem 'statsd-instrument'
 gem 'ruby-oembed', require: 'oembed'
+gem 'rack-timeout'
 
 gem 'react-rails'
 gem 'browserify-rails'
@@ -89,5 +90,4 @@ group :production do
   gem 'rails_12factor'
   gem 'redis-rails'
   gem 'lograge'
-  gem 'rack-timeout'
 end
diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx
index 05fa8e68d..37ebb9969 100644
--- a/app/assets/javascripts/components/actions/accounts.jsx
+++ b/app/assets/javascripts/components/actions/accounts.jsx
@@ -579,15 +579,18 @@ export function expandFollowingFail(id, error) {
   };
 };
 
-export function fetchRelationships(account_ids) {
+export function fetchRelationships(accountIds) {
   return (dispatch, getState) => {
-    if (account_ids.length === 0) {
+    const loadedRelationships = getState().get('relationships');
+    const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
+
+    if (newAccountIds.length === 0) {
       return;
     }
 
-    dispatch(fetchRelationshipsRequest(account_ids));
+    dispatch(fetchRelationshipsRequest(newAccountIds));
 
-    api(getState).get(`/api/v1/accounts/relationships?${account_ids.map(id => `id[]=${id}`).join('&')}`).then(response => {
+    api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
       dispatch(fetchRelationshipsSuccess(response.data));
     }).catch(error => {
       dispatch(fetchRelationshipsFail(error));
diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx
index 3e2d4ff43..6cd1f04b3 100644
--- a/app/assets/javascripts/components/actions/timelines.jsx
+++ b/app/assets/javascripts/components/actions/timelines.jsx
@@ -14,6 +14,9 @@ export const TIMELINE_EXPAND_FAIL    = 'TIMELINE_EXPAND_FAIL';
 
 export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
 
+export const TIMELINE_CONNECT    = 'TIMELINE_CONNECT';
+export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
+
 export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) {
   return {
     type: TIMELINE_REFRESH_SUCCESS,
@@ -76,6 +79,11 @@ export function refreshTimeline(timeline, id = null) {
     let skipLoading = false;
 
     if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) {
+      if (id === null && getState().getIn(['timelines', timeline, 'online'])) {
+        // Skip refreshing when timeline is live anyway
+        return;
+      }
+
       params          = { ...params, since_id: newestId };
       skipLoading     = true;
     }
@@ -162,3 +170,17 @@ export function scrollTopTimeline(timeline, top) {
     top
   };
 };
+
+export function connectTimeline(timeline) {
+  return {
+    type: TIMELINE_CONNECT,
+    timeline
+  };
+};
+
+export function disconnectTimeline(timeline) {
+  return {
+    type: TIMELINE_DISCONNECT,
+    timeline
+  };
+};
diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx
index 1d41c14d4..ab21ca9cd 100644
--- a/app/assets/javascripts/components/components/video_player.jsx
+++ b/app/assets/javascripts/components/components/video_player.jsx
@@ -24,7 +24,7 @@ const muteStyle = {
   top: '10px',
   right: '10px',
   color: 'white',
-  boxShadow: '1px 1px 1px #000',
+  textShadow: "0px 1px 1px black, 1px 0px 1px black",
   opacity: '0.8',
   zIndex: '5'
 };
@@ -57,7 +57,7 @@ const spoilerButtonStyle = {
   top: '6px',
   left: '8px',
   color: 'white',
-  boxShadow: '1px 1px 1px #000',
+  textShadow: "0px 1px 1px black, 1px 0px 1px black",
   zIndex: '100'
 };
 
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index 40fbac525..6dc08bb4c 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -4,7 +4,9 @@ import {
   refreshTimelineSuccess,
   updateTimeline,
   deleteFromTimelines,
-  refreshTimeline
+  refreshTimeline,
+  connectTimeline,
+  disconnectTimeline
 } from '../actions/timelines';
 import { updateNotifications, refreshNotifications } from '../actions/notifications';
 import createBrowserHistory from 'history/lib/createBrowserHistory';
@@ -70,6 +72,14 @@ const Mastodon = React.createClass({
 
     this.subscription = createStream(accessToken, 'user', {
 
+      connected () {
+        store.dispatch(connectTimeline('home'));
+      },
+
+      disconnected () {
+        store.dispatch(disconnectTimeline('home'));
+      },
+
       received (data) {
         switch(data.event) {
         case 'update':
@@ -85,6 +95,7 @@ const Mastodon = React.createClass({
       },
 
       reconnected () {
+        store.dispatch(connectTimeline('home'));
         store.dispatch(refreshTimeline('home'));
         store.dispatch(refreshNotifications());
       }
diff --git a/app/assets/javascripts/components/features/community_timeline/index.jsx b/app/assets/javascripts/components/features/community_timeline/index.jsx
index 2cfd7b2fe..0957338cf 100644
--- a/app/assets/javascripts/components/features/community_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/community_timeline/index.jsx
@@ -5,7 +5,9 @@ import Column from '../ui/components/column';
 import {
   refreshTimeline,
   updateTimeline,
-  deleteFromTimelines
+  deleteFromTimelines,
+  connectTimeline,
+  disconnectTimeline
 } from '../../actions/timelines';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnBackButtonSlim from '../../components/column_back_button_slim';
@@ -44,6 +46,18 @@ const CommunityTimeline = React.createClass({
 
     subscription = createStream(accessToken, 'public:local', {
 
+      connected () {
+        dispatch(connectTimeline('community'));
+      },
+
+      reconnected () {
+        dispatch(connectTimeline('community'));
+      },
+
+      disconnected () {
+        dispatch(disconnectTimeline('community'));
+      },
+
       received (data) {
         switch(data.event) {
         case 'update':
diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx
index d4df259dc..9421de3ff 100644
--- a/app/assets/javascripts/components/features/compose/index.jsx
+++ b/app/assets/javascripts/components/features/compose/index.jsx
@@ -72,7 +72,7 @@ const Compose = React.createClass({
 
           <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
             {({ x }) =>
-              <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)` }}>
+              <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
                 <SearchResultsContainer />
               </div>
             }
diff --git a/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx
index 3317210bf..92e700874 100644
--- a/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx
+++ b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx
@@ -6,7 +6,7 @@ import SettingToggle from '../../notifications/components/setting_toggle';
 import SettingText from './setting_text';
 
 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 = {
@@ -44,7 +44,7 @@ const ColumnSettings = React.createClass({
           <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
 
           <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 style={rowStyle}>
diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx
index b2342abbd..6d766a83b 100644
--- a/app/assets/javascripts/components/features/public_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/public_timeline/index.jsx
@@ -5,7 +5,9 @@ import Column from '../ui/components/column';
 import {
   refreshTimeline,
   updateTimeline,
-  deleteFromTimelines
+  deleteFromTimelines,
+  connectTimeline,
+  disconnectTimeline
 } from '../../actions/timelines';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnBackButtonSlim from '../../components/column_back_button_slim';
@@ -44,6 +46,18 @@ const PublicTimeline = React.createClass({
 
     subscription = createStream(accessToken, 'public', {
 
+      connected () {
+        dispatch(connectTimeline('public'));
+      },
+
+      reconnected () {
+        dispatch(connectTimeline('public'));
+      },
+
+      disconnected () {
+        dispatch(disconnectTimeline('public'));
+      },
+
       received (data) {
         switch(data.event) {
         case 'update':
diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx
index c67d05423..675a52759 100644
--- a/app/assets/javascripts/components/reducers/timelines.jsx
+++ b/app/assets/javascripts/components/reducers/timelines.jsx
@@ -7,7 +7,9 @@ import {
   TIMELINE_EXPAND_SUCCESS,
   TIMELINE_EXPAND_REQUEST,
   TIMELINE_EXPAND_FAIL,
-  TIMELINE_SCROLL_TOP
+  TIMELINE_SCROLL_TOP,
+  TIMELINE_CONNECT,
+  TIMELINE_DISCONNECT
 } from '../actions/timelines';
 import {
   REBLOG_SUCCESS,
@@ -35,6 +37,7 @@ const initialState = Immutable.Map({
     path: () => '/api/v1/timelines/home',
     next: null,
     isLoading: false,
+    online: false,
     loaded: false,
     top: true,
     unread: 0,
@@ -45,6 +48,7 @@ const initialState = Immutable.Map({
     path: () => '/api/v1/timelines/public',
     next: null,
     isLoading: false,
+    online: false,
     loaded: false,
     top: true,
     unread: 0,
@@ -56,6 +60,7 @@ const initialState = Immutable.Map({
     next: null,
     params: { local: true },
     isLoading: false,
+    online: false,
     loaded: false,
     top: true,
     unread: 0,
@@ -300,6 +305,10 @@ export default function timelines(state = initialState, action) {
     return filterTimelines(state, action.relationship, action.statuses);
   case TIMELINE_SCROLL_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:
     return state;
   }
diff --git a/app/assets/javascripts/components/selectors/index.jsx b/app/assets/javascripts/components/selectors/index.jsx
index 0e88654a1..01a6cb264 100644
--- a/app/assets/javascripts/components/selectors/index.jsx
+++ b/app/assets/javascripts/components/selectors/index.jsx
@@ -5,7 +5,7 @@ const getStatuses = state => state.get('statuses');
 const getAccounts = state => state.get('accounts');
 
 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 = () => {
   return createSelector([getAccountBase, getAccountRelationship], (base, relationship) => {
diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss
index 7c48c91f3..25e24a95a 100644
--- a/app/assets/stylesheets/accounts.scss
+++ b/app/assets/stylesheets/accounts.scss
@@ -311,6 +311,7 @@
       padding: 10px;
       padding-top: 15px;
       color: $color3;
+      word-wrap: break-word;
     }
   }
 }
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index d87e09453..f8003e5fd 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -424,6 +424,7 @@ a.status__content__spoiler-link {
 
 .account__header__content {
   word-wrap: break-word;
+  word-break: normal;
   font-weight: 400;
   overflow: hidden;
   color: $color3;
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index 67d57e4eb..0117a18ee 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -7,7 +7,7 @@ class Admin::ReportsController < ApplicationController
   layout 'admin'
 
   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
   end
 
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index b0dda1256..cd6ca1291 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -52,7 +52,7 @@ class FeedManager
     timeline_key = key(:home, into_account.id)
 
     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)
     end
 
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 0cacfd7cd..402b84b2f 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -6,7 +6,11 @@ class FanOutOnWriteService < BaseService
   def call(status)
     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?
 
diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb
index 54d11b631..e1ec56e8d 100644
--- a/app/services/precompute_feed_service.rb
+++ b/app/services/precompute_feed_service.rb
@@ -4,10 +4,10 @@ class PrecomputeFeedService < BaseService
   # Fill up a user's home/mentions feed from DB and return a subset
   # @param [Symbol] type :home or :mentions
   # @param [Account] account
-  def call(type, account)
-    Status.send("as_#{type}_timeline", account).limit(FeedManager::MAX_ITEMS).each do |status|
-      next if FeedManager.instance.filter?(type, status, account)
-      redis.zadd(FeedManager.instance.key(type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
+  def call(_, account)
+    Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS).each do |status|
+      next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account)
+      redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
     end
   end
 
diff --git a/config/initializers/timeout.rb b/config/initializers/timeout.rb
index 06a29492e..de87fd906 100644
--- a/config/initializers/timeout.rb
+++ b/config/initializers/timeout.rb
@@ -1,4 +1,6 @@
+Rack::Timeout::Logger.disable
+Rack::Timeout.service_timeout = false
+
 if Rails.env.production?
   Rack::Timeout.service_timeout = 90
-  Rack::Timeout::Logger.disable
 end
diff --git a/docs/Using-Mastodon/List-of-Mastodon-instances.md b/docs/Using-Mastodon/List-of-Mastodon-instances.md
index cc10e7b7b..2f307b8a0 100644
--- a/docs/Using-Mastodon/List-of-Mastodon-instances.md
+++ b/docs/Using-Mastodon/List-of-Mastodon-instances.md
@@ -1,21 +1,24 @@
 List of Known Mastodon instances
 ==========================
 
-| Name | Theme/Notes, if applicable | Open Registrations |
-| -------------|-------------|---|
-| [mastodon.social](https://mastodon.social) |Flagship, quick updates|Yes|
-| [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|
-| [social.tchncs.de](https://social.tchncs.de)|N/A|Yes|
-| [animalliberation.social](https://animalliberation.social) |Animal Rights|Yes|
-| [socially.constructed.space](https://socially.constructed.space) |Single user|No|
-| [epiktistes.com](https://epiktistes.com) |N/A|Yes|
-| [on.vu](https://on.vu) | Appears defunct|No|
-| [gay.crime.team](https://gay.crime.team) |N/A|Yes(?)|
-| [icosahedron.website](https://icosahedron.website/) |Icosahedron-themed (well, visually), open registration.|Yes|
-| [memetastic.space](https://memetastic.space) |Memes|Yes|
-| [social.diskseven.com](https://social.diskseven.com) |Single user|No|
-| [social.gestaltzerfall.net](https://social.gestaltzerfall.net) |Single user|No|
-| [mastodon.xyz](https://mastodon.xyz) |N/A|Yes|
-| [social.targaryen.house](https://social.targaryen.house) |N/A|Yes|
+There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz) showing realtime information about instances.
+
+| Name | Theme/Notes, if applicable | Open Registrations | IPv6 |
+| -------------|-------------|---|---|
+| [mastodon.social](https://mastodon.social) |Flagship, quick updates|Yes|No|
+| [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|No|
+| [social.tchncs.de](https://social.tchncs.de)|N/A|Yes|No|
+| [animalliberation.social](https://animalliberation.social) |Animal Rights|Yes|No|
+| [socially.constructed.space](https://socially.constructed.space) |Single user|No|No|
+| [epiktistes.com](https://epiktistes.com) |N/A|Yes|No|
+| [gay.crime.team](https://gay.crime.team) |the place for doin' gay crime online (please don't actually do crime here)|Yes|No|
+| [icosahedron.website](https://icosahedron.website/) |Icosahedron-themed (well, visually), open registration.|Yes|No|
+| [memetastic.space](https://memetastic.space) |Memes|Yes|No|
+| [social.diskseven.com](https://social.diskseven.com) |Single user|No|No (DNS entry but no response)|
+| [social.gestaltzerfall.net](https://social.gestaltzerfall.net) |Single user|No|No|
+| [mastodon.xyz](https://mastodon.xyz) |N/A|Yes|Yes|
+| [social.targaryen.house](https://social.targaryen.house) |N/A|Yes|No|
+| [social.mashek.net](https://social.mashek.net) |Themed and customised for Mashekstein Labs community. Selectively federates.|Yes|No|
+| [masto.themimitoof.fr](https://masto.themimitoof.fr) |N/A|Yes|Yes|
 
 Let me know if you start running one so I can add it to the list! (Alternatively, add it yourself as a pull request).
diff --git a/streaming/index.js b/streaming/index.js
index 0f838e411..7edf6203f 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -215,8 +215,11 @@ const streamHttpEnd = req => (id, listener) => {
 
 // Setup stream output to WebSockets
 const streamToWs = (req, ws) => {
+  const heartbeat = setInterval(() => ws.ping(), 15000)
+
   ws.on('close', () => {
     log.verbose(req.requestId, `Ending stream for ${req.accountId}`)
+    clearInterval(heartbeat)
   })
 
   return (event, payload) => {
@@ -234,6 +237,10 @@ const streamWsEnd = ws => (id, listener) => {
   ws.on('close', () => {
     unsubscribe(id, listener)
   })
+
+  ws.on('error', e => {
+    unsubscribe(id, listener)
+  })
 }
 
 app.use(setRequestId)