From 7b9f8766e88dceb9519085deada3fa673e4c015b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 6 Jan 2017 00:21:12 +0100 Subject: Fix #416 - Generate random unique 14-byte (19 characters) shortcodes for local attachments, use them in URLs. Check status privacy before redirecting to actual file. --- app/models/media_attachment.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'app/models') diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 2a5d23739..ecbed03e3 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -16,6 +16,7 @@ class MediaAttachment < ApplicationRecord validates :account, presence: true + scope :local, -> { where(remote_url: '') } default_scope { order('id asc') } def local? @@ -38,6 +39,12 @@ class MediaAttachment < ApplicationRecord image? ? 'image' : 'video' end + def to_param + shortcode + end + + before_create :set_shortcode + class << self private @@ -62,4 +69,15 @@ class MediaAttachment < ApplicationRecord end end end + + private + + def set_shortcode + return unless local? + + loop do + self.shortcode = SecureRandom.urlsafe_base64(14) + break if MediaAttachment.find_by(shortcode: shortcode).nil? + end + end end -- cgit From e5d9009d710ae6516e45789059e45118e9a0cb5d Mon Sep 17 00:00:00 2001 From: Kurtis Rainbolt-Greene Date: Sat, 7 Jan 2017 17:55:40 -0800 Subject: Using "rails style" order arguments --- app/models/account.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/models') diff --git a/app/models/account.rb b/app/models/account.rb index 5c1f6e7c1..41fcc864b 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -62,8 +62,8 @@ class Account < ApplicationRecord scope :expiring, ->(time) { where(subscription_expires_at: nil).or(where('subscription_expires_at < ?', time)).remote.with_followers } scope :silenced, -> { where(silenced: true) } scope :suspended, -> { where(suspended: true) } - scope :recent, -> { reorder('id desc') } - scope :alphabetic, -> { order('domain ASC, username ASC') } + scope :recent, -> { reorder(id: :desc) } + scope :alphabetic, -> { order(domain: :asc, username: :asc) } def follow!(other_account) active_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) -- cgit From 4a7e3e5082ed2a974a3938e8fb404445bd1d08f9 Mon Sep 17 00:00:00 2001 From: Kurtis Rainbolt-Greene Date: Sat, 7 Jan 2017 17:59:51 -0800 Subject: Less complicated determination --- app/models/account.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/models') diff --git a/app/models/account.rb b/app/models/account.rb index 41fcc864b..db110a9e1 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -104,7 +104,7 @@ class Account < ApplicationRecord end def subscribed? - !subscription_expires_at.nil? + subscription_expires_at end def favourited?(status) -- cgit From abe3ae1cc2c21243b570dbe34f54ea7b568cc629 Mon Sep 17 00:00:00 2001 From: Kurtis Rainbolt-Greene Date: Sat, 7 Jan 2017 18:09:00 -0800 Subject: Simplifying followers mappings Deduplicating some logic and switching to pluck, to avoid pulling the entire model in memory. --- app/models/account.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'app/models') diff --git a/app/models/account.rb b/app/models/account.rb index 41fcc864b..1b87210c7 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -175,19 +175,23 @@ class Account < ApplicationRecord end def following_map(target_account_ids, account_id) - Follow.where(target_account_id: target_account_ids).where(account_id: account_id).map { |f| [f.target_account_id, true] }.to_h + follow_mapping(Follow.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) end def followed_by_map(target_account_ids, account_id) - Follow.where(account_id: target_account_ids).where(target_account_id: account_id).map { |f| [f.account_id, true] }.to_h + follow_mapping(Follow.where(account_id: target_account_ids, target_account_id: account_id), :account_id) end def blocking_map(target_account_ids, account_id) - Block.where(target_account_id: target_account_ids).where(account_id: account_id).map { |b| [b.target_account_id, true] }.to_h + follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) end def requested_map(target_account_ids, account_id) - FollowRequest.where(target_account_id: target_account_ids).where(account_id: account_id).map { |r| [r.target_account_id, true] }.to_h + 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 } end end -- 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 'app/models') 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 'app/models') 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 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 'app/models') 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 2939e9898b1e0e7da6db802a00e594be4c85499d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 13 Jan 2017 02:42:22 +0100 Subject: Extend rails-settings-cached to merge db-saved hash values with defaults --- app/controllers/api/v1/accounts_controller.rb | 2 +- app/lib/settings/extend.rb | 9 ++++ app/lib/settings/scoped_settings.rb | 12 +++++ app/models/setting.rb | 20 ++++++++ app/models/user.rb | 2 +- db/schema.rb | 69 +-------------------------- 6 files changed, 44 insertions(+), 70 deletions(-) create mode 100644 app/lib/settings/extend.rb create mode 100644 app/lib/settings/scoped_settings.rb (limited to 'app/models') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 05ff806c5..bd52dfb2c 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -96,7 +96,7 @@ class Api::V1::AccountsController < ApiController limit = params[:limit] ? [DEFAULT_ACCOUNTS_LIMIT, params[:limit].to_i].min : DEFAULT_ACCOUNTS_LIMIT @accounts = SearchService.new.call(params[:q], limit, params[:resolve] == 'true') - set_account_counters_maps(@accounts) + set_account_counters_maps(@accounts) unless @accounts.nil? render action: :index end diff --git a/app/lib/settings/extend.rb b/app/lib/settings/extend.rb new file mode 100644 index 000000000..7241a1221 --- /dev/null +++ b/app/lib/settings/extend.rb @@ -0,0 +1,9 @@ +module Settings + module Extend + extend ActiveSupport::Concern + + def settings + ScopedSettings.for_thing(self) + end + end +end \ No newline at end of file diff --git a/app/lib/settings/scoped_settings.rb b/app/lib/settings/scoped_settings.rb new file mode 100644 index 000000000..f8f22a91b --- /dev/null +++ b/app/lib/settings/scoped_settings.rb @@ -0,0 +1,12 @@ +module Settings + class ScopedSettings < ::Setting + def self.for_thing(object) + @object = object + self + end + + def self.thing_scoped + unscoped.where(thing_type: @object.class.base_class.to_s, thing_id: @object.id) + end + end +end \ No newline at end of file diff --git a/app/models/setting.rb b/app/models/setting.rb index 0a429a62b..f3c65c054 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -9,6 +9,26 @@ 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) + + if db_val + default_value = default_settings[key] + + return default_value.with_indifferent_access.merge!(db_val.value) if default_value.is_a?(Hash) + db_val.value + else + default_settings[key] + end + end + + val + end + def all_as_records vars = thing_scoped records = vars.map { |r| [r.var, r] }.to_h diff --git a/app/models/user.rb b/app/models/user.rb index bf7d04d7c..71d3ee0b8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class User < ApplicationRecord - include RailsSettings::Extend + include Settings::Extend devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable diff --git a/db/schema.rb b/db/schema.rb index 1cd1258db..f1bd752c9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -169,74 +169,6 @@ 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" @@ -259,6 +191,7 @@ 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 -- 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 'app/models') 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 d6bc0e8db4d25a4533feb56164b0d9cd3ef2af6e Mon Sep 17 00:00:00 2001 From: Effy Elden Date: Sun, 15 Jan 2017 08:58:50 +1100 Subject: Add tracking of OAuth app that posted a status, extend OAuth apps to have optional website field, add application details to API, show application name and website on detailed status views. Resolves #11 --- .../components/features/status/components/detailed_status.jsx | 2 +- app/assets/stylesheets/components.scss | 2 +- app/controllers/api/v1/apps_controller.rb | 2 +- app/controllers/api/v1/statuses_controller.rb | 2 +- app/models/concerns/application.rb | 8 ++++++++ app/models/status.rb | 2 ++ app/services/post_status_service.rb | 2 +- app/views/api/v1/apps/show.rabl | 3 +++ app/views/api/v1/statuses/_show.rabl | 4 ++++ app/views/stream_entries/_detailed_status.html.haml | 3 +++ db/migrate/20170114194937_add_application_to_statuses.rb | 5 +++++ db/migrate/20170114203041_add_website_to_oauth_application.rb | 5 +++++ db/schema.rb | 4 +++- 13 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 app/models/concerns/application.rb create mode 100644 app/views/api/v1/apps/show.rabl create mode 100644 db/migrate/20170114194937_add_application_to_statuses.rb create mode 100644 db/migrate/20170114203041_add_website_to_oauth_application.rb (limited to 'app/models') 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 b967d966f..7cbca4633 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -54,7 +54,7 @@ const DetailedStatus = React.createClass({ {media} ); diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index f1edfce9d..9e9e564f9 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -183,7 +183,7 @@ } } -.status__display-name, .status__relative-time, .detailed-status__display-name, .detailed-status__datetime, .account__display-name { +.status__display-name, .status__relative-time, .detailed-status__display-name, .detailed-status__datetime, .detailed-status__application, .account__display-name { text-decoration: none; } diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb index 1b33770f4..ca9dd0b7e 100644 --- a/app/controllers/api/v1/apps_controller.rb +++ b/app/controllers/api/v1/apps_controller.rb @@ -4,6 +4,6 @@ class Api::V1::AppsController < ApiController respond_to :json def create - @app = Doorkeeper::Application.create!(name: params[:client_name], redirect_uri: params[:redirect_uris], scopes: (params[:scopes] || Doorkeeper.configuration.default_scopes)) + @app = Doorkeeper::Application.create!(name: params[:client_name], redirect_uri: params[:redirect_uris], scopes: (params[:scopes] || Doorkeeper.configuration.default_scopes), website: params[:website]) end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index f7b4ed610..f033ef6c1 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -52,7 +52,7 @@ 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], visibility: params[:visibility]) + @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], visibility: params[:visibility], application: doorkeeper_token.application) render action: :show end diff --git a/app/models/concerns/application.rb b/app/models/concerns/application.rb new file mode 100644 index 000000000..613be34ee --- /dev/null +++ b/app/models/concerns/application.rb @@ -0,0 +1,8 @@ +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 bc595c93b..8301ae16e 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -7,6 +7,8 @@ class Status < ApplicationRecord enum visibility: [:public, :unlisted, :private], _suffix: :visibility + belongs_to :application, class_name: 'Doorkeeper::Application' + belongs_to :account, inverse_of: :statuses belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account' diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 55405c0db..86a84f512 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -10,7 +10,7 @@ class PostStatusService < BaseService # @option [Enumerable] :media_ids Optional array of media IDs to attach # @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]) + 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 new file mode 100644 index 000000000..30cfd81ab --- /dev/null +++ b/app/views/api/v1/apps/show.rabl @@ -0,0 +1,3 @@ +object @application + +attributes :id, :name, :website \ 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 a3391a67e..a3fc78763 100644 --- a/app/views/api/v1/statuses/_show.rabl +++ b/app/views/api/v1/statuses/_show.rabl @@ -6,6 +6,10 @@ node(:url) { |status| TagManager.instance.url_for(status) } node(:reblogs_count) { |status| defined?(@reblogs_counts_map) ? (@reblogs_counts_map[status.id] || 0) : status.reblogs.count } node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites.count } +child :application do + extends 'api/v1/apps/show' +end + child :account do extends 'api/v1/accounts/show' end diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 32f7c2e40..bc9940915 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -28,6 +28,9 @@ = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: @external_links ? '_blank' : nil, rel: 'noopener' do %span= l(status.created_at) · + = link_to status.application.website, class: 'detailed-status__application', target: @external_links ? '_blank' : nil, rel: 'noooper' do + %span= status.application.name + · %span = fa_icon('retweet') %span= status.reblogs.count diff --git a/db/migrate/20170114194937_add_application_to_statuses.rb b/db/migrate/20170114194937_add_application_to_statuses.rb new file mode 100644 index 000000000..b699db2ac --- /dev/null +++ b/db/migrate/20170114194937_add_application_to_statuses.rb @@ -0,0 +1,5 @@ +class AddApplicationToStatuses < ActiveRecord::Migration[5.0] + def change + add_column :statuses, :application_id, :int + end +end diff --git a/db/migrate/20170114203041_add_website_to_oauth_application.rb b/db/migrate/20170114203041_add_website_to_oauth_application.rb new file mode 100644 index 000000000..ee674be72 --- /dev/null +++ b/db/migrate/20170114203041_add_website_to_oauth_application.rb @@ -0,0 +1,5 @@ +class AddWebsiteToOauthApplication < ActiveRecord::Migration[5.0] + def change + add_column :oauth_applications, :website, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 1cd1258db..37da0c44e 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: 20170112154826) do +ActiveRecord::Schema.define(version: 20170114203041) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -153,6 +153,7 @@ ActiveRecord::Schema.define(version: 20170112154826) do t.datetime "created_at" t.datetime "updated_at" t.boolean "superapp", default: false, null: false + t.string "website" t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree end @@ -259,6 +260,7 @@ 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.integer "application_id" 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 -- 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 'app/models') 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 6cf44ca92c3b92df5bda32adb59258104f1ac9c5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 16 Jan 2017 19:36:32 +0100 Subject: Improve how the list entry Account component looks when target is blocked/follow is requested --- .../javascripts/components/components/account.jsx | 26 +++++++++++++++++----- .../components/containers/account_container.jsx | 12 +++++++++- .../features/ui/containers/modal_container.jsx | 12 +++++++++- app/models/account.rb | 2 +- 4 files changed, 44 insertions(+), 8 deletions(-) (limited to 'app/models') diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx index 814d8a9c8..108401b2f 100644 --- a/app/assets/javascripts/components/components/account.jsx +++ b/app/assets/javascripts/components/components/account.jsx @@ -8,7 +8,9 @@ import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' } + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock' } }); const outerStyle = { @@ -42,7 +44,9 @@ const Account = React.createClass({ account: ImmutablePropTypes.map.isRequired, me: React.PropTypes.number.isRequired, onFollow: React.PropTypes.func.isRequired, - withNote: React.PropTypes.bool + onBlock: React.PropTypes.func.isRequired, + withNote: React.PropTypes.bool, + intl: React.PropTypes.object.isRequired }, getDefaultProps () { @@ -57,6 +61,10 @@ const Account = React.createClass({ this.props.onFollow(this.props.account); }, + handleBlock () { + this.props.onBlock(this.props.account); + }, + render () { const { account, me, withNote, intl } = this.props; @@ -70,10 +78,18 @@ const Account = React.createClass({ note =
{account.get('note')}
; } - if (account.get('id') !== me && account.get('relationship', null) != null) { + if (account.get('id') !== me && account.get('relationship', null) !== null) { const following = account.getIn(['relationship', 'following']); - - buttons = ; + const requested = account.getIn(['relationship', 'requested']); + const blocking = account.getIn(['relationship', 'blocking']); + + if (requested) { + buttons = + } else if (blocking) { + buttons = ; + } else { + buttons = ; + } } return ( diff --git a/app/assets/javascripts/components/containers/account_container.jsx b/app/assets/javascripts/components/containers/account_container.jsx index 1f49f9819..889c0ac4c 100644 --- a/app/assets/javascripts/components/containers/account_container.jsx +++ b/app/assets/javascripts/components/containers/account_container.jsx @@ -3,7 +3,9 @@ import { makeGetAccount } from '../selectors'; import Account from '../components/account'; import { followAccount, - unfollowAccount + unfollowAccount, + blockAccount, + unblockAccount } from '../actions/accounts'; const makeMapStateToProps = () => { @@ -24,6 +26,14 @@ const mapDispatchToProps = (dispatch) => ({ } else { dispatch(followAccount(account.get('id'))); } + }, + + onBlock (account) { + if (account.getIn(['relationship', 'blocking'])) { + dispatch(unblockAccount(account.get('id'))); + } else { + dispatch(blockAccount(account.get('id'))); + } } }); diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx index eeec55ff5..66dfe915e 100644 --- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx +++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx @@ -25,7 +25,17 @@ const imageStyle = { maxHeight: '80vh' }; -const preloader = () => ; +const loadingStyle = { + background: '#373b4a', + width: '400px', + paddingBottom: '120px' +}; + +const preloader = () => ( +
+ +
+); const Modal = React.createClass({ diff --git a/app/models/account.rb b/app/models/account.rb index 5f07615fc..5d6324a7b 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -126,7 +126,7 @@ class Account < ApplicationRecord def save_with_optional_avatar! save! rescue ActiveRecord::RecordInvalid => invalid - if invalid.record.errors[:avatar_file_size] || invalid[:avatar_content_type] + if invalid.record.errors[:avatar_file_size] || invalid.record.errors[:avatar_content_type] self.avatar = nil retry end -- cgit From 8b9206f7d9565ad8b9026e5cc5e085e28ab0117a Mon Sep 17 00:00:00 2001 From: Eugen Date: Thu, 19 Jan 2017 02:14:57 +0100 Subject: Try to avoid any potential loops --- app/models/account.rb | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) (limited to 'app/models') diff --git a/app/models/account.rb b/app/models/account.rb index 5d6324a7b..621a66e24 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -125,13 +125,9 @@ class Account < ApplicationRecord def save_with_optional_avatar! save! - rescue ActiveRecord::RecordInvalid => invalid - if invalid.record.errors[:avatar_file_size] || invalid.record.errors[:avatar_content_type] - self.avatar = nil - retry - end - - raise invalid + rescue ActiveRecord::RecordInvalid + self.avatar = nil + save! end def avatar_remote_url=(url) -- 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 'app/models') 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 'app/models') 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 cea9fb31fefbe3723669082650f32956fabd5123 Mon Sep 17 00:00:00 2001 From: Eugen Date: Sun, 22 Jan 2017 21:50:17 +0100 Subject: Mitigate inconsistent notifications --- app/models/notification.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/models') diff --git a/app/models/notification.rb b/app/models/notification.rb index c0b5c45a8..b7e8c9e71 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -39,9 +39,9 @@ class Notification < ApplicationRecord def target_status case type when :reblog - activity.reblog + activity&.reblog when :favourite, :mention - activity.status + activity&.status end end -- cgit From 6d98a731803eb37ff36f60ff004acfc4c27ae37b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 23 Jan 2017 17:38:38 +0100 Subject: Domain blocks now have varying severity - auto-suspend vs auto-silence --- app/models/domain_block.rb | 2 ++ app/services/block_domain_service.rb | 15 ++++++++------- app/services/follow_remote_account_service.rb | 5 ++++- app/services/suspend_account_service.rb | 1 - app/views/admin/domain_blocks/index.html.haml | 2 ++ .../20170123162658_add_severity_to_domain_blocks.rb | 5 +++++ db/schema.rb | 3 ++- spec/services/block_domain_service_spec.rb | 4 ++-- 8 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 db/migrate/20170123162658_add_severity_to_domain_blocks.rb (limited to 'app/models') diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 9075b90a0..b4606da60 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class DomainBlock < ApplicationRecord + enum severity: [:silence, :suspend] + validates :domain, presence: true, uniqueness: true def self.blocked?(domain) diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index a8fafe412..9518b1fcf 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true class BlockDomainService < BaseService - def call(domain) - DomainBlock.find_or_create_by!(domain: domain) + def call(domain, severity) + DomainBlock.where(domain: domain).first_or_create!(domain: domain, severity: severity) - Account.where(domain: domain).find_each do |account| - if account.subscribed? - account.subscription(api_subscription_url(account.id)).unsubscribe + if severity == :silence + Account.where(domain: domain).update_all(silenced: true) + else + Account.where(domain: domain).find_each do |account| + account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed? + SuspendAccountService.new.call(account) end - - account.destroy! end end end diff --git a/app/services/follow_remote_account_service.rb b/app/services/follow_remote_account_service.rb index d17cf0f45..b39eafc70 100644 --- a/app/services/follow_remote_account_service.rb +++ b/app/services/follow_remote_account_service.rb @@ -35,12 +35,15 @@ class FollowRemoteAccountService < BaseService Rails.logger.debug "Creating new remote account for #{uri}" + domain_block = DomainBlock.find_by(domain: domain) + account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href account.salmon_url = data.link('salmon').href account.url = data.link('http://webfinger.net/rel/profile-page').href account.public_key = magic_key_to_pem(data.link('magic-public-key').href) account.private_key = nil - account.suspended = true if DomainBlock.blocked?(domain) + account.suspended = true if domain_block && domain_block.suspend? + account.silenced = true if domain_block && domain_block.silence? xml = get_feed(account.remote_url) hubs = get_hubs(xml) diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 04a086613..8528ef62a 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -18,7 +18,6 @@ class SuspendAccountService < BaseService @account.media_attachments.destroy_all @account.stream_entries.destroy_all - @account.mentions.destroy_all @account.notifications.destroy_all @account.favourites.destroy_all @account.active_relationships.destroy_all diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml index aedf163f7..dbaeb4716 100644 --- a/app/views/admin/domain_blocks/index.html.haml +++ b/app/views/admin/domain_blocks/index.html.haml @@ -5,10 +5,12 @@ %thead %tr %th Domain + %th Severity %tbody - @blocks.each do |block| %tr %td %samp= block.domain + %td= block.severity = will_paginate @blocks, pagination_options diff --git a/db/migrate/20170123162658_add_severity_to_domain_blocks.rb b/db/migrate/20170123162658_add_severity_to_domain_blocks.rb new file mode 100644 index 000000000..dcbc32a1a --- /dev/null +++ b/db/migrate/20170123162658_add_severity_to_domain_blocks.rb @@ -0,0 +1,5 @@ +class AddSeverityToDomainBlocks < ActiveRecord::Migration[5.0] + def change + add_column :domain_blocks, :severity, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index abe6f1bfe..6d28f059d 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: 20170119214911) do +ActiveRecord::Schema.define(version: 20170123162658) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -58,6 +58,7 @@ ActiveRecord::Schema.define(version: 20170119214911) do t.string "domain", default: "", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "severity", default: 0 t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true, using: :btree end diff --git a/spec/services/block_domain_service_spec.rb b/spec/services/block_domain_service_spec.rb index 9933d016f..d88b3b55c 100644 --- a/spec/services/block_domain_service_spec.rb +++ b/spec/services/block_domain_service_spec.rb @@ -14,7 +14,7 @@ RSpec.describe BlockDomainService do bad_status2 bad_attachment - subject.call('evil.org') + subject.call('evil.org', :suspend) end it 'creates a domain block' do @@ -22,7 +22,7 @@ RSpec.describe BlockDomainService do end it 'removes remote accounts from that domain' do - expect(Account.find_remote('badguy666', 'evil.org')).to be_nil + expect(Account.find_remote('badguy666', 'evil.org').suspended?).to be true end it 'removes the remote accounts\'s statuses and media attachments' do -- cgit From bf0f6eb62d0f5bd1f0d8e4e2a6e9e8fd3b297b6c Mon Sep 17 00:00:00 2001 From: blackle Date: Thu, 12 Jan 2017 23:54:26 -0500 Subject: Implement a click-to-view spoiler system --- .../javascripts/components/actions/compose.jsx | 18 ++++++ .../components/components/status_content.jsx | 18 ++++++ .../features/compose/components/compose_form.jsx | 29 ++++++++- .../compose/containers/compose_form_container.jsx | 12 ++++ .../javascripts/components/reducers/compose.jsx | 10 +++ app/assets/javascripts/extras.jsx | 10 +++ app/assets/stylesheets/components.scss | 44 ++++++++++++-- app/controllers/api/v1/statuses_controller.rb | 2 +- app/helpers/atom_builder_helper.rb | 2 + app/lib/formatter.rb | 17 +++++- app/models/status.rb | 4 +- app/services/post_status_service.rb | 4 ++ app/services/process_hashtags_service.rb | 1 + app/validators/status_length_validator.rb | 15 +++++ app/views/api/v1/statuses/_show.rabl | 2 +- .../20170112041538_add_spoiler_to_statuses.rb | 5 ++ .../20170114014334_add_spoiler_text_to_statuses.rb | 5 ++ db/schema.rb | 71 +--------------------- 18 files changed, 192 insertions(+), 77 deletions(-) create mode 100644 app/validators/status_length_validator.rb create mode 100644 db/migrate/20170112041538_add_spoiler_to_statuses.rb create mode 100644 db/migrate/20170114014334_add_spoiler_text_to_statuses.rb (limited to 'app/models') diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx index 05674ba89..948ccf872 100644 --- a/app/assets/javascripts/components/actions/compose.jsx +++ b/app/assets/javascripts/components/actions/compose.jsx @@ -23,6 +23,8 @@ export const COMPOSE_MOUNT = 'COMPOSE_MOUNT'; export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; +export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; +export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; @@ -68,6 +70,8 @@ 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) { dispatch(submitComposeSuccess({ ...response.data })); @@ -218,6 +222,20 @@ export function changeComposeSensitivity(checked) { }; }; +export function changeComposeSpoilerness(checked) { + return { + type: COMPOSE_SPOILERNESS_CHANGE, + checked + }; +}; + +export function changeComposeSpoilerText(text) { + return { + type: COMPOSE_SPOILER_TEXT_CHANGE, + text + }; +}; + export function changeComposeVisibility(checked) { return { type: COMPOSE_VISIBILITY_CHANGE, diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index f2c88cee0..7287aa836 100644 --- a/app/assets/javascripts/components/components/status_content.jsx +++ b/app/assets/javascripts/components/components/status_content.jsx @@ -18,6 +18,12 @@ const StatusContent = React.createClass({ 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]; @@ -52,6 +58,18 @@ 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); + } + } + }, + onNormalClick (e) { e.stopPropagation(); }, 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 80cb38e16..84d273299 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -14,6 +14,7 @@ import { Motion, spring } from 'react-motion'; const messages = defineMessages({ placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, + spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' }, publish: { id: 'compose_form.publish', defaultMessage: 'Publish' } }); @@ -25,6 +26,8 @@ const ComposeForm = React.createClass({ suggestion_token: React.PropTypes.string, suggestions: ImmutablePropTypes.list, sensitive: React.PropTypes.bool, + spoiler: React.PropTypes.bool, + spoiler_text: React.PropTypes.string, unlisted: React.PropTypes.bool, private: React.PropTypes.bool, fileDropDate: React.PropTypes.instanceOf(Date), @@ -40,6 +43,8 @@ const ComposeForm = React.createClass({ onFetchSuggestions: React.PropTypes.func.isRequired, onSuggestionSelected: React.PropTypes.func.isRequired, onChangeSensitivity: React.PropTypes.func.isRequired, + onChangeSpoilerness: React.PropTypes.func.isRequired, + onChangeSpoilerText: React.PropTypes.func.isRequired, onChangeVisibility: React.PropTypes.func.isRequired, onChangeListability: React.PropTypes.func.isRequired, }, @@ -77,6 +82,15 @@ const ComposeForm = React.createClass({ this.props.onChangeSensitivity(e.target.checked); }, + handleChangeSpoilerness (e) { + this.props.onChangeSpoilerness(e.target.checked); + this.props.onChangeSpoilerText(''); + }, + + handleChangeSpoilerText (e) { + this.props.onChangeSpoilerText(e.target.value); + }, + handleChangeVisibility (e) { this.props.onChangeVisibility(e.target.checked); }, @@ -115,6 +129,14 @@ const ComposeForm = React.createClass({ return (
+ + {({ opacity, height }) => +
+ +
+ } +
+ {replyArea}
-
+
@@ -142,6 +164,11 @@ const ComposeForm = React.createClass({ + + {({ opacity, height }) =>