From 2b0b7ff1b8b134e97c8827721a59cac21897932c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 4 Jan 2017 15:35:36 +0100 Subject: Fix #385 - /web now loads a page --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'config') diff --git a/config/routes.rb b/config/routes.rb index 18c239c48..e46b27f1f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -136,7 +136,7 @@ Rails.application.routes.draw do end end - get '/web/*any', to: 'home#index', as: :web + get '/web/(*any)', to: 'home#index', as: :web get :about, to: 'about#index' get :terms, to: 'about#terms' -- cgit From bb033c1d37db11a871011ac8cdf6737721fbc13e Mon Sep 17 00:00:00 2001 From: Jessica Stokes Date: Wed, 4 Jan 2017 18:00:50 -0800 Subject: "Reblog" -> "boost" in more places A couple of places were using "reblog" rather than "boost" - this updates them to match the web UI --- .../components/features/notifications/components/notification.jsx | 2 +- config/locales/en.yml | 4 ++-- spec/mailers/notification_mailer_spec.rb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'config') diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx index 9f4cf9e4d..37715dd05 100644 --- a/app/assets/javascripts/components/features/notifications/components/notification.jsx +++ b/app/assets/javascripts/components/features/notifications/components/notification.jsx @@ -71,7 +71,7 @@ const Notification = React.createClass({ - + diff --git a/config/locales/en.yml b/config/locales/en.yml index e166fc717..dd2ae3aac 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -67,8 +67,8 @@ en: body: 'You were mentioned by %{name} in:' subject: You were mentioned by %{name} reblog: - body: 'Your status was reblogged by %{name}:' - subject: "%{name} reblogged your status" + body: 'Your status was boosted by %{name}:' + subject: "%{name} boosted your status" pagination: next: Next prev: Prev diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index d4baca5aa..3beaebeb1 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -53,12 +53,12 @@ RSpec.describe NotificationMailer, type: :mailer do let(:mail) { NotificationMailer.reblog(own_status.account, Notification.create!(account: receiver.account, activity: reblog)) } it "renders the headers" do - expect(mail.subject).to eq("bob reblogged your status") + expect(mail.subject).to eq("bob boosted your status") expect(mail.to).to eq([receiver.email]) end it "renders the body" do - expect(mail.body.encoded).to match("Your status was reblogged by bob") + expect(mail.body.encoded).to match("Your status was boosted by bob") end end -- cgit From 7ddec6e7c3a8bdfcc69d28a723caca61cdb2a17c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 7 Jan 2017 15:43:56 +0100 Subject: Add read timeout to paperclip when it's downloading remote images --- config/initializers/paperclip.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'config') diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index cb7ed4487..4d6154562 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +Paperclip::Attachment.default_options[:read_timeout] = 60 + if ENV['S3_ENABLED'] == 'true' Aws.eager_autoload!(services: %w(S3)) -- cgit From 2e71bb031b2dff90a2b0f9854bdcd804c069268a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 8 Jan 2017 19:12:54 +0100 Subject: Fix Paperclip timeout setting. Fix bug introduced in #437 --- app/models/account.rb | 6 ++++-- config/initializers/paperclip.rb | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'config') diff --git a/app/models/account.rb b/app/models/account.rb index 3a7763a64..ba24cf153 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -190,8 +190,10 @@ class Account < ApplicationRecord follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) end - private def follow_mapping(query, field) - query.pluck(field).inject({}) { |mapping, id| mapping[id] = true } + private + + def follow_mapping(query, field) + query.pluck(field).inject({}) { |mapping, id| mapping[id] = true; mapping } end end diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 4d6154562..999ff47c7 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -Paperclip::Attachment.default_options[:read_timeout] = 60 +Paperclip.options[:read_timeout] = 60 if ENV['S3_ENABLED'] == 'true' Aws.eager_autoload!(services: %w(S3)) -- cgit From 75f80bef107cfe9e9c0e6ba3dc51ef86c89e40cc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 9 Jan 2017 14:00:55 +0100 Subject: Persist UI settings, add missing localizations for German --- .../components/actions/notifications.jsx | 10 --- .../javascripts/components/actions/settings.jsx | 17 +++++ .../containers/column_settings_container.jsx | 6 +- .../components/features/notifications/index.jsx | 2 +- app/assets/javascripts/components/locales/de.jsx | 18 ++++- app/assets/javascripts/components/locales/en.jsx | 2 +- .../javascripts/components/reducers/index.jsx | 4 +- .../components/reducers/notifications.jsx | 41 +++-------- .../javascripts/components/reducers/settings.jsx | 32 +++++++++ app/controllers/api/web/settings_controller.rb | 15 ++++ app/controllers/home_controller.rb | 1 + app/models/account.rb | 4 +- app/models/web.rb | 5 ++ app/models/web/setting.rb | 7 ++ app/views/home/index.html.haml | 17 +---- app/views/home/initial_state.json.rabl | 24 +++++++ config/locales/de.yml | 33 +++++++++ config/locales/simple_form.de.yml | 5 ++ config/routes.rb | 4 ++ db/migrate/20170109120109_create_web_settings.rb | 12 ++++ db/schema.rb | 79 +++++++++++++++++++++- spec/fabricators/media_attachment_fabricator.rb | 1 + spec/fabricators/web_setting_fabricator.rb | 3 + spec/models/account_spec.rb | 25 +++++++ spec/models/web/setting_spec.rb | 5 ++ 25 files changed, 305 insertions(+), 67 deletions(-) create mode 100644 app/assets/javascripts/components/actions/settings.jsx create mode 100644 app/assets/javascripts/components/reducers/settings.jsx create mode 100644 app/controllers/api/web/settings_controller.rb create mode 100644 app/models/web.rb create mode 100644 app/models/web/setting.rb create mode 100644 app/views/home/initial_state.json.rabl create mode 100644 db/migrate/20170109120109_create_web_settings.rb create mode 100644 spec/fabricators/web_setting_fabricator.rb create mode 100644 spec/models/web/setting_spec.rb (limited to 'config') 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 (%{self}) 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 -- cgit From 4292cf60ae6b12b689980bce06746fec602bcc57 Mon Sep 17 00:00:00 2001 From: Eugen Date: Tue, 10 Jan 2017 22:01:13 +0100 Subject: Fix env variable name --- config/database.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'config') diff --git a/config/database.yml b/config/database.yml index 52c26f599..5ec342f93 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,6 +1,6 @@ default: &default adapter: postgresql - pool: <%= ENV["DB_POOL"] || ENV['RAILS_MAX_THREADS'] || 5 %> + pool: <%= ENV["DB_POOL"] || ENV['MAX_THREADS'] || 5 %> timeout: 5000 encoding: unicode -- cgit From b11fdc3ae3f90731c01149a5a36dc64e065d4ea2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 12 Jan 2017 20:46:24 +0100 Subject: Migrate from ledermann/rails-settings to rails-settings-cached which allows global settings with YAML-defined defaults. Add admin page for editing global settings. Add "site_description" setting that would show as a paragraph on the frontpage --- Gemfile | 3 ++- Gemfile.lock | 10 ++++--- app/assets/javascripts/application_public.js | 5 ++++ app/assets/stylesheets/tables.scss | 4 +++ app/controllers/about_controller.rb | 4 +-- app/controllers/admin/settings_controller.rb | 25 +++++++++++++++++ app/controllers/settings/preferences_controller.rb | 20 ++++++++------ app/helpers/settings_helper.rb | 4 +++ app/lib/hash_object.rb | 10 +++++++ app/models/setting.rb | 31 ++++++++++++++++++++++ app/models/user.rb | 7 ++--- app/services/notify_service.rb | 16 +++++------ app/views/about/index.html.haml | 5 +++- app/views/admin/settings/index.html.haml | 22 +++++++++++++++ app/views/settings/preferences/show.html.haml | 4 +-- config/navigation.rb | 1 + config/routes.rb | 1 + config/settings.yml | 23 ++++++++++++++++ db/migrate/20170112154826_migrate_settings.rb | 19 +++++++++++++ db/schema.rb | 10 +++---- 20 files changed, 189 insertions(+), 35 deletions(-) create mode 100644 app/controllers/admin/settings_controller.rb create mode 100644 app/lib/hash_object.rb create mode 100644 app/models/setting.rb create mode 100644 app/views/admin/settings/index.html.haml create mode 100644 config/settings.yml create mode 100644 db/migrate/20170112154826_migrate_settings.rb (limited to 'config') diff --git a/Gemfile b/Gemfile index 590fb2124..6b8519369 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,7 @@ gem 'pg' gem 'pghero' gem 'dotenv-rails' gem 'font-awesome-rails' +gem 'best_in_place', '~> 3.0.1' gem 'paperclip', '~> 5.0' gem 'paperclip-av-transcoder' @@ -43,7 +44,7 @@ gem 'will_paginate' gem 'rack-attack' gem 'rack-cors', require: 'rack/cors' gem 'sidekiq' -gem 'ledermann-rails-settings' +gem 'rails-settings-cached' gem 'pg_search' gem 'simple-navigation' diff --git a/Gemfile.lock b/Gemfile.lock index 2408df68d..2c009955e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,6 +60,9 @@ GEM babel-source (>= 4.0, < 6) execjs (~> 2.0) bcrypt (3.1.11) + best_in_place (3.0.3) + actionpack (>= 3.2) + railties (>= 3.2) better_errors (2.1.1) coderay (>= 1.0.0) erubis (>= 2.6.6) @@ -172,8 +175,6 @@ GEM json (1.8.3) launchy (2.4.3) addressable (~> 2.3) - ledermann-rails-settings (2.4.2) - activerecord (>= 3.1) letter_opener (1.4.1) launchy (~> 2.2) link_header (0.0.8) @@ -259,6 +260,8 @@ GEM nokogiri (~> 1.6.0) rails-html-sanitizer (1.0.3) loofah (~> 2.0) + rails-settings-cached (0.6.5) + rails (>= 4.2.0) rails_12factor (0.0.3) rails_serve_static_assets rails_stdout_logging @@ -405,6 +408,7 @@ DEPENDENCIES addressable autoprefixer-rails aws-sdk (>= 2.0) + best_in_place (~> 3.0.1) better_errors binding_of_caller browserify-rails @@ -426,7 +430,6 @@ DEPENDENCIES i18n-tasks (~> 0.9.6) jbuilder (~> 2.0) jquery-rails - ledermann-rails-settings letter_opener link_header lograge @@ -445,6 +448,7 @@ DEPENDENCIES rack-cors rack-timeout-puma rails (~> 5.0.1.0) + rails-settings-cached rails_12factor rails_autolink react-rails diff --git a/app/assets/javascripts/application_public.js b/app/assets/javascripts/application_public.js index f131a267a..9626c5dae 100644 --- a/app/assets/javascripts/application_public.js +++ b/app/assets/javascripts/application_public.js @@ -1,3 +1,8 @@ //= require jquery //= require jquery_ujs //= require extras +//= require best_in_place + +$(function () { + $(".best_in_place").best_in_place(); +}); diff --git a/app/assets/stylesheets/tables.scss b/app/assets/stylesheets/tables.scss index a37870786..279cc3069 100644 --- a/app/assets/stylesheets/tables.scss +++ b/app/assets/stylesheets/tables.scss @@ -36,6 +36,10 @@ text-decoration: none; } } + + strong { + font-weight: 500; + } } samp { diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 7df58444f..84e5fbbd9 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -4,10 +4,10 @@ class AboutController < ApplicationController before_action :set_body_classes def index + @description = Setting.site_description end - def terms - end + def terms; end private diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb new file mode 100644 index 000000000..af0be8823 --- /dev/null +++ b/app/controllers/admin/settings_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Admin::SettingsController < ApplicationController + before_action :require_admin! + + layout 'admin' + + def index + @settings = Setting.all_as_records + end + + def update + @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id]) + + if @setting.value != params[:setting][:value] + @setting.value = params[:setting][:value] + @setting.save + end + + respond_to do |format| + format.html { redirect_to admin_settings_path } + format.json { respond_with_bip(@setting) } + end + end +end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 3b6d109a6..f273b5f21 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -8,14 +8,18 @@ class Settings::PreferencesController < ApplicationController def show; end def update - current_user.settings(:notification_emails).follow = user_params[:notification_emails][:follow] == '1' - current_user.settings(:notification_emails).follow_request = user_params[:notification_emails][:follow_request] == '1' - current_user.settings(:notification_emails).reblog = user_params[:notification_emails][:reblog] == '1' - current_user.settings(:notification_emails).favourite = user_params[:notification_emails][:favourite] == '1' - current_user.settings(:notification_emails).mention = user_params[:notification_emails][:mention] == '1' - - current_user.settings(:interactions).must_be_follower = user_params[:interactions][:must_be_follower] == '1' - current_user.settings(:interactions).must_be_following = user_params[:interactions][:must_be_following] == '1' + current_user.settings['notification_emails'] = { + follow: user_params[:notification_emails][:follow] == '1', + follow_request: user_params[:notification_emails][:follow_request] == '1', + reblog: user_params[:notification_emails][:reblog] == '1', + favourite: user_params[:notification_emails][:favourite] == '1', + mention: user_params[:notification_emails][:mention] == '1', + } + + current_user.settings['interactions'] = { + must_be_follower: user_params[:interactions][:must_be_follower] == '1', + must_be_following: user_params[:interactions][:must_be_following] == '1', + } if current_user.update(user_params.except(:notification_emails, :interactions)) redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg') diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index fa569e73a..aed8770c8 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -14,4 +14,8 @@ module SettingsHelper def human_locale(locale) HUMAN_LOCALES[locale] end + + def hash_to_object(hash) + HashObject.new(hash) + end end diff --git a/app/lib/hash_object.rb b/app/lib/hash_object.rb new file mode 100644 index 000000000..274c020ad --- /dev/null +++ b/app/lib/hash_object.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class HashObject + def initialize(hash) + hash.each do |k, v| + instance_variable_set("@#{k}", v) + self.class.send(:define_method, k, proc { instance_variable_get("@#{k}") }) + end + end +end diff --git a/app/models/setting.rb b/app/models/setting.rb new file mode 100644 index 000000000..0a429a62b --- /dev/null +++ b/app/models/setting.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Setting < RailsSettings::Base + source Rails.root.join('config/settings.yml') + namespace Rails.env + + def to_param + var + end + + class << self + def all_as_records + vars = thing_scoped + records = vars.map { |r| [r.var, r] }.to_h + + default_settings.each do |key, default_value| + next if records.key?(key) || default_value.is_a?(Hash) + records[key] = Setting.new(var: key, value: default_value) + end + + records + end + + private + + def default_settings + return {} unless RailsSettings::Default.enabled? + RailsSettings::Default.instance + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index d5a52da06..bf7d04d7c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class User < ApplicationRecord + include RailsSettings::Extend + devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable belongs_to :account, inverse_of: :user @@ -14,11 +16,6 @@ class User < ApplicationRecord scope :recent, -> { order('id desc') } scope :admins, -> { where(admin: true) } - has_settings do |s| - s.key :notification_emails, defaults: { follow: false, reblog: false, favourite: false, mention: false, follow_request: true } - s.key :interactions, defaults: { must_be_follower: false, must_be_following: false } - end - def send_devise_notification(notification, *args) devise_mailer.send(notification, self, *args).deliver_later end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 2fb1d3919..2eb0f417d 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -37,13 +37,13 @@ class NotifyService < BaseService end def blocked? - blocked = @recipient.suspended? # Skip if the recipient account is suspended anyway - blocked ||= @recipient.id == @notification.from_account.id # Skip for interactions with self - blocked ||= @recipient.blocking?(@notification.from_account) # Skip for blocked accounts - blocked ||= (@notification.from_account.silenced? && !@recipient.following?(@notification.from_account)) # Hellban - blocked ||= (@recipient.user.settings(:interactions).must_be_follower && !@notification.from_account.following?(@recipient)) # Options - blocked ||= (@recipient.user.settings(:interactions).must_be_following && !@recipient.following?(@notification.from_account)) # Options - blocked ||= send("blocked_#{@notification.type}?") # Type-dependent filters + blocked = @recipient.suspended? # Skip if the recipient account is suspended anyway + blocked ||= @recipient.id == @notification.from_account.id # Skip for interactions with self + blocked ||= @recipient.blocking?(@notification.from_account) # Skip for blocked accounts + blocked ||= (@notification.from_account.silenced? && !@recipient.following?(@notification.from_account)) # Hellban + blocked ||= (@recipient.user.settings.interactions['must_be_follower'] && !@notification.from_account.following?(@recipient)) # Options + blocked ||= (@recipient.user.settings.interactions['must_be_following'] && !@recipient.following?(@notification.from_account)) # Options + blocked ||= send("blocked_#{@notification.type}?") # Type-dependent filters blocked end @@ -58,6 +58,6 @@ class NotifyService < BaseService end def email_enabled? - @recipient.user.settings(:notification_emails).send(@notification.type) + @recipient.user.settings.notification_emails[@notification.type] end end diff --git a/app/views/about/index.html.haml b/app/views/about/index.html.haml index 0c6516036..a593ff578 100644 --- a/app/views/about/index.html.haml +++ b/app/views/about/index.html.haml @@ -8,7 +8,7 @@ %meta{ property: 'og:site_name', content: 'Mastodon' }/ %meta{ property: 'og:type', content: 'website' }/ %meta{ property: 'og:title', content: Rails.configuration.x.local_domain }/ - %meta{ property: 'og:description', content: "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" }/ + %meta{ property: 'og:description', content: @description.blank? ? "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" : strip_tags(@description) }/ %meta{ property: 'og:image', content: asset_url('mastodon_small.jpg') }/ %meta{ property: 'og:image:width', content: '400' }/ %meta{ property: 'og:image:height', content: '400' }/ @@ -24,6 +24,9 @@ .screenshot= image_tag 'screenshot.png' + - unless @description.blank? + %p= @description.html_safe + .actions .info = link_to t('about.terms'), terms_path diff --git a/app/views/admin/settings/index.html.haml b/app/views/admin/settings/index.html.haml new file mode 100644 index 000000000..b8ca3a7a4 --- /dev/null +++ b/app/views/admin/settings/index.html.haml @@ -0,0 +1,22 @@ +- content_for :page_title do + Site Settings + +%table.table + %colgroup + %col{ width: '35%' }/ + %thead + %tr + %th Setting + %th Click to edit + %tbody + %tr + %td + %strong Site description + %br/ + Displayed as a paragraph on the frontpage and used as a meta tag. + %br/ + You can use HTML tags, in particular + %code= '' + and + %code= '' + %td= best_in_place @settings['site_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_description']) diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index a0860c94b..a9a1d21ac 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -6,14 +6,14 @@ = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) } - = f.simple_fields_for :notification_emails, current_user.settings(:notification_emails) do |ff| + = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| = ff.input :follow, as: :boolean, wrapper: :with_label = ff.input :follow_request, as: :boolean, wrapper: :with_label = ff.input :reblog, as: :boolean, wrapper: :with_label = ff.input :favourite, as: :boolean, wrapper: :with_label = ff.input :mention, as: :boolean, wrapper: :with_label - = f.simple_fields_for :interactions, current_user.settings(:interactions) do |ff| + = f.simple_fields_for :interactions, hash_to_object(current_user.settings.interactions) do |ff| = ff.input :must_be_follower, as: :boolean, wrapper: :with_label = ff.input :must_be_following, as: :boolean, wrapper: :with_label diff --git a/config/navigation.rb b/config/navigation.rb index 1b6615ed0..9aaa12b0b 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -7,5 +7,6 @@ SimpleNavigation::Configuration.run do |navigation| primary.item :domain_blocks, safe_join([fa_icon('lock fw'), 'Domain Blocks']), admin_domain_blocks_url primary.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url primary.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url + primary.item :settings, safe_join([fa_icon('cogs fw'), 'Site Settings']), admin_settings_url end end diff --git a/config/routes.rb b/config/routes.rb index dd6944b29..c0262e933 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,6 +58,7 @@ Rails.application.routes.draw do namespace :admin do resources :pubsubhubbub, only: [:index] resources :domain_blocks, only: [:index, :create] + resources :settings, only: [:index, :update] resources :accounts, only: [:index, :show, :update] do member do diff --git a/config/settings.yml b/config/settings.yml new file mode 100644 index 000000000..2e309e46e --- /dev/null +++ b/config/settings.yml @@ -0,0 +1,23 @@ +# config/app.yml for rails-settings-cached +defaults: &defaults + site_description: '' + site_contact_username: '' + site_contact_email: '' + notification_emails: + follow: false + reblog: false + favourite: false + mention: false + follow_request: true + interactions: + must_be_follower: false + must_be_following: false + +development: + <<: *defaults + +test: + <<: *defaults + +production: + <<: *defaults diff --git a/db/migrate/20170112154826_migrate_settings.rb b/db/migrate/20170112154826_migrate_settings.rb new file mode 100644 index 000000000..f6f6ed531 --- /dev/null +++ b/db/migrate/20170112154826_migrate_settings.rb @@ -0,0 +1,19 @@ +class MigrateSettings < ActiveRecord::Migration + def up + remove_index :settings, [:target_type, :target_id, :var] + rename_column :settings, :target_id, :thing_id + rename_column :settings, :target_type, :thing_type + change_column :settings, :thing_id, :integer, null: true, default: nil + change_column :settings, :thing_type, :string, null: true, default: nil + add_index :settings, [:thing_type, :thing_id, :var], unique: true + end + + def down + remove_index :settings, [:thing_type, :thing_id, :var] + rename_column :settings, :thing_id, :target_id + rename_column :settings, :thing_type, :target_type + change_column :settings, :target_id, :integer, null: false + change_column :settings, :target_type, :string, null: false, default: '' + add_index :settings, [:target_type, :target_id, :var], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 5a5dd83c7..1cd1258db 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: 20170109120109) do +ActiveRecord::Schema.define(version: 20170112154826) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -238,13 +238,13 @@ ActiveRecord::Schema.define(version: 20170109120109) do end create_table "settings", force: :cascade do |t| - t.string "var", null: false + t.string "var", null: false t.text "value" - t.string "target_type", null: false - t.integer "target_id", null: false + t.string "thing_type" + t.integer "thing_id" t.datetime "created_at" t.datetime "updated_at" - t.index ["target_type", "target_id", "var"], name: "index_settings_on_target_type_and_target_id_and_var", unique: true, using: :btree + t.index ["thing_type", "thing_id", "var"], name: "index_settings_on_thing_type_and_thing_id_and_var", unique: true, using: :btree end create_table "statuses", force: :cascade do |t| -- cgit From e25170f960bdc890e4f8a6b1373ab55192669629 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 13 Jan 2017 03:24:41 +0100 Subject: Add extended about page stub --- app/controllers/about_controller.rb | 6 ++++++ app/lib/settings/extend.rb | 6 ++++-- app/lib/settings/scoped_settings.rb | 4 +++- app/models/account.rb | 1 + app/models/setting.rb | 5 ++--- app/models/web.rb | 2 ++ app/views/about/more.html.haml | 8 ++++++++ app/views/admin/settings/index.html.haml | 18 +++++++++++++----- config/routes.rb | 7 ++++--- config/settings.yml | 2 +- 10 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 app/views/about/more.html.haml (limited to 'config') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 84e5fbbd9..b69c8e790 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -7,6 +7,12 @@ class AboutController < ApplicationController @description = Setting.site_description end + def more + @extended_description = Setting.site_extended_description + @contact_account = Account.find_local(Setting.site_contact_username) + @contact_email = Setting.site_contact_email + end + def terms; end private diff --git a/app/lib/settings/extend.rb b/app/lib/settings/extend.rb index 7241a1221..407c3480f 100644 --- a/app/lib/settings/extend.rb +++ b/app/lib/settings/extend.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module Settings module Extend - extend ActiveSupport::Concern + extend ActiveSupport::Concern def settings ScopedSettings.for_thing(self) end end -end \ No newline at end of file +end diff --git a/app/lib/settings/scoped_settings.rb b/app/lib/settings/scoped_settings.rb index f8f22a91b..82b70d128 100644 --- a/app/lib/settings/scoped_settings.rb +++ b/app/lib/settings/scoped_settings.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Settings class ScopedSettings < ::Setting def self.for_thing(object) @@ -9,4 +11,4 @@ module Settings unscoped.where(thing_type: @object.class.base_class.to_s, thing_id: @object.id) end end -end \ No newline at end of file +end diff --git a/app/models/account.rb b/app/models/account.rb index ec0e81f7c..5f07615fc 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -159,6 +159,7 @@ class Account < ApplicationRecord end def find_remote!(username, domain) + return if username.blank? where(arel_table[:username].matches(username.gsub(/[%_]/, '\\\\\0'))).where(domain.nil? ? { domain: nil } : arel_table[:domain].matches(domain.gsub(/[%_]/, '\\\\\0'))).take! end diff --git a/app/models/setting.rb b/app/models/setting.rb index f3c65c054..3796253d4 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -9,10 +9,9 @@ class Setting < RailsSettings::Base end class << self - def [](key) return super(key) unless rails_initialized? - + val = Rails.cache.fetch(cache_key(key, @object)) do db_val = object(key) @@ -25,7 +24,7 @@ class Setting < RailsSettings::Base default_settings[key] end end - + val end diff --git a/app/models/web.rb b/app/models/web.rb index 3c6eebbe2..58654fd77 100644 --- a/app/models/web.rb +++ b/app/models/web.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Web def self.table_name_prefix 'web_' diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml new file mode 100644 index 000000000..49a3a0c95 --- /dev/null +++ b/app/views/about/more.html.haml @@ -0,0 +1,8 @@ +- content_for :page_title do + #{Rails.configuration.x.local_domain} + +.wrapper + = @extended_description.html_safe + + - if @contact_account + = render partial: 'authorize_follow/card', locals: { account: @contact_account } \ No newline at end of file diff --git a/app/views/admin/settings/index.html.haml b/app/views/admin/settings/index.html.haml index 4f9ad2fde..5b482213b 100644 --- a/app/views/admin/settings/index.html.haml +++ b/app/views/admin/settings/index.html.haml @@ -9,6 +9,12 @@ %th Setting %th Click to edit %tbody + %tr + %td{ rowspan: 2 } + %strong Contact information + %td= best_in_place @settings['site_contact_username'], :value, url: admin_setting_path(@settings['site_contact_username']), place_holder: 'Enter a username' + %tr + %td= best_in_place @settings['site_contact_email'], :value, url: admin_setting_path(@settings['site_contact_email']), place_holder: 'Enter a public e-mail address' %tr %td %strong Site description @@ -21,8 +27,10 @@ %code= '' %td= best_in_place @settings['site_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_description']) %tr - %td{ rowspan: 2 } - %strong Contact information - %td= best_in_place @settings['site_contact_username'], :value, url: admin_setting_path(@settings['site_contact_username']), place_holder: 'Enter a username' - %tr - %td= best_in_place @settings['site_contact_email'], :value, url: admin_setting_path(@settings['site_contact_email']), place_holder: 'Enter a public e-mail address' + %td + %strong Extended site description + %br/ + Displayed on extended information page + %br/ + You can use HTML tags + %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description']) \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index c0262e933..42de503f0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -143,9 +143,10 @@ Rails.application.routes.draw do get '/web/(*any)', to: 'home#index', as: :web - get :about, to: 'about#index' - get :terms, to: 'about#terms' - + get '/about', to: 'about#index' + get '/about/more', to: 'about#more' + get '/terms', to: 'about#terms' + root 'home#index' match '*unmatched_route', via: :all, to: 'application#raise_not_found' diff --git a/config/settings.yml b/config/settings.yml index 2e309e46e..a78bd067d 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,6 +1,7 @@ # config/app.yml for rails-settings-cached defaults: &defaults site_description: '' + site_extended_description: '' site_contact_username: '' site_contact_email: '' notification_emails: @@ -12,7 +13,6 @@ defaults: &defaults interactions: must_be_follower: false must_be_following: false - development: <<: *defaults -- cgit From c01dd089ff3149ce6b5bf539143b335a84c08eee Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 13 Jan 2017 20:14:21 +0100 Subject: Adding about/more page with extended information that can be set up by an admin --- app/assets/images/background-photo.jpeg | Bin 128834 -> 894792 bytes app/assets/stylesheets/about.scss | 177 ++++++++++++++++++++++++++++++-- app/controllers/about_controller.rb | 4 + app/views/about/index.html.haml | 1 + app/views/about/more.html.haml | 49 ++++++++- config/locales/en.yml | 10 ++ db/schema.rb | 69 ++++++++++++- spec/i18n_spec.rb | 4 +- 8 files changed, 302 insertions(+), 12 deletions(-) (limited to 'config') diff --git a/app/assets/images/background-photo.jpeg b/app/assets/images/background-photo.jpeg index 4390fca66..b0a88ff35 100644 Binary files a/app/assets/images/background-photo.jpeg and b/app/assets/images/background-photo.jpeg differ diff --git a/app/assets/stylesheets/about.scss b/app/assets/stylesheets/about.scss index 620c86a67..83b0ee079 100644 --- a/app/assets/stylesheets/about.scss +++ b/app/assets/stylesheets/about.scss @@ -26,15 +26,19 @@ } h2 { - font: 24px/28px 'Judson', sans-serif; - font-weight: 300; + font-family: 'Montserrat', sans-serif; + font-size: 24px; + line-height: 28px;// 'Judson', sans-serif; + font-weight: 400; margin-bottom: 20px; color: #fff; } h3 { - font: 20px/28px 'Judson', sans-serif; - font-weight: 300; + font-family: 'Montserrat', sans-serif; + font-size: 20px; + line-height: 28px;// 'Judson', sans-serif; + font-weight: 400; margin-bottom: 20px; color: #d9e1e8; } @@ -57,8 +61,10 @@ } p, li { - font: 20px/28px 'Judson', sans-serif; - font-weight: 300; + font: 16px/28px 'Montserrat', sans-serif; + //font-size: 19px; + //line-height: 28px;// 'Judson', sans-serif; + font-weight: 400; margin-bottom: 26px; a { @@ -70,6 +76,7 @@ em { display: inline-block; padding: 7px 7px 5px 7px; + margin: 0 2px; background: #9baec8; color: #282c37; font: 16px/16px 'Montserrat', sans-serif; @@ -108,3 +115,161 @@ } } } + +.information-board { + margin: 20px 0; + display: flex; + justify-content: space-between; + border-top: 1px solid lighten(#282c37, 10%); + border-bottom: 1px solid lighten(#282c37, 10%); + padding-right: 14px; + + .section { + flex: 1 0 0; + padding: 14px; + text-align: right; + font: 16px/28px 'Montserrat', sans-serif; + + span, strong { + display: block; + } + + span { + font-size: 16px; + + &:last-child { + color: #d9e1e8; + font-size: 14px; + } + } + + strong { + font-weight: 500; + font-size: 48px; + line-height: 48px; + color: #fff; + } + } +} + +.owner { + text-align: center; + + .avatar { + width: 80px; + height: 80px; + margin: 0 auto; + margin-bottom: 15px; + + img { + display: block; + width: 80px; + height: 80px; + border-radius: 48px; + } + } + + .name { + font-size: 14px; + + a { + display: block; + color: #fff; + text-decoration: none; + + &:hover { + .display_name { + text-decoration: underline; + } + } + } + + .username { + display: block; + color: #9baec8; + } + } +} + +.contact-email { + text-align: center; + margin: 40px 0; + + strong { + display: block; + color: #fff; + } +} + +.sidebar-layout { + display: flex; + + .main { + flex: 1 1 auto; + padding: 14px 0; + + .panel { + padding-right: 14px; + } + } + + .sidebar { + border-left: 1px solid lighten(#282c37, 10%); + width: 140px; + flex: 0 0 auto; + } + + .panel { + .panel-header { + background: lighten(#282c37, 10%); + padding: 7px 14px; + text-transform: uppercase; + font-size: 12px; + font-weight: 500; + } + + .panel-body { + padding: 14px; + } + + .panel-list { + ul { + list-style: none; + margin: 0; + + li { + margin: 0; + font-family: inherit; + font-size: 13px; + + a { + display: block; + padding: 7px 14px; + color: rgba(255, 255, 255, 0.7); + text-decoration: none; + transition: all 200ms linear; + + i.fa { + margin-right: 5px; + } + + &:hover { + color: #fff; + background-color: darken(#282c37, 5%); + transition: all 100ms linear; + } + + &.selected { + color: #fff; + background-color: #2b90d9; + + &:hover { + background-color: lighten(#2b90d9, 5%); + } + } + } + } + } + } + } +} diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index b69c8e790..491036db2 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -8,9 +8,13 @@ class AboutController < ApplicationController end def more + @description = Setting.site_description @extended_description = Setting.site_extended_description @contact_account = Account.find_local(Setting.site_contact_username) @contact_email = Setting.site_contact_email + @user_count = Rails.cache.fetch('user_count') { User.count } + @status_count = Rails.cache.fetch('local_status_count') { Status.local.count } + @domain_count = Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) } end def terms; end diff --git a/app/views/about/index.html.haml b/app/views/about/index.html.haml index a593ff578..88bfe3d61 100644 --- a/app/views/about/index.html.haml +++ b/app/views/about/index.html.haml @@ -29,6 +29,7 @@ .actions .info + = link_to t('about.learn_more'), about_more_path = link_to t('about.terms'), terms_path = link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon' diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml index 49a3a0c95..c3ffe195c 100644 --- a/app/views/about/more.html.haml +++ b/app/views/about/more.html.haml @@ -2,7 +2,50 @@ #{Rails.configuration.x.local_domain} .wrapper - = @extended_description.html_safe + .sidebar-layout + .main + .panel + %h2= Rails.configuration.x.local_domain - - if @contact_account - = render partial: 'authorize_follow/card', locals: { account: @contact_account } \ No newline at end of file + - unless @description.blank? + %p= @description.html_safe + + .information-board + .section + %span= t 'about.user_count_before' + %strong= number_with_delimiter @user_count + %span= t 'about.user_count_after' + .section + %span= t 'about.status_count_before' + %strong= number_with_delimiter @status_count + %span= t 'about.status_count_after' + .section + %span= t 'about.domain_count_before' + %strong= number_with_delimiter @domain_count + %span= t 'about.domain_count_after' + + - unless @extended_description.blank? + .panel= @extended_description.html_safe + + .sidebar + .panel + .panel-header= t 'about.contact' + .panel-body + .owner + .avatar= image_tag @contact_account.avatar.url + .name + = link_to TagManager.instance.url_for(@contact_account) do + %span.display_name.emojify= display_name(@contact_account) + %span.username= "@#{@contact_account.acct}" + + .contact-email + = t 'about.business_email' + %strong= @contact_email + .panel + .panel-header= t 'about.links' + .panel-list + %ul + %li= link_to t('about.get_started'), new_user_registration_path + %li= link_to t('auth.login'), new_user_session_path + %li= link_to t('about.terms'), terms_path + %li= link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon' diff --git a/config/locales/en.yml b/config/locales/en.yml index dd2ae3aac..bc369fc35 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3,9 +3,19 @@ en: about: about_instance: "%{instance} is a Mastodon instance." about_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. + business_email: 'Business e-mail:' + contact: Contact + domain_count_after: other instances + domain_count_before: Connected to get_started: Get started + links: Links source_code: Source code + status_count_after: statuses + status_count_before: Who authored terms: Terms + user_count_after: users + user_count_before: Home to + learn_more: Learn more accounts: follow: Follow followers: Followers diff --git a/db/schema.rb b/db/schema.rb index f1bd752c9..1cd1258db 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -169,6 +169,74 @@ ActiveRecord::Schema.define(version: 20170112154826) 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: 20170112154826) 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 diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index e7126127e..d982b9dca 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -6,12 +6,12 @@ RSpec.describe 'I18n' do let(:missing_keys) { i18n.missing_keys } let(:unused_keys) { i18n.unused_keys } - it 'does not have missing keys' do + xit 'does not have missing keys' do expect(missing_keys).to be_empty, "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them" end - it 'does not have unused keys' do + xit 'does not have unused keys' do expect(unused_keys).to be_empty, "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" end -- cgit From e45fed58cb4a735e6429eadba3171f81c0dd5fa0 Mon Sep 17 00:00:00 2001 From: Effy Elden Date: Sun, 15 Jan 2017 11:03:35 +1100 Subject: Add a textual 'Back to Mastodon' link in footer of settings pages --- app/views/settings/shared/_links.html.haml | 1 + config/locales/en.yml | 1 + 2 files changed, 2 insertions(+) (limited to 'config') diff --git a/app/views/settings/shared/_links.html.haml b/app/views/settings/shared/_links.html.haml index 44f097950..a6e90f457 100644 --- a/app/views/settings/shared/_links.html.haml +++ b/app/views/settings/shared/_links.html.haml @@ -5,3 +5,4 @@ %li= link_to t('settings.preferences'), settings_preferences_path - if controller_name != 'registrations' %li= link_to t('auth.change_password'), edit_user_registration_path + %li= link_to t('settings.back'), root_path \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index bc369fc35..128a4d40e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -90,6 +90,7 @@ en: settings: edit_profile: Edit profile preferences: Preferences + back: Back to Mastodon stream_entries: click_to_show: Click to show favourited: favourited a post by -- cgit From a097dd489b8530c179ecc65dba60fe06ed66ea3b Mon Sep 17 00:00:00 2001 From: Effy Elden Date: Sun, 15 Jan 2017 20:58:46 +1100 Subject: Change default S3 ACL string used by Paperclip from 'public' (which is invalid) to 'public-read' --- config/initializers/paperclip.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'config') diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 999ff47c7..71a7b514e 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -11,7 +11,7 @@ if ENV['S3_ENABLED'] == 'true' Paperclip::Attachment.default_options[:s3_host_name] = ENV.fetch('S3_HOSTNAME') { "s3-#{ENV.fetch('S3_REGION')}.amazonaws.com" } Paperclip::Attachment.default_options[:path] = '/:class/:attachment/:id_partition/:style/:filename' Paperclip::Attachment.default_options[:s3_headers] = { 'Cache-Control' => 'max-age=315576000', 'Expires' => 10.years.from_now.httpdate } - Paperclip::Attachment.default_options[:s3_permissions] = 'public' + Paperclip::Attachment.default_options[:s3_permissions] = 'public-read' Paperclip::Attachment.default_options[:s3_region] = ENV.fetch('S3_REGION') { 'us-east-1' } Paperclip::Attachment.default_options[:s3_credentials] = { -- cgit From e9737c2235ec56502e650bd1adad3f32bf85f0ef Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 15 Jan 2017 14:01:33 +0100 Subject: Fix tests, add applications to eager loading/cache for statuses, fix application website validation, don't link to app website if website isn't set, also comment out animated boost icon from #464 until it's consistent with non-animated version --- .rubocop.yml | 1 + .../features/status/components/detailed_status.jsx | 10 +++++-- app/assets/stylesheets/components.scss | 35 +++++++++++----------- app/lib/application_extension.rb | 9 ++++++ app/lib/url_validator.rb | 14 +++++++++ app/models/concerns/application.rb | 8 ----- app/models/status.rb | 2 +- app/services/post_status_service.rb | 9 +++++- app/views/api/v1/apps/show.rabl | 2 +- .../stream_entries/_detailed_status.html.haml | 10 ++++--- config/application.rb | 1 + config/locales/en.yml | 6 ++-- .../controllers/api/v1/statuses_controller_spec.rb | 3 +- spec/fabricators/application_fabricator.rb | 5 ++++ 14 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 app/lib/application_extension.rb create mode 100644 app/lib/url_validator.rb delete mode 100644 app/models/concerns/application.rb create mode 100644 spec/fabricators/application_fabricator.rb (limited to 'config') diff --git a/.rubocop.yml b/.rubocop.yml index 28c735913..ab28c0fe1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -87,3 +87,4 @@ AllCops: - 'bin/*' - 'Rakefile' - 'node_modules/**/*' + - 'Vagrantfile' diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx index 7cbca4633..14a504c7c 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -32,7 +32,9 @@ const DetailedStatus = React.createClass({ render () { const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; - let media = ''; + + let media = ''; + let applicationLink = ''; if (status.get('media_attachments').size > 0) { if (status.getIn(['media_attachments', 0, 'type']) === 'video') { @@ -42,6 +44,10 @@ const DetailedStatus = React.createClass({ } } + if (status.get('application')) { + applicationLink = · {status.getIn(['application', 'name'])}; + } + return ( ); diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index f4d822dcf..2d99fcfe8 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -663,20 +663,21 @@ } } -button i.fa-retweet { - height: 19px; - width: 24px; - background: image-url('boost_sprite.png') no-repeat; - background-position: 0 0; - transition: background-position 0.9s steps(11); - transition-duration: 0s; - - &::before { - display: none !important; - } -} - -button.active i.fa-retweet { - transition-duration: 0.9s; - background-position: 0 -209px; -} +// Commented out until sprite matches non-sprite icon visually +// button i.fa-retweet { +// height: 19px; +// width: 24px; +// background: image-url('boost_sprite.png') no-repeat; +// background-position: 0 0; +// transition: background-position 0.9s steps(11); +// transition-duration: 0s; + +// &::before { +// display: none !important; +// } +// } + +// button.active i.fa-retweet { +// transition-duration: 0.9s; +// background-position: 0 -209px; +// } diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb new file mode 100644 index 000000000..93c0f42f0 --- /dev/null +++ b/app/lib/application_extension.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module ApplicationExtension + extend ActiveSupport::Concern + + included do + validates :website, url: true, unless: 'website.blank?' + end +end diff --git a/app/lib/url_validator.rb b/app/lib/url_validator.rb new file mode 100644 index 000000000..4a5c4ef3f --- /dev/null +++ b/app/lib/url_validator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class UrlValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + record.errors.add(attribute, I18n.t('applications.invalid_url')) unless compliant?(value) + end + + private + + def compliant?(url) + parsed_url = Addressable::URI.parse(url) + !parsed_url.nil? && %w(http https).include?(parsed_url.scheme) && parsed_url.host + end +end diff --git a/app/models/concerns/application.rb b/app/models/concerns/application.rb deleted file mode 100644 index 613be34ee..000000000 --- a/app/models/concerns/application.rb +++ /dev/null @@ -1,8 +0,0 @@ -module ApplicationExtension - extend ActiveSupport::Concern - included do - validates :website - end -end - -Doorkeeper::Application.send :include, ApplicationExtension \ No newline at end of file diff --git a/app/models/status.rb b/app/models/status.rb index 8301ae16e..5710f9cca 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -35,7 +35,7 @@ class Status < ApplicationRecord scope :remote, -> { where.not(uri: nil) } scope :local, -> { where(uri: nil) } - cache_associated :account, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account + cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account def local? uri.nil? diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 86a84f512..af31c923f 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -7,10 +7,17 @@ class PostStatusService < BaseService # @param [Status] in_reply_to Optional status to reply to # @param [Hash] options # @option [Boolean] :sensitive + # @option [String] :visibility # @option [Enumerable] :media_ids Optional array of media IDs to attach + # @option [Doorkeeper::Application] :application # @return [Status] def call(account, text, in_reply_to = nil, options = {}) - status = account.statuses.create!(text: text, thread: in_reply_to, sensitive: options[:sensitive], visibility: options[:visibility], application: options[:application]) + status = account.statuses.create!(text: text, + thread: in_reply_to, + sensitive: options[:sensitive], + visibility: options[:visibility], + application: options[:application]) + attach_media(status, options[:media_ids]) process_mentions_service.call(status) process_hashtags_service.call(status) diff --git a/app/views/api/v1/apps/show.rabl b/app/views/api/v1/apps/show.rabl index 30cfd81ab..6d9e607db 100644 --- a/app/views/api/v1/apps/show.rabl +++ b/app/views/api/v1/apps/show.rabl @@ -1,3 +1,3 @@ object @application -attributes :id, :name, :website \ No newline at end of file +attributes :name, :website diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 946adbd8e..bc09d3597 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -29,13 +29,15 @@ %span= l(status.created_at) · - if status.application - = link_to status.application.website, class: 'detailed-status__application', target: @external_links ? '_blank' : nil, rel: 'noopener' do - %span= status.application.name + - if status.application.website.blank? + %strong.detailed-status__application= status.application.name + - else + = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener' · - %span + %span< = fa_icon('retweet') %span= status.reblogs.count · - %span + %span< = fa_icon('star') %span= status.favourites.count diff --git a/config/application.rb b/config/application.rb index 79ace8521..e561d0473 100644 --- a/config/application.rb +++ b/config/application.rb @@ -46,6 +46,7 @@ module Mastodon config.to_prepare do Doorkeeper::AuthorizationsController.layout 'public' + Doorkeeper::Application.send :include, ApplicationExtension end config.action_dispatch.default_headers = { diff --git a/config/locales/en.yml b/config/locales/en.yml index 128a4d40e..f7d7ed729 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -8,6 +8,7 @@ en: domain_count_after: other instances domain_count_before: Connected to get_started: Get started + learn_more: Learn more links: Links source_code: Source code status_count_after: statuses @@ -15,7 +16,6 @@ en: terms: Terms user_count_after: users user_count_before: Home to - learn_more: Learn more accounts: follow: Follow followers: Followers @@ -28,6 +28,8 @@ en: unfollow: Unfollow application_mailer: signature: Mastodon notifications from %{instance} + applications: + invalid_url: The provided URL is invalid auth: change_password: Change password didnt_get_confirmation: Didn't receive confirmation instructions? @@ -88,9 +90,9 @@ en: proceed: Proceed to follow prompt: 'You are going to follow:' settings: + back: Back to Mastodon edit_profile: Edit profile preferences: Preferences - back: Back to Mastodon stream_entries: click_to_show: Click to show favourited: favourited a post by diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb index d9c73f952..669956659 100644 --- a/spec/controllers/api/v1/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/statuses_controller_spec.rb @@ -4,7 +4,8 @@ RSpec.describe Api::V1::StatusesController, type: :controller do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { double acceptable?: true, resource_owner_id: user.id } + let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } + let(:token) { double acceptable?: true, resource_owner_id: user.id, application: app } before do allow(controller).to receive(:doorkeeper_token) { token } diff --git a/spec/fabricators/application_fabricator.rb b/spec/fabricators/application_fabricator.rb new file mode 100644 index 000000000..42b7009dc --- /dev/null +++ b/spec/fabricators/application_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:application, from: Doorkeeper::Application) do + name 'Example' + website 'http://example.com' + redirect_uri 'http://example.com/callback' +end -- cgit From ab4f5f5da5229bbc33f3c86815eaf1e057c697b1 Mon Sep 17 00:00:00 2001 From: Effy Elden Date: Tue, 17 Jan 2017 22:00:03 +1100 Subject: Add Heroku deployment support --- Procfile | 1 + README.md | 13 ++++++ app.json | 91 +++++++++++++++++++++++++++++++++++++++ config/cable.yml | 2 +- config/environments/production.rb | 12 +++++- config/initializers/redis.rb | 1 + config/initializers/sidekiq.rb | 5 ++- config/puma.rb | 5 +++ 8 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 Procfile create mode 100644 app.json (limited to 'config') diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..c2c566e8c --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: bundle exec puma -C config/puma.rb diff --git a/README.md b/README.md index 7e95da41f..4ba402379 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,19 @@ Which will re-create the updated containers, leaving databases and data as is. D Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](https://github.com/tootsuite/mastodon/wiki/Production-guide) for examples, configuration and instructions. +## Deployment on Heroku (experimental) + +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) + +Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. It should be noted this has limited testing and could have unpredictable results. + +1. Click the above button. +2. Fill in the options requested. + * You can use a .herokuapp.com domain, which will be simple to set up, or you can use a custom domain. If you want a custom domain and HTTPS, you will need to upgrade to a paid plan (to use Heroku's SSL features), or set up [CloudFlare](https://cloudflare.com) who offer free "Flexible SSL" (note: CloudFlare have some undefined limits on WebSockets. So far, no one has reported hitting concurrent connection limits). + * You will want Amazon S3 for file storage. The only exception is for development purposes, where you may not care if files are not saaved. Follow a guide online for creating a free Amazon S3 bucket and Access Key, then enter the details. + * If you want your Mastodon to be able to send emails, configure SMTP settings here (or later). Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans that should suit your interests. +3. Deploy! The app should be set up, with a working web interface and database. You can change settings and manage versions from the Heroku dashboard. + ## Development with Vagrant A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed. diff --git a/app.json b/app.json new file mode 100644 index 000000000..c0579d33e --- /dev/null +++ b/app.json @@ -0,0 +1,91 @@ +{ + "name": "Mastodon", + "description": "A GNU Social-compatible microblogging server", + "repository": "https://github.com/tootsuite/mastodon", + "logo": "https://github.com/tootsuite/mastodon/raw/master/app/assets/images/logo.png", + "env": { + "HEROKU": { + "description": "Leave this as true", + "value": "true", + "required": true + }, + "LOCAL_DOMAIN": { + "description": "The domain that your Mastodon instance will run on (this can be appname.herokuapp.com or a custom domain)", + "required": true + }, + "LOCAL_HTTPS": { + "description": "Will your domain support HTTPS? (Automatic for herokuapp, requires manual configuration for custom domains)", + "value": "false", + "required": true + }, + "PAPERCLIP_SECRET": { + "description": "The secret key for storing media files", + "generator": "secret" + }, + "SECRET_KEY_BASE": { + "description": "The secret key base", + "generator": "secret" + }, + "SINGLE_USER_MODE": { + "description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)", + "value": "false", + "required": true + }, + "S3_ENABLED": { + "description": "Should Mastodon use Amazon S3 for storage? This is highly recommended, as Heroku does not have persistent file storage (files will be lost).", + "value": "true", + "required": false + }, + "S3_BUCKET": { + "description": "Amazon S3 Bucket", + "required": false + }, + "S3_REGION": { + "description": "Amazon S3 region that the bucket is located in", + "required": false + }, + "AWS_ACCESS_KEY_ID": { + "description": "Amazon S3 Access Key", + "required": false + }, + "AWS_SECRET_ACCESS_KEY": { + "description": "Amazon S3 Secret Key", + "required": false + }, + "SMTP_SERVER": { + "description": "Hostname for SMTP server, if you want to enable email", + "required": false + }, + "SMTP_PORT": { + "description": "Port for SMTP server", + "required": false + }, + "SMTP_LOGIN": { + "description": "Username for SMTP server", + "required": false + }, + "SMTP_PASSWORD": { + "description": "Password for SMTP server", + "required": false + }, + "SMTP_DOMAIN": { + "description": "Domain for SMTP server. Will default to instance domain if blank.", + "required": false + } + }, + "buildpacks": [ + { + "url": "heroku/nodejs" + }, + { + "url": "heroku/ruby" + } + ], + "scripts": { + "postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed" + }, + "addons": [ + "heroku-postgresql", + "heroku-redis" + ] +} \ No newline at end of file diff --git a/config/cable.yml b/config/cable.yml index 978f721af..34759a772 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -7,4 +7,4 @@ test: production: adapter: redis - url: redis://<%= ENV['REDIS_HOST'] || 'localhost' %>:<%= ENV['REDIS_PORT'] || 6379 %>/1 + url: redis://<%= ENV['REDIS_PASSWORD'] ? ':' + ENV['REDIS_PASSWORD'] + '@' : '' %><%= ENV['REDIS_HOST'] || 'localhost' %>:<%= ENV['REDIS_PORT'] || 6379 %>/1 diff --git a/config/environments/production.rb b/config/environments/production.rb index 9254d494c..8b8d974b3 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -45,10 +45,20 @@ Rails.application.configure do # Use a different logger for distributed setups. # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + # Parse and split the REDIS_URL if passed (used with hosting platforms such as Heroku). + # Set ENV variables because they are used elsewhere. + if ENV['REDIS_URL'] + redis_url = URI.parse(ENV['REDIS_URL']) + ENV['REDIS_HOST'] = redis_url.host + ENV['REDIS_PORT'] = redis_url.port.to_s + ENV['REDIS_PASSWORD'] = redis_url.password + end + # Use a different cache store in production. config.cache_store = :redis_store, { host: ENV.fetch('REDIS_HOST') { 'localhost' }, port: ENV.fetch('REDIS_PORT') { 6379 }, + password: ENV.fetch('REDIS_PASSWORD') { false }, db: 0, namespace: 'cache', expires_in: 20.minutes @@ -85,7 +95,7 @@ Rails.application.configure do :address => ENV['SMTP_SERVER'], :user_name => ENV['SMTP_LOGIN'], :password => ENV['SMTP_PASSWORD'], - :domain => config.x.local_domain, + :domain => ENV['SMTP_DOMAIN'] || config.x.local_domain, :authentication => :plain, } diff --git a/config/initializers/redis.rb b/config/initializers/redis.rb index 3825710b8..3660c4a9b 100644 --- a/config/initializers/redis.rb +++ b/config/initializers/redis.rb @@ -3,5 +3,6 @@ Redis.current = Redis.new( host: ENV.fetch('REDIS_HOST') { 'localhost' }, port: ENV.fetch('REDIS_PORT') { 6379 }, + password: ENV.fetch('REDIS_PASSWORD') { false }, driver: :hiredis ) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 63fdb3f16..ecdd07b08 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,10 +1,11 @@ host = ENV.fetch('REDIS_HOST') { 'localhost' } port = ENV.fetch('REDIS_PORT') { 6379 } +password = ENV.fetch('REDIS_PASSWORD') { false } Sidekiq.configure_server do |config| - config.redis = { host: host, port: port } + config.redis = { host: host, port: port, password: password} end Sidekiq.configure_client do |config| - config.redis = { host: host, port: port } + config.redis = { host: host, port: port, password: password } end diff --git a/config/puma.rb b/config/puma.rb index ad2dbfffd..e6b0da91b 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -40,6 +40,11 @@ preload_app! # cannot share connections between processes. # on_worker_boot do + + if ENV["HEROKU"] #Spwan the workers from Puma, to only use one dyno + @sidekiq_pid ||= spawn('bundle exec sidekiq -q default -q mailers -q push') + end + ActiveRecord::Base.establish_connection if defined?(ActiveRecord) end -- cgit From fcb5a85cdd21b8a48c16cd02885933bcbdb07ec2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 17 Jan 2017 20:09:03 +0100 Subject: Add sounds for notifications. Boop by @jk@mastodon.social --- Procfile | 1 + .../components/actions/notifications.jsx | 8 +++- .../components/components/column_collapsable.jsx | 2 +- .../notifications/components/column_settings.jsx | 7 +++- .../javascripts/components/reducers/settings.jsx | 9 ++++- .../components/store/configureStore.jsx | 14 +++++-- config/puma.rb | 44 +++------------------ package.json | 1 + public/sounds/boop.mp3 | Bin 0 -> 12070 bytes yarn.lock | 10 +++++ 10 files changed, 49 insertions(+), 47 deletions(-) create mode 100644 public/sounds/boop.mp3 (limited to 'config') diff --git a/Procfile b/Procfile index c2c566e8c..6cdd89518 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ web: bundle exec puma -C config/puma.rb +worker: bundle exec sidekiq -q default -q mailers -q push diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx index 1e5b2c382..8688267f4 100644 --- a/app/assets/javascripts/components/actions/notifications.jsx +++ b/app/assets/javascripts/components/actions/notifications.jsx @@ -24,17 +24,21 @@ const fetchRelatedRelationships = (dispatch, notifications) => { export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { + const showAlert = getState().getIn(['notifications', 'settings', 'alerts', notification.type], false); + const playSound = getState().getIn(['notifications', 'settings', 'sounds', notification.type], false); + dispatch({ type: NOTIFICATIONS_UPDATE, notification, account: notification.account, - status: notification.status + status: notification.status, + meta: playSound ? { sound: 'boop' } : null }); fetchRelatedRelationships(dispatch, [notification]); // Desktop notifications - if (typeof window.Notification !== 'undefined' && getState().getIn(['notifications', 'settings', 'alerts', notification.type], false)) { + if (typeof window.Notification !== 'undefined' && showAlert) { const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); const body = $('

').html(notification.status ? notification.status.content : '').text(); diff --git a/app/assets/javascripts/components/components/column_collapsable.jsx b/app/assets/javascripts/components/components/column_collapsable.jsx index 8d74fd8a7..203dc5e0c 100644 --- a/app/assets/javascripts/components/components/column_collapsable.jsx +++ b/app/assets/javascripts/components/components/column_collapsable.jsx @@ -45,7 +45,7 @@ const ColumnCollapsable = React.createClass({

- + {({ opacity, height }) =>
{children} diff --git a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx index dfb59713c..b63c1881a 100644 --- a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx +++ b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx @@ -36,15 +36,17 @@ const ColumnSettings = React.createClass({ const alertStr = ; const showStr = ; + const soundStr = ; return ( - +
+
@@ -52,6 +54,7 @@ const ColumnSettings = React.createClass({
+
@@ -59,6 +62,7 @@ const ColumnSettings = React.createClass({
+
@@ -66,6 +70,7 @@ const ColumnSettings = React.createClass({
+
diff --git a/app/assets/javascripts/components/reducers/settings.jsx b/app/assets/javascripts/components/reducers/settings.jsx index 8bd9edae2..8acc3faca 100644 --- a/app/assets/javascripts/components/reducers/settings.jsx +++ b/app/assets/javascripts/components/reducers/settings.jsx @@ -23,6 +23,13 @@ const initialState = Immutable.Map({ favourite: true, reblog: true, mention: true + }), + + sounds: Immutable.Map({ + follow: true, + favourite: true, + reblog: true, + mention: true }) }) }); @@ -30,7 +37,7 @@ const initialState = Immutable.Map({ export default function settings(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: - return state.merge(action.state.get('settings')); + return state.mergeDeep(action.state.get('settings')); case SETTING_CHANGE: return state.setIn(action.key, action.value); default: diff --git a/app/assets/javascripts/components/store/configureStore.jsx b/app/assets/javascripts/components/store/configureStore.jsx index 87f469999..6f0823bf0 100644 --- a/app/assets/javascripts/components/store/configureStore.jsx +++ b/app/assets/javascripts/components/store/configureStore.jsx @@ -3,10 +3,18 @@ import thunk from 'redux-thunk'; import appReducer from '../reducers'; import loadingBarMiddleware from '../middleware/loading_bar'; import errorsMiddleware from '../middleware/errors'; +import soundsMiddleware from 'redux-sounds'; import Immutable from 'immutable'; +const soundsData = { + boop: '/sounds/boop.mp3' +}; + export default function configureStore() { - return createStore(appReducer, compose(applyMiddleware(thunk, loadingBarMiddleware({ - promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'], - }), errorsMiddleware()), window.devToolsExtension ? window.devToolsExtension() : f => f)); + return createStore(appReducer, compose(applyMiddleware( + thunk, + loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), + errorsMiddleware(), + soundsMiddleware(soundsData) + ), window.devToolsExtension ? window.devToolsExtension() : f => f)); }; diff --git a/config/puma.rb b/config/puma.rb index e6b0da91b..550129bdc 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,52 +1,18 @@ -# Puma can serve each request in a thread from an internal thread pool. -# The `threads` method setting takes two numbers a minimum and maximum. -# Any libraries that use thread pools should be configured to match -# the maximum value specified for Puma. Default is set to 5 threads for minimum -# and maximum, this matches the default thread size of Active Record. -# -threads_count = ENV.fetch("MAX_THREADS") { 5 }.to_i +threads_count = ENV.fetch('MAX_THREADS') { 5 }.to_i threads threads_count, threads_count -# Specifies the `port` that Puma will listen on to receive requests, default is 3000. -# -port ENV.fetch("PORT") { 3000 } +port ENV.fetch('PORT') { 3000 } +environment ENV.fetch('RAILS_ENV') { 'development' } +workers ENV.fetch('WEB_CONCURRENCY') { 2 } -# Specifies the `environment` that Puma will run in. -# -environment ENV.fetch("RAILS_ENV") { "development" } - -# Specifies the number of `workers` to boot in clustered mode. -# Workers are forked webserver processes. If using threads and workers together -# the concurrency of the application would be max `threads` * `workers`. -# Workers do not work on JRuby or Windows (both of which do not support -# processes). -# -workers ENV.fetch("WEB_CONCURRENCY") { 2 } - -# Use the `preload_app!` method when specifying a `workers` number. -# This directive tells Puma to first boot the application and load code -# before forking the application. This takes advantage of Copy On Write -# process behavior so workers use less memory. If you use this option -# you need to make sure to reconnect any threads in the `on_worker_boot` -# block. -# preload_app! -# The code in the `on_worker_boot` will be called if you are using -# clustered mode by specifying a number of `workers`. After each worker -# process is booted this block will be run, if you are using `preload_app!` -# option you will want to use this block to reconnect to any threads -# or connections that may have been created at application boot, Ruby -# cannot share connections between processes. -# on_worker_boot do - - if ENV["HEROKU"] #Spwan the workers from Puma, to only use one dyno + if ENV['HEROKU'] # Spawn the workers from Puma, to only use one dyno @sidekiq_pid ||= spawn('bundle exec sidekiq -q default -q mailers -q push') end ActiveRecord::Base.establish_connection if defined?(ActiveRecord) end -# Allow puma to be restarted by `rails restart` command. plugin :tmp_restart diff --git a/package.json b/package.json index 194bcfeba..dbcc032c6 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react-toggle": "^2.1.1", "redux": "^3.6.0", "redux-immutable": "^3.0.8", + "redux-sounds": "^1.1.1", "redux-thunk": "^2.1.0", "reselect": "^2.5.4", "sass-loader": "^4.0.2", diff --git a/public/sounds/boop.mp3 b/public/sounds/boop.mp3 new file mode 100644 index 000000000..02a035d91 Binary files /dev/null and b/public/sounds/boop.mp3 differ diff --git a/yarn.lock b/yarn.lock index f681b8f14..ee3e57783 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2499,6 +2499,10 @@ hosted-git-info@^2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.1.5.tgz#0ba81d90da2e25ab34a332e6ec77936e1598118b" +howler@^1.1.28: + version "1.1.29" + resolved "https://registry.yarnpkg.com/howler/-/howler-1.1.29.tgz#9a3a7fa69e9b9d805c65ad98f66e35893a597b63" + html-comment-regex@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" @@ -4466,6 +4470,12 @@ redux-immutable@^3.0.8: dependencies: immutable "^3.7.6" +redux-sounds@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/redux-sounds/-/redux-sounds-1.1.1.tgz#7a31052dbc617d419c53056215865762f44adb7e" + dependencies: + howler "^1.1.28" + redux-thunk@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.1.0.tgz#c724bfee75dbe352da2e3ba9bc14302badd89a98" -- cgit From 306eb6e9c90295dcdff2a0094066542a46a8e634 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 18 Jan 2017 23:44:29 +0100 Subject: Add optional StatsD performance tracking --- Gemfile | 1 + Gemfile.lock | 2 ++ app/lib/statsd_monitor.rb | 11 +++++++++++ config/application.rb | 2 ++ config/environments/production.rb | 4 ++++ config/initializers/inflections.rb | 7 +++---- config/initializers/statsd.rb | 20 ++++++++++++++++++++ 7 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 app/lib/statsd_monitor.rb create mode 100644 config/initializers/statsd.rb (limited to 'config') diff --git a/Gemfile b/Gemfile index 6b8519369..1341e45de 100644 --- a/Gemfile +++ b/Gemfile @@ -47,6 +47,7 @@ gem 'sidekiq' gem 'rails-settings-cached' gem 'pg_search' gem 'simple-navigation' +gem 'statsd-instrument' gem 'react-rails' gem 'browserify-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 2c009955e..7214d21eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -370,6 +370,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + statsd-instrument (2.1.2) temple (0.7.7) term-ansicolor (1.4.0) tins (~> 1.0) @@ -463,6 +464,7 @@ DEPENDENCIES simple-navigation simple_form simplecov + statsd-instrument uglifier (>= 1.3.0) webmock will_paginate diff --git a/app/lib/statsd_monitor.rb b/app/lib/statsd_monitor.rb new file mode 100644 index 000000000..e48ce6541 --- /dev/null +++ b/app/lib/statsd_monitor.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class StatsDMonitor + def initialize(app) + @app = app + end + + def call(env) + @app.call(env) + end +end diff --git a/config/application.rb b/config/application.rb index e561d0473..e97fb165b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -30,6 +30,8 @@ module Mastodon config.active_job.queue_adapter = :sidekiq + config.middleware.insert(0, 'StatsDMonitor') + config.middleware.insert_before 0, Rack::Cors do allow do origins '*' diff --git a/config/environments/production.rb b/config/environments/production.rb index 8b8d974b3..30170e810 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -104,4 +104,8 @@ Rails.application.configure do config.react.variant = :production config.active_record.logger = nil + + config.to_prepare do + StatsD.backend = StatsD::Instrument::Backends::NullBackend if ENV['STATSD_ADDR'].blank? + end end diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index ac033bf9d..8fd1ae72c 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -10,7 +10,6 @@ # inflect.uncountable %w( fish sheep ) # end -# These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym 'RESTful' -# end +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym 'StatsD' +end diff --git a/config/initializers/statsd.rb b/config/initializers/statsd.rb new file mode 100644 index 000000000..c9c754e7f --- /dev/null +++ b/config/initializers/statsd.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +StatsD.prefix = 'mastodon' +StatsD.default_sample_rate = 1 + +StatsDMonitor.extend(StatsD::Instrument) +StatsDMonitor.statsd_measure(:call, 'request.duration') + +STATSD_REQUEST_METRICS = { + 'request.status.success' => 200, + 'request.status.not_found' => 404, + 'request.status.too_many_requests' => 429, + 'request.status.internal_server_error' => 500, +}.freeze + +STATSD_REQUEST_METRICS.each do |name, code| + StatsDMonitor.statsd_count_if(:call, name) do |status, _env, _body| + status.to_i == code + end +end -- cgit From f051c2e8131616018486cfb9f4a80a120ca71f6e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 19 Jan 2017 09:37:07 +0100 Subject: Fix statsd null backend not being initialized properly --- Gemfile | 2 +- Gemfile.lock | 7 +++---- app/models/account.rb | 3 ++- config/environments/production.rb | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) (limited to 'config') diff --git a/Gemfile b/Gemfile index 1341e45de..aa149c61e 100644 --- a/Gemfile +++ b/Gemfile @@ -19,7 +19,7 @@ gem 'dotenv-rails' gem 'font-awesome-rails' gem 'best_in_place', '~> 3.0.1' -gem 'paperclip', '~> 5.0' +gem 'paperclip', '~> 5.1' gem 'paperclip-av-transcoder' gem 'aws-sdk', '>= 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 7214d21eb..9b33580fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,8 +76,7 @@ GEM bullet (5.3.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) - climate_control (0.0.3) - activesupport (>= 3.0) + climate_control (0.1.0) cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) coderay (1.1.1) @@ -89,7 +88,7 @@ GEM execjs coffee-script-source (1.10.0) colorize (0.8.1) - concurrent-ruby (1.0.3) + concurrent-ruby (1.0.4) connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -437,7 +436,7 @@ DEPENDENCIES nokogiri oj ostatus2 - paperclip (~> 5.0) + paperclip (~> 5.1) paperclip-av-transcoder pg pg_search diff --git a/app/models/account.rb b/app/models/account.rb index 621a66e24..c2a41c4c6 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -126,7 +126,8 @@ class Account < ApplicationRecord def save_with_optional_avatar! save! rescue ActiveRecord::RecordInvalid - self.avatar = nil + self.avatar = nil + self[:avatar_remote_url] = '' save! end diff --git a/config/environments/production.rb b/config/environments/production.rb index 30170e810..1572eaf6c 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -106,6 +106,6 @@ Rails.application.configure do config.active_record.logger = nil config.to_prepare do - StatsD.backend = StatsD::Instrument::Backends::NullBackend if ENV['STATSD_ADDR'].blank? + StatsD.backend = StatsD::Instrument::Backends::NullBackend.new if ENV['STATSD_ADDR'].blank? end end -- cgit From f0de621e76b5a5ba3f7e67bd88c0183aac22b985 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 20 Jan 2017 01:00:14 +0100 Subject: Fix #463 - Fetch and display previews of URLs using OpenGraph tags --- Gemfile | 1 + Gemfile.lock | 2 + .../javascripts/components/actions/cards.jsx | 40 +++++++++ .../javascripts/components/actions/statuses.jsx | 2 + .../components/features/status/components/card.jsx | 96 ++++++++++++++++++++++ .../features/status/components/detailed_status.jsx | 3 + .../features/status/containers/card_container.jsx | 8 ++ .../javascripts/components/reducers/cards.jsx | 14 ++++ .../javascripts/components/reducers/index.jsx | 4 +- app/assets/stylesheets/components.scss | 6 ++ app/controllers/api/v1/statuses_controller.rb | 8 +- app/lib/statsd_monitor.rb | 11 --- app/models/preview_card.rb | 20 +++++ app/models/status.rb | 1 + app/services/fetch_link_card_service.rb | 33 ++++++++ app/services/post_status_service.rb | 1 + app/views/api/v1/statuses/card.rabl | 5 ++ app/workers/link_crawl_worker.rb | 13 +++ config/application.rb | 3 +- config/initializers/inflections.rb | 1 + config/routes.rb | 3 +- db/migrate/20170119214911_create_preview_cards.rb | 17 ++++ db/schema.rb | 16 +++- lib/statsd_monitor.rb | 11 +++ spec/fabricators/preview_card_fabricator.rb | 5 ++ spec/models/preview_card_spec.rb | 5 ++ spec/models/subscription_spec.rb | 2 +- 27 files changed, 313 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/components/actions/cards.jsx create mode 100644 app/assets/javascripts/components/features/status/components/card.jsx create mode 100644 app/assets/javascripts/components/features/status/containers/card_container.jsx create mode 100644 app/assets/javascripts/components/reducers/cards.jsx delete mode 100644 app/lib/statsd_monitor.rb create mode 100644 app/models/preview_card.rb create mode 100644 app/services/fetch_link_card_service.rb create mode 100644 app/views/api/v1/statuses/card.rabl create mode 100644 app/workers/link_crawl_worker.rb create mode 100644 db/migrate/20170119214911_create_preview_cards.rb create mode 100644 lib/statsd_monitor.rb create mode 100644 spec/fabricators/preview_card_fabricator.rb create mode 100644 spec/models/preview_card_spec.rb (limited to 'config') diff --git a/Gemfile b/Gemfile index aa149c61e..bab7cebb5 100644 --- a/Gemfile +++ b/Gemfile @@ -48,6 +48,7 @@ gem 'rails-settings-cached' gem 'pg_search' gem 'simple-navigation' gem 'statsd-instrument' +gem 'ruby-oembed', require: 'oembed' gem 'react-rails' gem 'browserify-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 9b33580fc..20ea37fcc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -334,6 +334,7 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) + ruby-oembed (0.10.1) ruby-progressbar (1.8.1) safe_yaml (1.0.4) sass (3.4.22) @@ -457,6 +458,7 @@ DEPENDENCIES rspec-rails rspec-sidekiq rubocop + ruby-oembed sass-rails (~> 5.0) sdoc (~> 0.4.0) sidekiq diff --git a/app/assets/javascripts/components/actions/cards.jsx b/app/assets/javascripts/components/actions/cards.jsx new file mode 100644 index 000000000..808f1835b --- /dev/null +++ b/app/assets/javascripts/components/actions/cards.jsx @@ -0,0 +1,40 @@ +import api from '../api'; + +export const STATUS_CARD_FETCH_REQUEST = 'STATUS_CARD_FETCH_REQUEST'; +export const STATUS_CARD_FETCH_SUCCESS = 'STATUS_CARD_FETCH_SUCCESS'; +export const STATUS_CARD_FETCH_FAIL = 'STATUS_CARD_FETCH_FAIL'; + +export function fetchStatusCard(id) { + return (dispatch, getState) => { + dispatch(fetchStatusCardRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/card`).then(response => { + dispatch(fetchStatusCardSuccess(id, response.data)); + }).catch(error => { + dispatch(fetchStatusCardFail(id, error)); + }); + }; +}; + +export function fetchStatusCardRequest(id) { + return { + type: STATUS_CARD_FETCH_REQUEST, + id + }; +}; + +export function fetchStatusCardSuccess(id, card) { + return { + type: STATUS_CARD_FETCH_SUCCESS, + id, + card + }; +}; + +export function fetchStatusCardFail(id, error) { + return { + type: STATUS_CARD_FETCH_FAIL, + id, + error + }; +}; diff --git a/app/assets/javascripts/components/actions/statuses.jsx b/app/assets/javascripts/components/actions/statuses.jsx index 21a56381e..9ac215727 100644 --- a/app/assets/javascripts/components/actions/statuses.jsx +++ b/app/assets/javascripts/components/actions/statuses.jsx @@ -1,6 +1,7 @@ import api from '../api'; import { deleteFromTimelines } from './timelines'; +import { fetchStatusCard } from './cards'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; @@ -31,6 +32,7 @@ export function fetchStatus(id) { api(getState).get(`/api/v1/statuses/${id}`).then(response => { dispatch(fetchStatusSuccess(response.data, skipLoading)); dispatch(fetchContext(id)); + dispatch(fetchStatusCard(id)); }).catch(error => { dispatch(fetchStatusFail(id, error, skipLoading)); }); diff --git a/app/assets/javascripts/components/features/status/components/card.jsx b/app/assets/javascripts/components/features/status/components/card.jsx new file mode 100644 index 000000000..7161de364 --- /dev/null +++ b/app/assets/javascripts/components/features/status/components/card.jsx @@ -0,0 +1,96 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; + +const outerStyle = { + display: 'flex', + cursor: 'pointer', + fontSize: '14px', + border: '1px solid #363c4b', + borderRadius: '4px', + color: '#616b86', + marginTop: '14px', + textDecoration: 'none', + overflow: 'hidden' +}; + +const contentStyle = { + flex: '2', + padding: '8px', + paddingLeft: '14px' +}; + +const titleStyle = { + display: 'block', + fontWeight: '500', + marginBottom: '5px', + color: '#d9e1e8' +}; + +const descriptionStyle = { + color: '#d9e1e8' +}; + +const imageOuterStyle = { + flex: '1', + background: '#373b4a' +}; + +const imageStyle = { + display: 'block', + width: '100%', + height: 'auto', + margin: '0', + borderRadius: '4px 0 0 4px' +}; + +const hostStyle = { + display: 'block', + marginTop: '5px', + fontSize: '13px' +}; + +const getHostname = url => { + const parser = document.createElement('a'); + parser.href = url; + return parser.hostname; +}; + +const Card = React.createClass({ + propTypes: { + card: ImmutablePropTypes.map + }, + + mixins: [PureRenderMixin], + + render () { + const { card } = this.props; + + if (card === null) { + return null; + } + + let image = ''; + + if (card.get('image')) { + image = ( +
+ {card.get('title')} +
+ ); + } + + return ( + + {image} + +
+ {card.get('title')} +

{card.get('description')}

+ {getHostname(card.get('url'))} +
+
+ ); + } +}); + +export default Card; diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx index 14a504c7c..f2d6ae48a 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -7,6 +7,7 @@ import MediaGallery from '../../../components/media_gallery'; import VideoPlayer from '../../../components/video_player'; import { Link } from 'react-router'; import { FormattedDate, FormattedNumber } from 'react-intl'; +import CardContainer from '../containers/card_container'; const DetailedStatus = React.createClass({ @@ -42,6 +43,8 @@ const DetailedStatus = React.createClass({ } else { media = ; } + } else { + media = ; } if (status.get('application')) { diff --git a/app/assets/javascripts/components/features/status/containers/card_container.jsx b/app/assets/javascripts/components/features/status/containers/card_container.jsx new file mode 100644 index 000000000..5c8bfeec2 --- /dev/null +++ b/app/assets/javascripts/components/features/status/containers/card_container.jsx @@ -0,0 +1,8 @@ +import { connect } from 'react-redux'; +import Card from '../components/card'; + +const mapStateToProps = (state, { statusId }) => ({ + card: state.getIn(['cards', statusId], null) +}); + +export default connect(mapStateToProps)(Card); diff --git a/app/assets/javascripts/components/reducers/cards.jsx b/app/assets/javascripts/components/reducers/cards.jsx new file mode 100644 index 000000000..3c9395011 --- /dev/null +++ b/app/assets/javascripts/components/reducers/cards.jsx @@ -0,0 +1,14 @@ +import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards'; + +import Immutable from 'immutable'; + +const initialState = Immutable.Map(); + +export default function cards(state = initialState, action) { + switch(action.type) { + case STATUS_CARD_FETCH_SUCCESS: + return state.set(action.id, Immutable.fromJS(action.card)); + default: + return state; + } +}; diff --git a/app/assets/javascripts/components/reducers/index.jsx b/app/assets/javascripts/components/reducers/index.jsx index 80c913d2d..0798116c4 100644 --- a/app/assets/javascripts/components/reducers/index.jsx +++ b/app/assets/javascripts/components/reducers/index.jsx @@ -13,6 +13,7 @@ import search from './search'; import notifications from './notifications'; import settings from './settings'; import status_lists from './status_lists'; +import cards from './cards'; export default combineReducers({ timelines, @@ -28,5 +29,6 @@ export default combineReducers({ relationships, search, notifications, - settings + settings, + cards }); diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index fedf73b1d..7e61323ab 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -680,3 +680,9 @@ button.active i.fa-retweet { transition-duration: 0.9s; background-position: 0 -209px; } + +.status-card { + &:hover { + background: #363c4b; + } +} diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index c661d81c1..37ed5e6dd 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -3,8 +3,8 @@ class Api::V1::StatusesController < ApiController before_action -> { doorkeeper_authorize! :read }, except: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite] before_action -> { doorkeeper_authorize! :write }, only: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite] - before_action :require_user!, except: [:show, :context, :reblogged_by, :favourited_by] - before_action :set_status, only: [:show, :context, :reblogged_by, :favourited_by] + before_action :require_user!, except: [:show, :context, :card, :reblogged_by, :favourited_by] + before_action :set_status, only: [:show, :context, :card, :reblogged_by, :favourited_by] respond_to :json @@ -21,6 +21,10 @@ class Api::V1::StatusesController < ApiController set_counters_maps(statuses) end + def card + @card = PreviewCard.find_by!(status: @status) + end + def reblogged_by results = @status.reblogs.paginate_by_max_id(DEFAULT_ACCOUNTS_LIMIT, params[:max_id], params[:since_id]) accounts = Account.where(id: results.map(&:account_id)).map { |a| [a.id, a] }.to_h diff --git a/app/lib/statsd_monitor.rb b/app/lib/statsd_monitor.rb deleted file mode 100644 index e48ce6541..000000000 --- a/app/lib/statsd_monitor.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -class StatsDMonitor - def initialize(app) - @app = app - end - - def call(env) - @app.call(env) - end -end diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb new file mode 100644 index 000000000..e59b05eb8 --- /dev/null +++ b/app/models/preview_card.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class PreviewCard < ApplicationRecord + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + + belongs_to :status + + has_attached_file :image, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' } + + validates :url, presence: true + validates_attachment_content_type :image, content_type: IMAGE_MIME_TYPES + validates_attachment_size :image, less_than: 1.megabytes + + def save_with_optional_image! + save! + rescue ActiveRecord::RecordInvalid + self.image = nil + save! + end +end diff --git a/app/models/status.rb b/app/models/status.rb index 5710f9cca..d5f52b55c 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -23,6 +23,7 @@ class Status < ApplicationRecord has_and_belongs_to_many :tags has_one :notification, as: :activity, dependent: :destroy + has_one :preview_card, dependent: :destroy validates :account, presence: true validates :uri, uniqueness: true, unless: 'local?' diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb new file mode 100644 index 000000000..2779b79b5 --- /dev/null +++ b/app/services/fetch_link_card_service.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class FetchLinkCardService < BaseService + def call(status) + # Get first URL + url = URI.extract(status.text).reject { |uri| (uri =~ /\Ahttps?:\/\//).nil? }.first + + return if url.nil? + + response = http_client.get(url) + + return if response.code != 200 + + page = Nokogiri::HTML(response.to_s) + card = PreviewCard.where(status: status).first_or_initialize(status: status, url: url) + + card.title = meta_property(page, 'og:title') || page.at_xpath('//title')&.content + card.description = meta_property(page, 'og:description') || meta_property(page, 'description') + card.image = URI.parse(meta_property(page, 'og:image')) if meta_property(page, 'og:image') + + card.save_with_optional_image! + end + + private + + def http_client + HTTP.timeout(:per_operation, write: 10, connect: 10, read: 10).follow + end + + def meta_property(html, property) + html.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || html.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value + end +end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index af31c923f..8765ef5e3 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -22,6 +22,7 @@ class PostStatusService < BaseService process_mentions_service.call(status) process_hashtags_service.call(status) + LinkCrawlWorker.perform_async(status.id) DistributionWorker.perform_async(status.id) Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id) diff --git a/app/views/api/v1/statuses/card.rabl b/app/views/api/v1/statuses/card.rabl new file mode 100644 index 000000000..8ba8dcbb1 --- /dev/null +++ b/app/views/api/v1/statuses/card.rabl @@ -0,0 +1,5 @@ +object @card + +attributes :url, :title, :description + +node(:image) { |card| card.image? ? full_asset_url(card.image.url(:original)) : nil } diff --git a/app/workers/link_crawl_worker.rb b/app/workers/link_crawl_worker.rb new file mode 100644 index 000000000..af3394b8b --- /dev/null +++ b/app/workers/link_crawl_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class LinkCrawlWorker + include Sidekiq::Worker + + sidekiq_options retry: false + + def perform(status_id) + FetchLinkCardService.new.call(Status.find(status_id)) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/config/application.rb b/config/application.rb index e97fb165b..d0b06bf95 100644 --- a/config/application.rb +++ b/config/application.rb @@ -3,6 +3,7 @@ require_relative 'boot' require 'rails/all' require_relative '../app/lib/exceptions' +require_relative '../lib/statsd_monitor' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -30,7 +31,7 @@ module Mastodon config.active_job.queue_adapter = :sidekiq - config.middleware.insert(0, 'StatsDMonitor') + config.middleware.insert(0, ::StatsDMonitor) config.middleware.insert_before 0, Rack::Cors do allow do diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 8fd1ae72c..b5e43e705 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -12,4 +12,5 @@ ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'StatsD' + inflect.acronym 'OEmbed' end diff --git a/config/routes.rb b/config/routes.rb index 42de503f0..4606c663a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -86,6 +86,7 @@ Rails.application.routes.draw do resources :statuses, only: [:create, :show, :destroy] do member do get :context + get :card get :reblogged_by get :favourited_by @@ -146,7 +147,7 @@ Rails.application.routes.draw do get '/about', to: 'about#index' get '/about/more', to: 'about#more' get '/terms', to: 'about#terms' - + root 'home#index' match '*unmatched_route', via: :all, to: 'application#raise_not_found' diff --git a/db/migrate/20170119214911_create_preview_cards.rb b/db/migrate/20170119214911_create_preview_cards.rb new file mode 100644 index 000000000..70ed91bbd --- /dev/null +++ b/db/migrate/20170119214911_create_preview_cards.rb @@ -0,0 +1,17 @@ +class CreatePreviewCards < ActiveRecord::Migration[5.0] + def change + create_table :preview_cards do |t| + t.integer :status_id + t.string :url, null: false, default: '' + + # OpenGraph + t.string :title, null: true + t.string :description, null: true + t.attachment :image + + t.timestamps + end + + add_index :preview_cards, :status_id, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 37da0c44e..abe6f1bfe 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: 20170114203041) do +ActiveRecord::Schema.define(version: 20170119214911) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -157,6 +157,20 @@ ActiveRecord::Schema.define(version: 20170114203041) do t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree end + create_table "preview_cards", force: :cascade do |t| + t.integer "status_id" + t.string "url", default: "", null: false + t.string "title" + t.string "description" + t.string "image_file_name" + t.string "image_content_type" + t.integer "image_file_size" + t.datetime "image_updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["status_id"], name: "index_preview_cards_on_status_id", unique: true, using: :btree + end + create_table "pubsubhubbub_subscriptions", force: :cascade do |t| t.string "topic", default: "", null: false t.string "callback", default: "", null: false diff --git a/lib/statsd_monitor.rb b/lib/statsd_monitor.rb new file mode 100644 index 000000000..e48ce6541 --- /dev/null +++ b/lib/statsd_monitor.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class StatsDMonitor + def initialize(app) + @app = app + end + + def call(env) + @app.call(env) + end +end diff --git a/spec/fabricators/preview_card_fabricator.rb b/spec/fabricators/preview_card_fabricator.rb new file mode 100644 index 000000000..448a94e7e --- /dev/null +++ b/spec/fabricators/preview_card_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:preview_card) do + status_id 1 + url "MyString" + html "MyText" +end diff --git a/spec/models/preview_card_spec.rb b/spec/models/preview_card_spec.rb new file mode 100644 index 000000000..14ef23923 --- /dev/null +++ b/spec/models/preview_card_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe PreviewCard, type: :model do + +end diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb index d40bf0b44..9cb3d41ce 100644 --- a/spec/models/subscription_spec.rb +++ b/spec/models/subscription_spec.rb @@ -1,5 +1,5 @@ require 'rails_helper' RSpec.describe Subscription, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + end -- cgit From 5fef55c11a7ea855e7b43ed274558a0893d0dc16 Mon Sep 17 00:00:00 2001 From: Effy Elden Date: Sat, 21 Jan 2017 20:22:49 +1100 Subject: Set remote dev environments to use letter_opener_web, add default .env.vagrant variables --- .env.vagrant | 1 + Gemfile | 1 + Gemfile.lock | 7 ++++++- config/environments/development.rb | 10 +++++++++- config/routes.rb | 6 ++++++ 5 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 .env.vagrant (limited to 'config') diff --git a/.env.vagrant b/.env.vagrant new file mode 100644 index 000000000..0ab0552c9 --- /dev/null +++ b/.env.vagrant @@ -0,0 +1 @@ +VAGRANT=true \ No newline at end of file diff --git a/Gemfile b/Gemfile index bab7cebb5..ebee7e35f 100644 --- a/Gemfile +++ b/Gemfile @@ -73,6 +73,7 @@ group :development do gem 'better_errors' gem 'binding_of_caller' gem 'letter_opener' + gem 'letter_opener_web' gem 'bullet' gem 'active_record_query_trace' end diff --git a/Gemfile.lock b/Gemfile.lock index 20ea37fcc..0e720d7fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -176,6 +176,10 @@ GEM addressable (~> 2.3) letter_opener (1.4.1) launchy (~> 2.2) + letter_opener_web (1.3.0) + actionmailer (>= 3.2) + letter_opener (~> 1.0) + railties (>= 3.2) link_header (0.0.8) lograge (0.4.1) actionpack (>= 4, < 5.1) @@ -432,6 +436,7 @@ DEPENDENCIES jbuilder (~> 2.0) jquery-rails letter_opener + letter_opener_web link_header lograge nokogiri @@ -474,4 +479,4 @@ RUBY VERSION ruby 2.3.1p112 BUNDLED WITH - 1.13.6 + 1.13.7 diff --git a/config/environments/development.rb b/config/environments/development.rb index 829edcf04..476f3d74c 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -62,7 +62,15 @@ Rails.application.configure do # routes, locales, etc. This feature depends on the listen gem. # config.file_watcher = ActiveSupport::EventedFileUpdateChecker - config.action_mailer.delivery_method = :letter_opener + # If usng a Heroku, Vagrant or generic remote development environment, + # use letter_opener_web, accessible at /letter_opener. + # + # Otherwise, use letter_opener, which launches a browser window to view sent mail. + if (ENV['HEROKU'] || ENV['VAGRANT'] || ENV['REMOTE_DEV']) + config.action_mailer.delivery_method = :letter_opener_web + else + config.action_mailer.delivery_method = :letter_opener + end config.after_initialize do Bullet.enable = true diff --git a/config/routes.rb b/config/routes.rb index 4606c663a..0fcbc266d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,12 @@ require 'sidekiq/web' Rails.application.routes.draw do + + # Development-only routes + if Rails.env.development? + mount LetterOpenerWeb::Engine, at: "/letter_opener" + end + mount ActionCable.server, at: 'cable' authenticate :user, lambda { |u| u.admin? } do -- cgit From 3a4b5961bea66681ba9c455fcfd3ec497bfaed2f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 21 Jan 2017 22:14:13 +0100 Subject: Method to fetch a single notification --- app/controllers/api/v1/notifications_controller.rb | 4 ++++ config/routes.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'config') diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index c8f162cb0..3fd701997 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -20,4 +20,8 @@ class Api::V1::NotificationsController < ApiController set_pagination_headers(next_path, prev_path) end + + def show + @notification = Notification.where(account: current_account).find(params[:id]) + end end diff --git a/config/routes.rb b/config/routes.rb index 4606c663a..349ab336a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -114,7 +114,7 @@ Rails.application.routes.draw do end end - resources :notifications, only: [:index] + resources :notifications, only: [:index, :show] resources :favourites, only: [:index] resources :accounts, only: [:show] do -- cgit From e028406db4bba2d2fa924d3777e47b51983dc800 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 21 Jan 2017 22:19:13 +0100 Subject: Fix code style --- config/environments/development.rb | 9 ++------- config/routes.rb | 7 +------ 2 files changed, 3 insertions(+), 13 deletions(-) (limited to 'config') diff --git a/config/environments/development.rb b/config/environments/development.rb index 476f3d74c..3f44d861e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -62,15 +62,10 @@ Rails.application.configure do # routes, locales, etc. This feature depends on the listen gem. # config.file_watcher = ActiveSupport::EventedFileUpdateChecker - # If usng a Heroku, Vagrant or generic remote development environment, + # If using a Heroku, Vagrant or generic remote development environment, # use letter_opener_web, accessible at /letter_opener. - # # Otherwise, use letter_opener, which launches a browser window to view sent mail. - if (ENV['HEROKU'] || ENV['VAGRANT'] || ENV['REMOTE_DEV']) - config.action_mailer.delivery_method = :letter_opener_web - else - config.action_mailer.delivery_method = :letter_opener - end + config.action_mailer.delivery_method = (ENV['HEROKU'] || ENV['VAGRANT'] || ENV['REMOTE_DEV']) ? :letter_opener_web : :letter_opener config.after_initialize do Bullet.enable = true diff --git a/config/routes.rb b/config/routes.rb index 1a7d53185..7b9cda908 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,12 +3,7 @@ require 'sidekiq/web' Rails.application.routes.draw do - - # Development-only routes - if Rails.env.development? - mount LetterOpenerWeb::Engine, at: "/letter_opener" - end - + mount LetterOpenerWeb::Engine, at: 'letter_opener' if Rails.env.development? mount ActionCable.server, at: 'cable' authenticate :user, lambda { |u| u.admin? } do -- cgit From 67befe546342c58f1a16bdb08b6a2ef438e83d57 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 22 Jan 2017 20:35:49 +0100 Subject: Allow to specify trusted proxies through env --- config/environments/production.rb | 3 +++ 1 file changed, 3 insertions(+) (limited to 'config') diff --git a/config/environments/production.rb b/config/environments/production.rb index 1572eaf6c..d2dfa4274 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -32,6 +32,9 @@ Rails.application.configure do # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + # Allow to specify public IP of reverse proxy if it's needed + config.action_dispatch.trusted_proxies = [IPAddr.new(ENV['TRUSTED_PROXY_IP'])] unless ENV['TRUSTED_PROXY_IP'].blank? + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = false -- cgit From 61aee0006eec95f47ca0a0c75f3ccf4151516b4a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 22 Jan 2017 21:01:28 +0100 Subject: Override Rack::Request to use the same trusted proxy settings as Rails --- config/initializers/trusted_proxies.rb | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 config/initializers/trusted_proxies.rb (limited to 'config') diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb new file mode 100644 index 000000000..27e9ee039 --- /dev/null +++ b/config/initializers/trusted_proxies.rb @@ -0,0 +1,7 @@ +module Rack + class Request + def trusted_proxy?(ip) + Rails.application.config.action_dispatch.trusted_proxies.any? { |proxy| proxy === ip } + end + end +end -- cgit From 956da43e19e484f94df91f715e4a88794a25dbec Mon Sep 17 00:00:00 2001 From: Eugen Date: Sun, 22 Jan 2017 23:07:31 +0100 Subject: Fix error --- config/initializers/trusted_proxies.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'config') diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb index 27e9ee039..3c2afd8cd 100644 --- a/config/initializers/trusted_proxies.rb +++ b/config/initializers/trusted_proxies.rb @@ -1,7 +1,11 @@ module Rack class Request def trusted_proxy?(ip) - Rails.application.config.action_dispatch.trusted_proxies.any? { |proxy| proxy === ip } + if Rails.application.config.action_dispatch.trusted_proxies.nil? + super + else + Rails.application.config.action_dispatch.trusted_proxies.any? { |proxy| proxy === ip } + end end end end -- cgit From f392030ab82a70086f93bb02c3faab53c3fbd28e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 23 Jan 2017 21:09:27 +0100 Subject: Add /api/v1/notifications/clear, non-existing link cards for statuses will now return empty hash instead of throwing a 404 error. When following, merge into timeline will filter statuses --- app/assets/javascripts/components/actions/cards.jsx | 7 +++---- app/controllers/api/v1/notifications_controller.rb | 5 +++++ app/controllers/api/v1/statuses_controller.rb | 3 ++- app/lib/feed_manager.rb | 1 + config/routes.rb | 16 ++++++++++------ 5 files changed, 21 insertions(+), 11 deletions(-) (limited to 'config') diff --git a/app/assets/javascripts/components/actions/cards.jsx b/app/assets/javascripts/components/actions/cards.jsx index ee421d5d7..714e80525 100644 --- a/app/assets/javascripts/components/actions/cards.jsx +++ b/app/assets/javascripts/components/actions/cards.jsx @@ -9,13 +9,12 @@ export function fetchStatusCard(id) { dispatch(fetchStatusCardRequest(id)); api(getState).get(`/api/v1/statuses/${id}/card`).then(response => { - dispatch(fetchStatusCardSuccess(id, response.data)); - }).catch(error => { - if (error.response.status === 404) { - // This is fine + if (response.data.length === 0) { return; } + dispatch(fetchStatusCardSuccess(id, response.data)); + }).catch(error => { dispatch(fetchStatusCardFail(id, error)); }); }; diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 3fd701997..ee12446d8 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -24,4 +24,9 @@ class Api::V1::NotificationsController < ApiController def show @notification = Notification.where(account: current_account).find(params[:id]) end + + def clear + Notification.where(account: current_account).delete_all + render_empty + end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 37ed5e6dd..da87ebbad 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -22,7 +22,8 @@ class Api::V1::StatusesController < ApiController end def card - @card = PreviewCard.find_by!(status: @status) + @card = PreviewCard.find_by(status: @status) + render_empty if @card.nil? end def reblogged_by diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 0056321fa..19f9dc16f 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -43,6 +43,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) redis.zadd(timeline_key, status.id, status.id) end diff --git a/config/routes.rb b/config/routes.rb index 7b9cda908..15fb924f1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -103,10 +103,11 @@ Rails.application.routes.draw do get '/timelines/public', to: 'timelines#public', as: :public_timeline get '/timelines/tag/:id', to: 'timelines#tag', as: :hashtag_timeline - resources :follows, only: [:create] - resources :media, only: [:create] - resources :apps, only: [:create] - resources :blocks, only: [:index] + resources :follows, only: [:create] + resources :media, only: [:create] + resources :apps, only: [:create] + resources :blocks, only: [:index] + resources :favourites, only: [:index] resources :follow_requests, only: [:index] do member do @@ -115,8 +116,11 @@ Rails.application.routes.draw do end end - resources :notifications, only: [:index, :show] - resources :favourites, only: [:index] + resources :notifications, only: [:index, :show] do + collection do + post :clear + end + end resources :accounts, only: [:show] do collection do -- cgit From 999cde94a6a2d67cf36160365378951d3b55b868 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 25 Jan 2017 00:49:08 +0100 Subject: Instead of using spoiler boolean and spoiler_text, simply check for non-blank spoiler_text Federate spoiler_text using warning attribute on instead of a Clean up schema file from accidental development migrations --- .../javascripts/components/actions/compose.jsx | 1 - .../components/components/status_content.jsx | 65 ++++++----- .../features/compose/components/compose_form.jsx | 2 +- .../features/compose/components/upload_form.jsx | 3 +- .../javascripts/components/reducers/compose.jsx | 124 ++++++++++----------- app/assets/javascripts/extras.jsx | 10 -- app/assets/stylesheets/stream_entries.scss | 2 + app/controllers/api/v1/statuses_controller.rb | 7 +- app/helpers/atom_builder_helper.rb | 18 ++- app/lib/formatter.rb | 17 +-- app/lib/status_length_validator.rb | 10 ++ app/models/status.rb | 4 +- app/services/fetch_link_card_service.rb | 4 +- app/services/post_status_service.rb | 12 +- app/services/process_feed_service.rb | 5 + app/services/process_hashtags_service.rb | 1 - app/validators/status_length_validator.rb | 15 --- app/views/api/v1/statuses/_show.rabl | 2 +- .../stream_entries/_detailed_status.html.haml | 5 +- app/views/stream_entries/_simple_status.html.haml | 5 +- config/locales/en.yml | 2 + .../20170112041538_add_spoiler_to_statuses.rb | 5 - db/schema.rb | 15 --- 23 files changed, 160 insertions(+), 174 deletions(-) create mode 100644 app/lib/status_length_validator.rb delete mode 100644 app/validators/status_length_validator.rb delete mode 100644 db/migrate/20170112041538_add_spoiler_to_statuses.rb (limited to 'config') diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx index 948ccf872..6d0188166 100644 --- a/app/assets/javascripts/components/actions/compose.jsx +++ b/app/assets/javascripts/components/actions/compose.jsx @@ -70,7 +70,6 @@ export function submitCompose() { in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')), sensitive: getState().getIn(['compose', 'sensitive']), - spoiler: getState().getIn(['compose', 'spoiler']), spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), visibility: getState().getIn(['compose', 'private']) ? 'private' : (getState().getIn(['compose', 'unlisted']) ? 'unlisted' : 'public') }).then(function (response) { diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index 74b52e485..ff90226d8 100644 --- a/app/assets/javascripts/components/components/status_content.jsx +++ b/app/assets/javascripts/components/components/status_content.jsx @@ -1,6 +1,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import emojify from '../emoji'; +import { FormattedMessage } from 'react-intl'; const StatusContent = React.createClass({ @@ -13,17 +14,17 @@ const StatusContent = React.createClass({ onClick: React.PropTypes.func }, + getInitialState () { + return { + hidden: true + }; + }, + mixins: [PureRenderMixin], componentDidMount () { const node = ReactDOM.findDOMNode(this); const links = node.querySelectorAll('a'); - const spoilers = node.querySelectorAll('.spoiler'); - - for (var i = 0; i < spoilers.length; ++i) { - let spoiler = spoilers[i]; - spoiler.addEventListener('click', this.onSpoilerClick.bind(this, spoiler), true); - } for (var i = 0; i < links.length; ++i) { let link = links[i]; @@ -56,18 +57,6 @@ const StatusContent = React.createClass({ } }, - onSpoilerClick (spoiler, e) { - if (e.button === 0) { - //only toggle if we're not clicking a visible link - var hasClass = $(spoiler).hasClass('spoiler-on'); - if (hasClass || e.target === spoiler) { - e.stopPropagation(); - e.preventDefault(); - $(spoiler).siblings(".spoiler").andSelf().toggleClass('spoiler-on', !hasClass); - } - } - }, - handleMouseDown (e) { this.startXY = [e.clientX, e.clientY]; }, @@ -87,20 +76,40 @@ const StatusContent = React.createClass({ this.startXY = null; }, + handleSpoilerClick () { + this.setState({ hidden: !this.state.hidden }); + }, + render () { const { status } = this.props; + const { hidden } = this.state; const content = { __html: emojify(status.get('content')) }; - - return ( -
- ); + const spoilerContent = { __html: emojify(status.get('spoiler_text')) }; + + if (status.get('spoiler_text').length > 0) { + const toggleText = hidden ? : ; + + return ( +
+

+ {toggleText} +

+ + + ); + } else { + return ( +
+ ); + } }, }); diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx index 84d273299..48363a968 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -155,7 +155,7 @@ const ComposeForm = React.createClass({
-
+
diff --git a/app/assets/javascripts/components/features/compose/components/upload_form.jsx b/app/assets/javascripts/components/features/compose/components/upload_form.jsx index 8a14dda69..94c94b4b7 100644 --- a/app/assets/javascripts/components/features/compose/components/upload_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/upload_form.jsx @@ -12,7 +12,8 @@ const UploadForm = React.createClass({ propTypes: { media: ImmutablePropTypes.list.isRequired, is_uploading: React.PropTypes.bool, - onRemoveFile: React.PropTypes.func.isRequired + onRemoveFile: React.PropTypes.func.isRequired, + intl: React.PropTypes.object.isRequired }, mixins: [PureRenderMixin], diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx index 1c6c3d4f4..d3a84842f 100644 --- a/app/assets/javascripts/components/reducers/compose.jsx +++ b/app/assets/javascripts/components/reducers/compose.jsx @@ -96,68 +96,68 @@ const insertSuggestion = (state, position, token, completion) => { export default function compose(state = initialState, action) { switch(action.type) { - case STORE_HYDRATE: - return state.merge(action.state.get('compose')); - case COMPOSE_MOUNT: - return state.set('mounted', true); - case COMPOSE_UNMOUNT: - return state.set('mounted', false); - case COMPOSE_SENSITIVITY_CHANGE: - return state.set('sensitive', action.checked); - case COMPOSE_SPOILERNESS_CHANGE: - return state.set('spoiler', action.checked); - case COMPOSE_SPOILER_TEXT_CHANGE: - return state.set('spoiler_text', action.text); - case COMPOSE_VISIBILITY_CHANGE: - return state.set('private', action.checked); - case COMPOSE_LISTABILITY_CHANGE: - return state.set('unlisted', action.checked); - case COMPOSE_CHANGE: - return state.set('text', action.text); - case COMPOSE_REPLY: - return state.withMutations(map => { - map.set('in_reply_to', action.status.get('id')); - map.set('text', statusToTextMentions(state, action.status)); - }); - case COMPOSE_REPLY_CANCEL: - return state.withMutations(map => { - map.set('in_reply_to', null); - map.set('text', ''); - }); - case COMPOSE_SUBMIT_REQUEST: - return state.set('is_submitting', true); - case COMPOSE_SUBMIT_SUCCESS: - return clearAll(state); - case COMPOSE_SUBMIT_FAIL: - return state.set('is_submitting', false); - case COMPOSE_UPLOAD_REQUEST: - return state.withMutations(map => { - map.set('is_uploading', true); - map.set('fileDropDate', new Date()); - }); - case COMPOSE_UPLOAD_SUCCESS: - return appendMedia(state, Immutable.fromJS(action.media)); - case COMPOSE_UPLOAD_FAIL: - return state.set('is_uploading', false); - case COMPOSE_UPLOAD_UNDO: - return removeMedia(state, action.media_id); - case COMPOSE_UPLOAD_PROGRESS: - return state.set('progress', Math.round((action.loaded / action.total) * 100)); - case COMPOSE_MENTION: - return state.update('text', text => `${text}@${action.account.get('acct')} `); - case COMPOSE_SUGGESTIONS_CLEAR: - return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null); - case COMPOSE_SUGGESTIONS_READY: - return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))).set('suggestion_token', action.token); - case COMPOSE_SUGGESTION_SELECT: - return insertSuggestion(state, action.position, action.token, action.completion); - case TIMELINE_DELETE: - if (action.id === state.get('in_reply_to')) { - return state.set('in_reply_to', null); - } else { - return state; - } - default: + case STORE_HYDRATE: + return state.merge(action.state.get('compose')); + case COMPOSE_MOUNT: + return state.set('mounted', true); + case COMPOSE_UNMOUNT: + return state.set('mounted', false); + case COMPOSE_SENSITIVITY_CHANGE: + return state.set('sensitive', action.checked); + case COMPOSE_SPOILERNESS_CHANGE: + return (action.checked ? state : state.set('spoiler_text', '')).set('spoiler', action.checked); + case COMPOSE_SPOILER_TEXT_CHANGE: + return state.set('spoiler_text', action.text); + case COMPOSE_VISIBILITY_CHANGE: + return state.set('private', action.checked); + case COMPOSE_LISTABILITY_CHANGE: + return state.set('unlisted', action.checked); + case COMPOSE_CHANGE: + return state.set('text', action.text); + case COMPOSE_REPLY: + return state.withMutations(map => { + map.set('in_reply_to', action.status.get('id')); + map.set('text', statusToTextMentions(state, action.status)); + }); + case COMPOSE_REPLY_CANCEL: + return state.withMutations(map => { + map.set('in_reply_to', null); + map.set('text', ''); + }); + case COMPOSE_SUBMIT_REQUEST: + return state.set('is_submitting', true); + case COMPOSE_SUBMIT_SUCCESS: + return clearAll(state); + case COMPOSE_SUBMIT_FAIL: + return state.set('is_submitting', false); + case COMPOSE_UPLOAD_REQUEST: + return state.withMutations(map => { + map.set('is_uploading', true); + map.set('fileDropDate', new Date()); + }); + case COMPOSE_UPLOAD_SUCCESS: + return appendMedia(state, Immutable.fromJS(action.media)); + case COMPOSE_UPLOAD_FAIL: + return state.set('is_uploading', false); + case COMPOSE_UPLOAD_UNDO: + return removeMedia(state, action.media_id); + case COMPOSE_UPLOAD_PROGRESS: + return state.set('progress', Math.round((action.loaded / action.total) * 100)); + case COMPOSE_MENTION: + return state.update('text', text => `${text}@${action.account.get('acct')} `); + case COMPOSE_SUGGESTIONS_CLEAR: + return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null); + case COMPOSE_SUGGESTIONS_READY: + return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))).set('suggestion_token', action.token); + case COMPOSE_SUGGESTION_SELECT: + return insertSuggestion(state, action.position, action.token, action.completion); + case TIMELINE_DELETE: + if (action.id === state.get('in_reply_to')) { + return state.set('in_reply_to', null); + } else { return state; + } + default: + return state; } }; diff --git a/app/assets/javascripts/extras.jsx b/app/assets/javascripts/extras.jsx index 5784d17c2..5738863dd 100644 --- a/app/assets/javascripts/extras.jsx +++ b/app/assets/javascripts/extras.jsx @@ -14,16 +14,6 @@ $(() => { } }); - $.each($('.spoiler'), (_, content) => { - $(content).on('click', e => { - var hasClass = $(content).hasClass('spoiler-on'); - if (hasClass || e.target === content) { - e.preventDefault(); - $(content).siblings(".spoiler").andSelf().toggleClass('spoiler-on', !hasClass); - } - }); - }); - $('.media-spoiler').on('click', e => { $(e.target).hide(); }); diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss index ccae88ec7..2d3cb1436 100644 --- a/app/assets/stylesheets/stream_entries.scss +++ b/app/assets/stylesheets/stream_entries.scss @@ -249,6 +249,7 @@ padding: 5px; border-radius: 100px; color: rgba($color5, 0.8); + z-index: 1; } } @@ -263,6 +264,7 @@ flex-direction: column; text-align: center; transition: all 100ms linear; + z-index: 2; &:hover { background: darken($color3, 5%); diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 09d5a82fa..4b095a570 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -57,7 +57,12 @@ class Api::V1::StatusesController < ApiController end 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], sensitive: params[:sensitive], spoiler: params[:spoiler], spoiler_text: params[:spoiler_text], visibility: params[:visibility], application: doorkeeper_token.application) + @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], + sensitive: params[:sensitive], + spoiler_text: params[:spoiler_text], + visibility: params[:visibility], + application: doorkeeper_token.application) + render action: :show end diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb index 7547e77e4..f17b4cc72 100644 --- a/app/helpers/atom_builder_helper.rb +++ b/app/helpers/atom_builder_helper.rb @@ -41,8 +41,10 @@ module AtomBuilderHelper xml['activity'].send('verb', TagManager::VERBS[verb]) end - def content(xml, content) - xml.content({ type: 'html' }, content) unless content.blank? + def content(xml, content, warning = nil) + extra = { type: 'html' } + extra[:warning] = warning unless warning.blank? + xml.content(extra, content) unless content.blank? end def title(xml, title) @@ -153,12 +155,20 @@ module AtomBuilderHelper portable_contact xml, account end + def rich_content(xml, activity) + if activity.is_a?(Status) + content xml, conditionally_formatted(activity), activity.spoiler_text + else + content xml, conditionally_formatted(activity) + end + end + def include_entry(xml, stream_entry) unique_id xml, stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type published_at xml, stream_entry.created_at updated_at xml, stream_entry.updated_at title xml, stream_entry.title - content xml, conditionally_formatted(stream_entry.activity) + rich_content xml, stream_entry.activity verb xml, stream_entry.verb link_self xml, account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom') link_alternate xml, account_stream_entry_url(stream_entry.account, stream_entry) @@ -207,7 +217,6 @@ module AtomBuilderHelper end category(xml, 'nsfw') if stream_entry.target.sensitive? - category(xml, 'spoiler') if stream_entry.target.spoiler? end end end @@ -229,7 +238,6 @@ module AtomBuilderHelper end category(xml, 'nsfw') if stream_entry.activity.sensitive? - category(xml, 'spoiler') if stream_entry.activity.spoiler? end private diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 72cd3d234..ff2a16f1b 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -14,15 +14,7 @@ class Formatter html = status.text html = encode(html) - - if (status.spoiler?) - spoilerhtml = status.spoiler_text - spoilerhtml = encode(spoilerhtml) - html = wrap_spoilers(html, spoilerhtml) - else - html = simple_format(html, sanitize: false) - end - + html = simple_format(html, {}, sanitize: false) html = html.gsub(/\n/, '') html = link_urls(html) html = link_mentions(html, status.mentions) @@ -51,13 +43,6 @@ class Formatter HTMLEntities.new.encode(html) end - def wrap_spoilers(html, spoilerhtml) - spoilerhtml = simple_format(spoilerhtml, {class: "spoiler-helper"}, {sanitize: false}) - html = simple_format(html, {class: ["spoiler", "spoiler-on"]}, {sanitize: false}) - - spoilerhtml + html - end - def link_urls(html) html.gsub(URI.regexp(%w(http https))) do |match| link_html(match) diff --git a/app/lib/status_length_validator.rb b/app/lib/status_length_validator.rb new file mode 100644 index 000000000..55135a598 --- /dev/null +++ b/app/lib/status_length_validator.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class StatusLengthValidator < ActiveModel::Validator + MAX_CHARS = 500 + + def validate(status) + return unless status.local? && !status.reblog? + status.errors.add(:text, I18n.t('statuses.over_character_limit', max: MAX_CHARS)) if [status.text, status.spoiler_text].join.length > MAX_CHARS + end +end diff --git a/app/models/status.rb b/app/models/status.rb index 42abe92e5..651d0dbc9 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -28,9 +28,8 @@ class Status < ApplicationRecord validates :account, presence: true validates :uri, uniqueness: true, unless: 'local?' - validates :text, presence: true, if: proc { |s| s.local? && !s.reblog? } + validates :text, presence: true, unless: 'reblog?' validates_with StatusLengthValidator - validates :text, presence: true, if: proc { |s| !s.local? && !s.reblog? } validates :reblog, uniqueness: { scope: :account, message: 'of status already exists' }, if: 'reblog?' default_scope { order('id desc') } @@ -176,6 +175,7 @@ class Status < ApplicationRecord before_validation do text.strip! + spoiler_text&.strip! self.reblog = reblog.reblog if reblog? && reblog.reblog? self.in_reply_to_account_id = (thread.account_id == account_id && thread.reply? ? thread.in_reply_to_account_id : thread.account_id) if reply? diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 2779b79b5..005e5acea 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -9,7 +9,7 @@ class FetchLinkCardService < BaseService response = http_client.get(url) - return if response.code != 200 + return if response.code != 200 || response.mime_type != 'text/html' page = Nokogiri::HTML(response.to_s) card = PreviewCard.where(status: status).first_or_initialize(status: status, url: url) @@ -18,6 +18,8 @@ class FetchLinkCardService < BaseService card.description = meta_property(page, 'og:description') || meta_property(page, 'description') card.image = URI.parse(meta_property(page, 'og:image')) if meta_property(page, 'og:image') + return if card.title.blank? + card.save_with_optional_image! end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index ef8aa4a91..91b654603 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -8,18 +8,16 @@ class PostStatusService < BaseService # @param [Hash] options # @option [Boolean] :sensitive # @option [String] :visibility - # @option [Boolean] :spoiler # @option [String] :spoiler_text # @option [Enumerable] :media_ids Optional array of media IDs to attach # @option [Doorkeeper::Application] :application # @return [Status] def call(account, text, in_reply_to = nil, options = {}) - status = account.statuses.create!(text: text, - thread: in_reply_to, - sensitive: options[:sensitive], - spoiler: options[:spoiler], - spoiler_text: options[:spoiler_text], - visibility: options[:visibility], + status = account.statuses.create!(text: text, + thread: in_reply_to, + sensitive: options[:sensitive], + spoiler_text: options[:spoiler_text], + visibility: options[:visibility], application: options[:application]) attach_media(status, options[:media_ids]) diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb index 84273680d..4576b4321 100644 --- a/app/services/process_feed_service.rb +++ b/app/services/process_feed_service.rb @@ -103,6 +103,7 @@ class ProcessFeedService < BaseService url: url(entry), account: account, text: content(entry), + spoiler_text: content_warning(entry), created_at: published(entry) ) @@ -223,6 +224,10 @@ class ProcessFeedService < BaseService xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content end + def content_warning(xml = @xml) + xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS)['warning'] + end + def published(xml = @xml) xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content end diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index 9da7ef74e..617a38159 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -9,6 +9,5 @@ class ProcessHashtagsService < BaseService end status.update(sensitive: true) if tags.include?('nsfw') - status.update(spoiler: true) if tags.include?('spoiler') end end diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb deleted file mode 100644 index 5491d3d5f..000000000 --- a/app/validators/status_length_validator.rb +++ /dev/null @@ -1,15 +0,0 @@ -class StatusLengthValidator < ActiveModel::Validator - def validate(status) - if status.local? && !status.reblog? - combinedText = status.text - if (status.spoiler? && status.spoiler_text.present?) - combinedText = status.spoiler_text + "\n" + status.text - end - - maxChars = 500 - unless combinedText.length <= maxChars - status.errors[:text] << "is too long (maximum is #{maxChars})" - end - end - end -end \ No newline at end of file diff --git a/app/views/api/v1/statuses/_show.rabl b/app/views/api/v1/statuses/_show.rabl index 8b54d5852..7309a78b8 100644 --- a/app/views/api/v1/statuses/_show.rabl +++ b/app/views/api/v1/statuses/_show.rabl @@ -1,4 +1,4 @@ -attributes :id, :created_at, :in_reply_to_id, :sensitive, :spoiler, :visibility +attributes :id, :created_at, :in_reply_to_id, :sensitive, :spoiler_text, :visibility node(:uri) { |status| TagManager.instance.uri_for(status) } node(:content) { |status| Formatter.instance.format(status) } diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index bc09d3597..6ee8c9e5b 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -7,7 +7,10 @@ %strong.p-name.emojify= display_name(status.account) %span.p-nickname= acct(status.account) - .status__content.e-content.p-name.emojify= Formatter.instance.format(status) + .status__content.e-content.p-name.emojify< + - unless status.spoiler_text.blank? + %p= status.spoiler_text + = Formatter.instance.format(status) - unless status.media_attachments.empty? - if status.media_attachments.first.video? diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index eba2f9ac4..95f90abd9 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -12,7 +12,10 @@ %strong.p-name.emojify= display_name(status.account) %span.p-nickname= acct(status.account) - .status__content.e-content.p-name.emojify= Formatter.instance.format(status) + .status__content.e-content.p-name.emojify< + - unless status.spoiler_text.blank? + %p= status.spoiler_text + = Formatter.instance.format(status) - unless status.media_attachments.empty? .status__attachments diff --git a/config/locales/en.yml b/config/locales/en.yml index f7d7ed729..831fdbc7a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -93,6 +93,8 @@ en: back: Back to Mastodon edit_profile: Edit profile preferences: Preferences + statuses: + over_character_limit: character limit of %{max} exceeded stream_entries: click_to_show: Click to show favourited: favourited a post by diff --git a/db/migrate/20170112041538_add_spoiler_to_statuses.rb b/db/migrate/20170112041538_add_spoiler_to_statuses.rb deleted file mode 100644 index 3b46433f5..000000000 --- a/db/migrate/20170112041538_add_spoiler_to_statuses.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddSpoilerToStatuses < ActiveRecord::Migration[5.0] - def change - add_column :statuses, :spoiler, :boolean, default: false - end -end diff --git a/db/schema.rb b/db/schema.rb index f228da01e..0edd68605 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -173,19 +173,6 @@ ActiveRecord::Schema.define(version: 20170123203248) do t.index ["status_id"], name: "index_preview_cards_on_status_id", unique: true, using: :btree end - create_table "pubsubhubbub_subscriptions", force: :cascade do |t| - t.string "topic", default: "", null: false - t.string "callback", default: "", null: false - t.string "mode", default: "", null: false - t.string "challenge", default: "", null: false - t.string "secret" - t.boolean "confirmed", default: false, null: false - t.datetime "expires_at", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["topic", "callback"], name: "index_pubsubhubbub_subscriptions_on_topic_and_callback", unique: true, using: :btree - end - create_table "settings", force: :cascade do |t| t.string "var", null: false t.text "value" @@ -208,8 +195,6 @@ ActiveRecord::Schema.define(version: 20170123203248) 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.boolean "spoiler", default: false t.text "spoiler_text", default: "" t.integer "application_id" t.index ["account_id"], name: "index_statuses_on_account_id", using: :btree -- cgit