about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2017-01-09 14:00:55 +0100
committerEugen Rochko <eugen@zeonfederated.com>2017-01-09 14:00:55 +0100
commit75f80bef107cfe9e9c0e6ba3dc51ef86c89e40cc (patch)
treed507368baac4e85d417af4c64ecc0d9c43a65963
parent23ebf60b95984764992c4b356048786ed0ab2953 (diff)
Persist UI settings, add missing localizations for German
-rw-r--r--app/assets/javascripts/components/actions/notifications.jsx10
-rw-r--r--app/assets/javascripts/components/actions/settings.jsx17
-rw-r--r--app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx6
-rw-r--r--app/assets/javascripts/components/features/notifications/index.jsx2
-rw-r--r--app/assets/javascripts/components/locales/de.jsx18
-rw-r--r--app/assets/javascripts/components/locales/en.jsx2
-rw-r--r--app/assets/javascripts/components/reducers/index.jsx4
-rw-r--r--app/assets/javascripts/components/reducers/notifications.jsx41
-rw-r--r--app/assets/javascripts/components/reducers/settings.jsx32
-rw-r--r--app/controllers/api/web/settings_controller.rb15
-rw-r--r--app/controllers/home_controller.rb1
-rw-r--r--app/models/account.rb4
-rw-r--r--app/models/web.rb5
-rw-r--r--app/models/web/setting.rb7
-rw-r--r--app/views/home/index.html.haml17
-rw-r--r--app/views/home/initial_state.json.rabl24
-rw-r--r--config/locales/de.yml33
-rw-r--r--config/locales/simple_form.de.yml5
-rw-r--r--config/routes.rb4
-rw-r--r--db/migrate/20170109120109_create_web_settings.rb12
-rw-r--r--db/schema.rb79
-rw-r--r--spec/fabricators/media_attachment_fabricator.rb1
-rw-r--r--spec/fabricators/web_setting_fabricator.rb3
-rw-r--r--spec/models/account_spec.rb25
-rw-r--r--spec/models/web/setting_spec.rb5
25 files changed, 305 insertions, 67 deletions
diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx
index 182b598aa..1e5b2c382 100644
--- a/app/assets/javascripts/components/actions/notifications.jsx
+++ b/app/assets/javascripts/components/actions/notifications.jsx
@@ -14,8 +14,6 @@ export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
 export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
 export const NOTIFICATIONS_EXPAND_FAIL    = 'NOTIFICATIONS_EXPAND_FAIL';
 
-export const NOTIFICATIONS_SETTING_CHANGE = 'NOTIFICATIONS_SETTING_CHANGE';
-
 const fetchRelatedRelationships = (dispatch, notifications) => {
   const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
 
@@ -133,11 +131,3 @@ export function expandNotificationsFail(error) {
     error
   };
 };
-
-export function changeNotificationsSetting(key, checked) {
-  return {
-    type: NOTIFICATIONS_SETTING_CHANGE,
-    key,
-    checked
-  };
-};
diff --git a/app/assets/javascripts/components/actions/settings.jsx b/app/assets/javascripts/components/actions/settings.jsx
new file mode 100644
index 000000000..0a6fb7cdb
--- /dev/null
+++ b/app/assets/javascripts/components/actions/settings.jsx
@@ -0,0 +1,17 @@
+import axios from 'axios';
+
+export const SETTING_CHANGE = 'SETTING_CHANGE';
+
+export function changeSetting(key, value) {
+  return (dispatch, getState) => {
+    dispatch({
+      type: SETTING_CHANGE,
+      key,
+      value
+    });
+
+    axios.put('/api/web/settings', {
+      data: getState().get('settings').toJS()
+    });
+  };
+};
diff --git a/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx b/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx
index 6907fd351..5792e97e3 100644
--- a/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx
+++ b/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx
@@ -1,15 +1,15 @@
 import { connect } from 'react-redux';
 import ColumnSettings from '../components/column_settings';
-import { changeNotificationsSetting } from '../../../actions/notifications';
+import { changeSetting } from '../../../actions/settings';
 
 const mapStateToProps = state => ({
-  settings: state.getIn(['notifications', 'settings'])
+  settings: state.getIn(['settings', 'notifications'])
 });
 
 const mapDispatchToProps = dispatch => ({
 
   onChange (key, checked) {
-    dispatch(changeNotificationsSetting(key, checked));
+    dispatch(changeSetting(['notifications', ...key], checked));
   }
 
 });
diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx
index 7e706ad6a..29be491eb 100644
--- a/app/assets/javascripts/components/features/notifications/index.jsx
+++ b/app/assets/javascripts/components/features/notifications/index.jsx
@@ -18,7 +18,7 @@ const messages = defineMessages({
 });
 
 const getNotifications = createSelector([
-  state => Immutable.List(state.getIn(['notifications', 'settings', 'shows']).filter(item => !item).keys()),
+  state => Immutable.List(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
   state => state.getIn(['notifications', 'items'])
 ], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type'))));
 
diff --git a/app/assets/javascripts/components/locales/de.jsx b/app/assets/javascripts/components/locales/de.jsx
index 97df67480..c37a71c21 100644
--- a/app/assets/javascripts/components/locales/de.jsx
+++ b/app/assets/javascripts/components/locales/de.jsx
@@ -8,6 +8,9 @@ const en = {
   "status.reblog": "Teilen",
   "status.favourite": "Favorisieren",
   "status.reblogged_by": "{name} teilte",
+  "status.sensitive_warning": "Sensible Inhalte",
+  "status.sensitive_toggle": "Klicken um zu zeigen",
+  "status.open": "Öffnen",
   "video_player.toggle_sound": "Ton umschalten",
   "account.mention": "Erwähnen",
   "account.edit_profile": "Profil bearbeiten",
@@ -19,14 +22,17 @@ const en = {
   "account.follows": "Folgt",
   "account.followers": "Folger",
   "account.follows_you": "Folgt dir",
+  "account.requested": "Warte auf Erlaubnis",
   "getting_started.heading": "Erste Schritte",
   "getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben an der Seite eingibst.",
   "getting_started.about_shortcuts": "Falls der Zielnutzer an derselben Domain ist wie du, funktioniert der Nutzername auch alleine. Das gilt auch für Erwähnungen in Beiträgen.",
   "getting_started.about_developer": "Der Entwickler des Projekts kann unter Gargron@mastodon.social gefunden werden",
+  "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
   "column.home": "Home",
   "column.mentions": "Erwähnungen",
   "column.public": "Gesamtes Bekanntes Netz",
   "column.notifications": "Mitteilungen",
+  "column.follow_requests": "Folgeanfragen",
   "tabs_bar.compose": "Schreiben",
   "tabs_bar.home": "Home",
   "tabs_bar.mentions": "Erwähnungen",
@@ -36,10 +42,12 @@ const en = {
   "compose_form.publish": "Veröffentlichen",
   "compose_form.sensitive": "Medien als sensitiv markieren",
   "compose_form.unlisted": "Öffentlich nicht auflisten",
+  "compose_form.private": "Als privat markieren",
   "navigation_bar.edit_profile": "Profil bearbeiten",
   "navigation_bar.preferences": "Einstellungen",
   "navigation_bar.public_timeline": "Öffentlich",
   "navigation_bar.logout": "Abmelden",
+  "navigation_bar.follow_requests": "Folgeanfragen",
   "reply_indicator.cancel": "Abbrechen",
   "search.placeholder": "Suche",
   "search.account": "Konto",
@@ -49,7 +57,15 @@ const en = {
   "notification.follow": "{name} folgt dir",
   "notification.favourite": "{name} favorisierte deinen Status",
   "notification.reblog": "{name} teilte deinen Status",
-  "notification.mention": "{name} erwähnte dich"
+  "notification.mention": "{name} erwähnte dich",
+  "notifications.column_settings.alert": "Desktop-Benachrichtigunen",
+  "notifications.column_settings.show": "In der Spalte anzeigen",
+  "notifications.column_settings.follow": "Neue Folger:",
+  "notifications.column_settings.favourite": "Favorisierungen:",
+  "notifications.column_settings.mention": "Erwähnungen:",
+  "notifications.column_settings.reblog": "Geteilte Beiträge:",
+  "follow_request.authorize": "Erlauben",
+  "follow_request.reject": "Ablehnen"
 };
 
 export default en;
diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx
index b166c48ba..92dcbaeb9 100644
--- a/app/assets/javascripts/components/locales/en.jsx
+++ b/app/assets/javascripts/components/locales/en.jsx
@@ -17,7 +17,6 @@ const en = {
   "account.unfollow": "Unfollow",
   "account.block": "Block",
   "account.follow": "Follow",
-  "account.block": "Block",
   "account.posts": "Posts",
   "account.follows": "Follows",
   "account.followers": "Followers",
@@ -41,6 +40,7 @@ const en = {
   "compose_form.publish": "Toot",
   "compose_form.sensitive": "Mark media as sensitive",
   "compose_form.private": "Mark as private",
+  "compose_form.unlisted": "Do not display in public timeline",
   "navigation_bar.edit_profile": "Edit profile",
   "navigation_bar.preferences": "Preferences",
   "navigation_bar.public_timeline": "Public timeline",
diff --git a/app/assets/javascripts/components/reducers/index.jsx b/app/assets/javascripts/components/reducers/index.jsx
index aea9239f8..068491949 100644
--- a/app/assets/javascripts/components/reducers/index.jsx
+++ b/app/assets/javascripts/components/reducers/index.jsx
@@ -11,6 +11,7 @@ import statuses from './statuses';
 import relationships from './relationships';
 import search from './search';
 import notifications from './notifications';
+import settings from './settings';
 
 export default combineReducers({
   timelines,
@@ -24,5 +25,6 @@ export default combineReducers({
   statuses,
   relationships,
   search,
-  notifications
+  notifications,
+  settings
 });
diff --git a/app/assets/javascripts/components/reducers/notifications.jsx b/app/assets/javascripts/components/reducers/notifications.jsx
index e0d1ccf83..c85e7b460 100644
--- a/app/assets/javascripts/components/reducers/notifications.jsx
+++ b/app/assets/javascripts/components/reducers/notifications.jsx
@@ -2,7 +2,6 @@ import {
   NOTIFICATIONS_UPDATE,
   NOTIFICATIONS_REFRESH_SUCCESS,
   NOTIFICATIONS_EXPAND_SUCCESS,
-  NOTIFICATIONS_SETTING_CHANGE
 } from '../actions/notifications';
 import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts';
 import Immutable from 'immutable';
@@ -10,23 +9,7 @@ import Immutable from 'immutable';
 const initialState = Immutable.Map({
   items: Immutable.List(),
   next: null,
-  loaded: false,
-
-  settings: Immutable.Map({
-    alerts: Immutable.Map({
-      follow: true,
-      favourite: true,
-      reblog: true,
-      mention: true
-    }),
-
-    shows: Immutable.Map({
-      follow: true,
-      favourite: true,
-      reblog: true,
-      mention: true
-    })
-  })
+  loaded: false
 });
 
 const notificationToMap = notification => Immutable.Map({
@@ -67,17 +50,15 @@ const filterNotifications = (state, relationship) => {
 
 export default function notifications(state = initialState, action) {
   switch(action.type) {
-    case NOTIFICATIONS_UPDATE:
-      return normalizeNotification(state, action.notification);
-    case NOTIFICATIONS_REFRESH_SUCCESS:
-      return normalizeNotifications(state, action.notifications, action.next);
-    case NOTIFICATIONS_EXPAND_SUCCESS:
-      return appendNormalizedNotifications(state, action.notifications, action.next);
-    case ACCOUNT_BLOCK_SUCCESS:
-      return filterNotifications(state, action.relationship);
-    case NOTIFICATIONS_SETTING_CHANGE:
-      return state.setIn(['settings', ...action.key], action.checked);
-    default:
-      return state;
+  case NOTIFICATIONS_UPDATE:
+    return normalizeNotification(state, action.notification);
+  case NOTIFICATIONS_REFRESH_SUCCESS:
+    return normalizeNotifications(state, action.notifications, action.next);
+  case NOTIFICATIONS_EXPAND_SUCCESS:
+    return appendNormalizedNotifications(state, action.notifications, action.next);
+  case ACCOUNT_BLOCK_SUCCESS:
+    return filterNotifications(state, action.relationship);
+  default:
+    return state;
   }
 };
diff --git a/app/assets/javascripts/components/reducers/settings.jsx b/app/assets/javascripts/components/reducers/settings.jsx
new file mode 100644
index 000000000..2a834d81c
--- /dev/null
+++ b/app/assets/javascripts/components/reducers/settings.jsx
@@ -0,0 +1,32 @@
+import { SETTING_CHANGE } from '../actions/settings';
+import { STORE_HYDRATE } from '../actions/store';
+import Immutable from 'immutable';
+
+const initialState = Immutable.Map({
+  notifications: Immutable.Map({
+    alerts: Immutable.Map({
+      follow: true,
+      favourite: true,
+      reblog: true,
+      mention: true
+    }),
+
+    shows: Immutable.Map({
+      follow: true,
+      favourite: true,
+      reblog: true,
+      mention: true
+    })
+  })
+});
+
+export default function settings(state = initialState, action) {
+  switch(action.type) {
+  case STORE_HYDRATE:
+    return state.merge(action.state.get('settings'));
+  case SETTING_CHANGE:
+    return state.setIn(action.key, action.value);
+  default:
+    return state;
+  }
+};
diff --git a/app/controllers/api/web/settings_controller.rb b/app/controllers/api/web/settings_controller.rb
new file mode 100644
index 000000000..e6f690114
--- /dev/null
+++ b/app/controllers/api/web/settings_controller.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Api::Web::SettingsController < ApiController
+  respond_to :json
+
+  before_action :require_user!
+
+  def update
+    setting      = Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
+    setting.data = params[:data]
+    setting.save!
+
+    render_empty
+  end
+end
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index a25fe77da..814b1f758 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -6,6 +6,7 @@ class HomeController < ApplicationController
   def index
     @body_classes = 'app-body'
     @token        = find_or_create_access_token.token
+    @web_settings = Web::Setting.find_by(user: current_user)&.data || {}
   end
 
   private
diff --git a/app/models/account.rb b/app/models/account.rb
index ba24cf153..ec0e81f7c 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -104,7 +104,7 @@ class Account < ApplicationRecord
   end
 
   def subscribed?
-    subscription_expires_at
+    !subscription_expires_at.blank?
   end
 
   def favourited?(status)
@@ -189,7 +189,7 @@ class Account < ApplicationRecord
     def requested_map(target_account_ids, account_id)
       follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
     end
-    
+
     private
 
     def follow_mapping(query, field)
diff --git a/app/models/web.rb b/app/models/web.rb
new file mode 100644
index 000000000..3c6eebbe2
--- /dev/null
+++ b/app/models/web.rb
@@ -0,0 +1,5 @@
+module Web
+  def self.table_name_prefix
+    'web_'
+  end
+end
diff --git a/app/models/web/setting.rb b/app/models/web/setting.rb
new file mode 100644
index 000000000..3d601189b
--- /dev/null
+++ b/app/models/web/setting.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class Web::Setting < ApplicationRecord
+  belongs_to :user
+
+  validates :user, uniqueness: true
+end
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index b4e935041..730249129 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,21 +1,6 @@
 - content_for :header_tags do
   :javascript
-    window.INITIAL_STATE = {
-      "meta": {
-        "access_token": "#{@token}",
-        "locale": "#{I18n.locale}",
-        "me": #{current_account.id}
-      },
-
-      "compose": {
-        "me": #{current_account.id},
-        "private": #{current_account.locked?}
-      },
-
-      "accounts": {
-        #{current_account.id}: #{render(file: 'api/v1/accounts/show', locals: { account: current_user.account }, formats: :json)}
-      }
-    };
+    window.INITIAL_STATE = #{render(file: 'home/initial_state', formats: :json)}
 
   = javascript_include_tag 'application'
 
diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl
new file mode 100644
index 000000000..0e9736f5f
--- /dev/null
+++ b/app/views/home/initial_state.json.rabl
@@ -0,0 +1,24 @@
+object false
+
+node(:meta) {
+  {
+    access_token: @token,
+    locale: I18n.locale,
+    me: current_account.id,
+  }
+}
+
+node(:compose) {
+  {
+    me: current_account.id,
+    private: current_account.locked?,
+  }
+}
+
+node(:accounts) {
+  {
+    current_account.id => partial('api/v1/accounts/show', object: current_account),
+  }
+}
+
+node(:settings) { @web_settings }
diff --git a/config/locales/de.yml b/config/locales/de.yml
index ead3ae514..f36cc64c8 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -14,6 +14,7 @@ de:
     people_followed_by: Nutzer, denen %{name} folgt
     people_who_follow: Nutzer, die %{name} folgen
     posts: Beiträge
+    remote_follow: Folgen
     unfollow: Entfolgen
   application_mailer:
     signature: Mastodon-Benachrichtigungen von %{instance}
@@ -26,6 +27,25 @@ de:
     resend_confirmation: Bestätigung nochmal versenden
     reset_password: Passwort zurücksetzen
     set_new_password: Neues Passwort setzen
+  authorize_follow:
+    error: Das entfernte Profil konnte nicht geladen werden
+    follow: Folgen
+    prompt_html: 'Du (<strong>%{self}</strong>) möchtest dieser Person folgen:'
+    title: "%{acct} folgen"
+  datetime:
+    distance_in_words:
+      about_x_hours: "%{count}h"
+      about_x_months: "%{count}mo"
+      about_x_years: "%{count}y"
+      almost_x_years: "%{count}y"
+      half_a_minute: Gerade eben
+      less_than_x_minutes: "%{count}m"
+      less_than_x_seconds: Gerade eben
+      over_x_years: "%{count}y"
+      x_days: "%{count}d"
+      x_minutes: "%{count}m"
+      x_months: "%{count}mo"
+      x_seconds: "%{count}s"
   generic:
     changes_saved_msg: Änderungen gespeichert!
     powered_by: angetrieben von %{link}
@@ -40,6 +60,9 @@ de:
     follow:
       body: "%{name} folgt dir jetzt!"
       subject: "%{name} folgt dir nun"
+    follow_request:
+      body: "%{name} möchte dir folgen:"
+      subject: "%{name} möchte dir folgen"
     mention:
       body: "%{name} hat dich erwähnt:"
       subject: "%{name} hat dich erwähnt"
@@ -49,13 +72,23 @@ de:
   pagination:
     next: Vorwärts
     prev: Zurück
+  remote_follow:
+    acct: Dein Nutzername@Domain, von dem du dieser Person folgen möchtest
+    missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden
+    proceed: Weiter
+    prompt: 'Du wirst dieser Person folgen:'
   settings:
     edit_profile: Profil bearbeiten
     preferences: Einstellungen
   stream_entries:
+    click_to_show: Klicken um zu zeigen
     favourited: favorisierte einen Beitrag von
     is_now_following: folgt nun
     reblogged: teilte
+    sensitive_content: Sensible Inhalte
+  time:
+    formats:
+      default: "%d.%m.%Y %H:%M"
   users:
     invalid_email: Inkorrekte E-mail-Addresse
   will_paginate:
diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml
index d0aed9d0e..614cd4911 100644
--- a/config/locales/simple_form.de.yml
+++ b/config/locales/simple_form.de.yml
@@ -1,6 +1,9 @@
 ---
 de:
   simple_form:
+    hints:
+      defaults:
+        locked: Erlaubt dir, Folger zu überprüfen, bevor sie dir folgen können
     labels:
       defaults:
         avatar: Avatar
@@ -11,6 +14,7 @@ de:
         email: E-mail-Addresse
         header: Kopfbild
         locale: Sprache
+        locked: Gesperrter Profil
         new_password: Neues Passwort
         note: Über mich
         password: Passwort
@@ -21,6 +25,7 @@ de:
       notification_emails:
         favourite: E-mail senden, wenn jemand meinen Beitrag favorisiert
         follow: E-mail senden, wenn mir jemand folgt
+        follow_request: E-mail senden, wenn mir jemand folgen möchte
         mention: E-mail senden, wenn mich jemand erwähnt
         reblog: E-mail senden, wenn jemand meinen Beitrag teilt
     'no': Nein
diff --git a/config/routes.rb b/config/routes.rb
index e46b27f1f..dd6944b29 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -134,6 +134,10 @@ Rails.application.routes.draw do
         end
       end
     end
+
+    namespace :web do
+      resource :settings, only: [:update]
+    end
   end
 
   get '/web/(*any)', to: 'home#index', as: :web
diff --git a/db/migrate/20170109120109_create_web_settings.rb b/db/migrate/20170109120109_create_web_settings.rb
new file mode 100644
index 000000000..2aeae1f91
--- /dev/null
+++ b/db/migrate/20170109120109_create_web_settings.rb
@@ -0,0 +1,12 @@
+class CreateWebSettings < ActiveRecord::Migration[5.0]
+  def change
+    create_table :web_settings do |t|
+      t.integer :user_id
+      t.json :data
+
+      t.timestamps
+    end
+
+    add_index :web_settings, :user_id, unique: true
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a535c5fdb..5a5dd83c7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20170105224407) do
+ActiveRecord::Schema.define(version: 20170109120109) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -169,6 +169,74 @@ ActiveRecord::Schema.define(version: 20170105224407) do
     t.index ["topic", "callback"], name: "index_pubsubhubbub_subscriptions_on_topic_and_callback", unique: true, using: :btree
   end
 
+  create_table "push_devices", force: :cascade do |t|
+    t.string   "service",    default: "", null: false
+    t.string   "token",      default: "", null: false
+    t.integer  "account",                 null: false
+    t.datetime "created_at",              null: false
+    t.datetime "updated_at",              null: false
+    t.index ["service", "token"], name: "index_push_devices_on_service_and_token", unique: true, using: :btree
+  end
+
+  create_table "rpush_apps", force: :cascade do |t|
+    t.string   "name",                                null: false
+    t.string   "environment"
+    t.text     "certificate"
+    t.string   "password"
+    t.integer  "connections",             default: 1, null: false
+    t.datetime "created_at",                          null: false
+    t.datetime "updated_at",                          null: false
+    t.string   "type",                                null: false
+    t.string   "auth_key"
+    t.string   "client_id"
+    t.string   "client_secret"
+    t.string   "access_token"
+    t.datetime "access_token_expiration"
+  end
+
+  create_table "rpush_feedback", force: :cascade do |t|
+    t.string   "device_token", limit: 64, null: false
+    t.datetime "failed_at",               null: false
+    t.datetime "created_at",              null: false
+    t.datetime "updated_at",              null: false
+    t.integer  "app_id"
+    t.index ["device_token"], name: "index_rpush_feedback_on_device_token", using: :btree
+  end
+
+  create_table "rpush_notifications", force: :cascade do |t|
+    t.integer  "badge"
+    t.string   "device_token",      limit: 64
+    t.string   "sound",                        default: "default"
+    t.text     "alert"
+    t.text     "data"
+    t.integer  "expiry",                       default: 86400
+    t.boolean  "delivered",                    default: false,     null: false
+    t.datetime "delivered_at"
+    t.boolean  "failed",                       default: false,     null: false
+    t.datetime "failed_at"
+    t.integer  "error_code"
+    t.text     "error_description"
+    t.datetime "deliver_after"
+    t.datetime "created_at",                                       null: false
+    t.datetime "updated_at",                                       null: false
+    t.boolean  "alert_is_json",                default: false
+    t.string   "type",                                             null: false
+    t.string   "collapse_key"
+    t.boolean  "delay_while_idle",             default: false,     null: false
+    t.text     "registration_ids"
+    t.integer  "app_id",                                           null: false
+    t.integer  "retries",                      default: 0
+    t.string   "uri"
+    t.datetime "fail_after"
+    t.boolean  "processing",                   default: false,     null: false
+    t.integer  "priority"
+    t.text     "url_args"
+    t.string   "category"
+    t.boolean  "content_available",            default: false
+    t.text     "notification"
+    t.index ["delivered", "failed"], name: "index_rpush_notifications_multi", where: "((NOT delivered) AND (NOT failed))", using: :btree
+  end
+
   create_table "settings", force: :cascade do |t|
     t.string   "var",         null: false
     t.text     "value"
@@ -191,7 +259,6 @@ ActiveRecord::Schema.define(version: 20170105224407) do
     t.boolean  "sensitive",              default: false
     t.integer  "visibility",             default: 0,     null: false
     t.integer  "in_reply_to_account_id"
-    t.string   "conversation_uri"
     t.index ["account_id"], name: "index_statuses_on_account_id", using: :btree
     t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", using: :btree
     t.index ["reblog_of_id"], name: "index_statuses_on_reblog_of_id", using: :btree
@@ -260,4 +327,12 @@ ActiveRecord::Schema.define(version: 20170105224407) do
     t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
   end
 
+  create_table "web_settings", force: :cascade do |t|
+    t.integer  "user_id"
+    t.json     "data"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["user_id"], name: "index_web_settings_on_user_id", unique: true, using: :btree
+  end
+
 end
diff --git a/spec/fabricators/media_attachment_fabricator.rb b/spec/fabricators/media_attachment_fabricator.rb
index b1a0cd991..59db2440d 100644
--- a/spec/fabricators/media_attachment_fabricator.rb
+++ b/spec/fabricators/media_attachment_fabricator.rb
@@ -1,2 +1,3 @@
 Fabricator(:media_attachment) do
+
 end
diff --git a/spec/fabricators/web_setting_fabricator.rb b/spec/fabricators/web_setting_fabricator.rb
new file mode 100644
index 000000000..e5136829b9
--- /dev/null
+++ b/spec/fabricators/web_setting_fabricator.rb
@@ -0,0 +1,3 @@
+Fabricator('Web::Setting') do
+
+end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index a72369b1c..287f389ac 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -154,6 +154,31 @@ RSpec.describe Account, type: :model do
     end
   end
 
+  describe '.following_map' do
+    it 'returns an hash' do
+      expect(Account.following_map([], 1)).to be_a Hash
+    end
+  end
+
+  describe '.followed_by_map' do
+    it 'returns an hash' do
+      expect(Account.followed_by_map([], 1)).to be_a Hash
+    end
+  end
+
+  describe '.blocking_map' do
+    it 'returns an hash' do
+      expect(Account.blocking_map([], 1)).to be_a Hash
+    end
+  end
+
+  describe '.requested_map' do
+    it 'returns an hash' do
+      expect(Account.requested_map([], 1)).to be_a Hash
+    end
+  end
+
+
   describe 'MENTION_RE' do
     subject { Account::MENTION_RE }
 
diff --git a/spec/models/web/setting_spec.rb b/spec/models/web/setting_spec.rb
new file mode 100644
index 000000000..90e7695aa
--- /dev/null
+++ b/spec/models/web/setting_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Web::Setting, type: :model do
+
+end