From 442fdbfc5309f46c23a073829e5fe16d10c7c6ca Mon Sep 17 00:00:00 2001 From: Kit Redgrave Date: Sun, 5 Feb 2017 19:51:56 -0600 Subject: Mute button progress so far. WIP, doesn't entirely work correctly. --- app/models/account.rb | 21 +++++++++++++++++++++ app/models/mute.rb | 32 ++++++++++++++++++++++++++++++++ app/models/status.rb | 13 +++++++++---- 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 app/models/mute.rb (limited to 'app/models') diff --git a/app/models/account.rb b/app/models/account.rb index a93a0668a..2fa6bab71 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -46,6 +46,10 @@ class Account < ApplicationRecord has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account + # Mute relationships + has_many :mute_relationships, class_name: 'Mute', foreign_key: 'account_id', dependent: :destroy + has_many :muting, -> { order('mutes.id desc') }, through: :mute_relationships, source: :target_account + # Media has_many :media_attachments, dependent: :destroy @@ -73,6 +77,10 @@ class Account < ApplicationRecord block_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) end + def mute!(other_account) + mute_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) + end + def unfollow!(other_account) follow = active_relationships.find_by(target_account: other_account) follow&.destroy @@ -83,6 +91,11 @@ class Account < ApplicationRecord block&.destroy end + def unmute!(other_account) + mute = mute_relationships.find_by(target_account: other_account) + mute&.destroy + end + def following?(other_account) following.include?(other_account) end @@ -91,6 +104,10 @@ class Account < ApplicationRecord blocking.include?(other_account) end + def muting?(other_account) + muting.include?(other_account) + end + def requested?(other_account) follow_requests.where(target_account: other_account).exists? end @@ -188,6 +205,10 @@ class Account < ApplicationRecord follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) end + def muting_map(target_account_ids, account_id) + follow_mapping(Mute.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) + end + 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 diff --git a/app/models/mute.rb b/app/models/mute.rb new file mode 100644 index 000000000..505196453 --- /dev/null +++ b/app/models/mute.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Mute < ApplicationRecord + include Paginable + include Streamable + + belongs_to :account + belongs_to :target_account, class_name: 'Account' + + validates :account, :target_account, presence: true + validates :account_id, uniqueness: { scope: :target_account_id } + + def verb + destroyed? ? :unmute : :mute + end + + def target + target_account + end + + def object_type + :person + end + + def hidden? + true + end + + def title + destroyed? ? "#{account.acct} is no longer muting #{target_account.acct}" : "#{account.acct} muted #{target_account.acct}" + end +end diff --git a/app/models/status.rb b/app/models/status.rb index 1b40897f3..b3424f36d 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -103,7 +103,10 @@ class Status < ApplicationRecord class << self def as_home_timeline(account) - where(account: [account] + account.following) + muted = Mute.where(account: account).pluck(:target_account_id) + query = where(account: [account] + account.following) + query = query.where('statuses.account_id NOT IN (?)', muted) unless muted.empty? + query end def as_public_timeline(account = nil, local_only = false) @@ -169,8 +172,10 @@ class Status < ApplicationRecord def filter_timeline(query, account) blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) - query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? - query = query.where('accounts.silenced = TRUE') if account.silenced? + muted = Mute.where(account: account).pluck(:target_account_id) + query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? # Only give us statuses from people we haven't blocked + query = query.where('statuses.account_id NOT IN (?)', muted) unless muted.empty? # and out of those, only people we haven't muted + query = query.where('accounts.silenced = TRUE') if account.silenced? # and if we're hellbanned, only people who are also hellbanned query end @@ -192,6 +197,6 @@ class Status < ApplicationRecord private def filter_from_context?(status, account) - account&.blocking?(status.account_id) || (status.account.silenced? && !account&.following?(status.account_id)) || !status.permitted?(account) + account&.blocking?(status.account_id) || account&.muting?(status.account_id) || (status.account.silenced? && !account&.following?(status.account_id)) || !status.permitted?(account) end end -- cgit From c64a1c25c4e9a07c694863a38334ed66e368752e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 2 Mar 2017 18:49:32 +0100 Subject: Fix #231 - Muting --- .../features/account/components/action_bar.jsx | 28 ++++++------ .../account_timeline/components/header.jsx | 4 +- .../compose/components/emoji_picker_dropdown.jsx | 2 +- .../javascripts/components/reducers/timelines.jsx | 4 +- app/lib/feed_manager.rb | 53 ++++++++++------------ app/models/mute.rb | 21 --------- app/models/status.rb | 11 ++--- app/services/unmute_service.rb | 2 + streaming/index.js | 2 +- 9 files changed, 50 insertions(+), 77 deletions(-) (limited to 'app/models') diff --git a/app/assets/javascripts/components/features/account/components/action_bar.jsx b/app/assets/javascripts/components/features/account/components/action_bar.jsx index 60947767c..80a32d3e2 100644 --- a/app/assets/javascripts/components/features/account/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx @@ -9,9 +9,9 @@ const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, report: { id: 'account.report', defaultMessage: 'Report @{name}' }, disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' } @@ -54,15 +54,19 @@ const ActionBar = React.createClass({ if (account.get('id') === me) { menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); - } else if (account.getIn(['relationship', 'blocking'])) { - menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); - } else if (account.getIn(['relationship', 'following'])) { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); } else { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); - } + if (account.getIn(['relationship', 'muting'])) { + menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); + } else { + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); + } + + if (account.getIn(['relationship', 'blocking'])) { + menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); + } else { + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); + } - if (account.get('id') !== me) { menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); } @@ -70,12 +74,6 @@ const ActionBar = React.createClass({ extraInfo = *; } - if (account.getIn(['relationship', 'muting'])) { - menu.push({ text: intl.formatMessage(messages.unmute), action: this.props.onMute }); - } else { - menu.push({ text: intl.formatMessage(messages.mute), action: this.props.onMute }); - } - return (
diff --git a/app/assets/javascripts/components/features/account_timeline/components/header.jsx b/app/assets/javascripts/components/features/account_timeline/components/header.jsx index f436a180b..99a10562e 100644 --- a/app/assets/javascripts/components/features/account_timeline/components/header.jsx +++ b/app/assets/javascripts/components/features/account_timeline/components/header.jsx @@ -15,8 +15,8 @@ const Header = React.createClass({ onFollow: React.PropTypes.func.isRequired, onBlock: React.PropTypes.func.isRequired, onMention: React.PropTypes.func.isRequired, - onReport: React.PropTypes.func.isRequired - onMute: React.PropTypes.func.isRequired, + onReport: React.PropTypes.func.isRequired, + onMute: React.PropTypes.func.isRequired }, mixins: [PureRenderMixin], diff --git a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx index 6419ff08a..3a454a5fb 100644 --- a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx @@ -36,7 +36,7 @@ const EmojiPickerDropdown = React.createClass({ return ( - + diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index 6472ac6a0..c67d05423 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -22,7 +22,8 @@ import { ACCOUNT_TIMELINE_EXPAND_REQUEST, ACCOUNT_TIMELINE_EXPAND_SUCCESS, ACCOUNT_TIMELINE_EXPAND_FAIL, - ACCOUNT_BLOCK_SUCCESS + ACCOUNT_BLOCK_SUCCESS, + ACCOUNT_MUTE_SUCCESS } from '../actions/accounts'; import { CONTEXT_FETCH_SUCCESS @@ -295,6 +296,7 @@ export default function timelines(state = initialState, action) { case ACCOUNT_TIMELINE_EXPAND_SUCCESS: return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: return filterTimelines(state, action.relationship, action.statuses); case TIMELINE_SCROLL_TOP: return updateTop(state, action.timeline, action.top); diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index d52260713..3a26c5c05 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -22,8 +22,18 @@ class FeedManager end def push(timeline_type, account, status) - redis.zadd(key(timeline_type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) - trim(timeline_type, account.id) + timeline_key = key(timeline_type, account.id) + + if status.reblog? + # If the original status is within 40 statuses from top, do not re-insert it into the feed + rank = redis.zrevrank(timeline_key, status.reblog_of_id) + return if !rank.nil? && rank < 40 + redis.zadd(timeline_key, status.id, status.reblog_of_id) + else + redis.zadd(timeline_key, status.id, status.id) + trim(timeline_type, account.id) + end + broadcast(account.id, event: 'update', payload: inline_render(account, 'api/v1/statuses/show', status)) end @@ -85,47 +95,34 @@ class FeedManager end def filter_from_home?(status, receiver) - should_filter = receiver.muting?(status.account_id) # Filter if I'm muting this person + return true if receiver.muting?(status.account) + + should_filter = false - if status.reply? && status.in_reply_to_id.nil? # Filter out replies to nobody + if status.reply? && status.in_reply_to_id.nil? should_filter = true - elsif status.reply? && !status.in_reply_to_account_id.nil? # If it's a reply - should_filter = !receiver.following?(status.in_reply_to_account) # filter if I'm not following the person it's a reply to + elsif status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply + should_filter = !receiver.following?(status.in_reply_to_account) # and I'm not following the person it's a reply to should_filter &&= !(receiver.id == status.in_reply_to_account_id) # and it's not a reply to me should_filter &&= !(status.account_id == status.in_reply_to_account_id) # and it's not a self-reply - elsif status.reblog? # If it's a reblog - should_filter = receiver.blocking?(status.reblog.account) # filter if I'm blocking the reblogged person - should_filter ||= receiver.muting?(status.reblog.account) # or if I'm muting the reblogged person + elsif status.reblog? # Filter out a reblog + should_filter = receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person + should_filter ||= receiver.muting?(status.reblog.account) # or muting that person end - should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # Filter if it mentions someone I blocked + should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # or if it mentions someone I blocked + should_filter end def filter_from_mentions?(status, receiver) - should_filter = receiver.id == status.account_id # Filter out if I'm mentioning myself + should_filter = receiver.id == status.account_id # Filter if I'm mentioning myself should_filter ||= receiver.blocking?(status.account) # or it's from someone I blocked should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked should_filter ||= (status.account.silenced? && !receiver.following?(status.account)) # of if the account is silenced and I'm not following them - if status.reply? && !status.in_reply_to_account_id.nil? - should_filter ||= receiver.blocking?(status.in_reply_to_account) # or if it's a reply to a user I blocked - end - - should_filter - end - - def filter_from_public?(status, receiver) - should_filter = receiver.blocking?(status.account) # Filter out if I'm blocking that account - should_filter ||= receiver.muting?(status.account_id) # or if I'm muting this person - should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked - if status.reply? && !status.in_reply_to_account_id.nil? # or it's a reply - should_filter ||= receiver.blocking?(status.in_reply_to_account) # to somebody I've blocked - should_filter ||= receiver.muting?(status.in_reply_to_account) # or to somebody I'm muting - elsif status.reblog? # or if it's a reblog - should_filter ||= receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person - should_filter ||= receiver.muting?(status.reblog.account) # or if I'm muting the reblogged person + should_filter ||= receiver.blocking?(status.in_reply_to_account) # to a user I blocked end should_filter diff --git a/app/models/mute.rb b/app/models/mute.rb index 505196453..a5b334c85 100644 --- a/app/models/mute.rb +++ b/app/models/mute.rb @@ -2,31 +2,10 @@ class Mute < ApplicationRecord include Paginable - include Streamable belongs_to :account belongs_to :target_account, class_name: 'Account' validates :account, :target_account, presence: true validates :account_id, uniqueness: { scope: :target_account_id } - - def verb - destroyed? ? :unmute : :mute - end - - def target - target_account - end - - def object_type - :person - end - - def hidden? - true - end - - def title - destroyed? ? "#{account.acct} is no longer muting #{target_account.acct}" : "#{account.acct} muted #{target_account.acct}" - end end diff --git a/app/models/status.rb b/app/models/status.rb index b3424f36d..e5e740360 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -103,10 +103,7 @@ class Status < ApplicationRecord class << self def as_home_timeline(account) - muted = Mute.where(account: account).pluck(:target_account_id) - query = where(account: [account] + account.following) - query = query.where('statuses.account_id NOT IN (?)', muted) unless muted.empty? - query + where(account: [account] + account.following) end def as_public_timeline(account = nil, local_only = false) @@ -171,10 +168,8 @@ class Status < ApplicationRecord private def filter_timeline(query, account) - blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) - muted = Mute.where(account: account).pluck(:target_account_id) - query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? # Only give us statuses from people we haven't blocked - query = query.where('statuses.account_id NOT IN (?)', muted) unless muted.empty? # and out of those, only people we haven't muted + blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) + Mute.where(account: account).pluck(:target_account_id) + query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? # Only give us statuses from people we haven't blocked, or muted, or that have blocked us query = query.where('accounts.silenced = TRUE') if account.silenced? # and if we're hellbanned, only people who are also hellbanned query end diff --git a/app/services/unmute_service.rb b/app/services/unmute_service.rb index ed268b7c5..6aeea358f 100644 --- a/app/services/unmute_service.rb +++ b/app/services/unmute_service.rb @@ -5,5 +5,7 @@ class UnmuteService < BaseService return unless account.muting?(target_account) account.unmute!(target_account) + + MergeWorker.perform_async(target_account.id, account.id) if account.following?(target_account) end end diff --git a/streaming/index.js b/streaming/index.js index 125b35bb4..0f838e411 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -164,7 +164,7 @@ const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false) const unpackedPayload = JSON.parse(payload) const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)).concat(unpackedPayload.reblog ? [unpackedPayload.reblog.account.id] : []) - client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => { + client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)}) UNION SELECT target_account_id FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => { done() if (err) { -- cgit From 6b81d100306259cd17b38d3f0f9dec0f0fb5b5d9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 3 Mar 2017 23:45:48 +0100 Subject: Add digest e-mails --- app/controllers/api_controller.rb | 1 + app/controllers/settings/preferences_controller.rb | 3 ++- app/lib/formatter.rb | 5 +++++ app/mailers/notification_mailer.rb | 13 +++++++++++++ app/models/setting.rb | 1 - app/models/user.rb | 7 ++++--- app/views/layouts/mailer.text.erb | 2 +- app/views/notification_mailer/_status.text.erb | 4 ++-- app/views/notification_mailer/digest.text.erb | 15 +++++++++++++++ app/views/notification_mailer/favourite.text.erb | 4 ++-- app/views/notification_mailer/follow.text.erb | 4 ++-- app/views/notification_mailer/follow_request.text.erb | 4 ++-- app/views/notification_mailer/mention.text.erb | 4 ++-- app/views/notification_mailer/reblog.text.erb | 4 ++-- app/views/settings/preferences/show.html.haml | 1 + app/workers/digest_mailer_worker.rb | 14 ++++++++++++++ config/application.rb | 7 ------- config/environments/production.rb | 7 +++++++ config/locales/en.yml | 11 +++++++++++ config/locales/simple_form.en.yml | 1 + config/settings.yml | 1 + .../20170303212857_add_last_emailed_at_to_users.rb | 5 +++++ db/schema.rb | 3 ++- lib/tasks/mastodon.rake | 11 ++++++++++- spec/mailers/previews/notification_mailer_preview.rb | 17 ++++++++++++----- 25 files changed, 117 insertions(+), 32 deletions(-) create mode 100644 app/views/notification_mailer/digest.text.erb create mode 100644 app/workers/digest_mailer_worker.rb create mode 100644 db/migrate/20170303212857_add_last_emailed_at_to_users.rb (limited to 'app/models') diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index c2002cb79..db16f82e5 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -79,6 +79,7 @@ class ApiController < ApplicationController def require_user! current_resource_owner + set_user_activity rescue ActiveRecord::RecordNotFound render json: { error: 'This method requires an authenticated user' }, status: 422 end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index b7479bf8c..60400e465 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -14,6 +14,7 @@ class Settings::PreferencesController < ApplicationController reblog: user_params[:notification_emails][:reblog] == '1', favourite: user_params[:notification_emails][:favourite] == '1', mention: user_params[:notification_emails][:mention] == '1', + digest: user_params[:notification_emails][:digest] == '1', } current_user.settings['interactions'] = { @@ -33,6 +34,6 @@ class Settings::PreferencesController < ApplicationController private def user_params - params.require(:user).permit(:locale, :setting_default_privacy, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention], interactions: [:must_be_follower, :must_be_following]) + params.require(:user).permit(:locale, :setting_default_privacy, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention, :digest], interactions: [:must_be_follower, :must_be_following]) end end diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index e353c3504..b58952ae0 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -29,6 +29,11 @@ class Formatter sanitize(html, tags: %w(a br p span), attributes: %w(href rel class)) end + def plaintext(status) + return status.text if status.local? + strip_tags(status.text) + end + def simplified_format(account) return reformat(account.note) unless account.local? diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index a1b084682..bf4c16e43 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -49,4 +49,17 @@ class NotificationMailer < ApplicationMailer mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) end end + + def digest(recipient, opts = {}) + @me = recipient + @since = opts[:since] || @me.user.last_emailed_at || @me.user.current_sign_in_at + @notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since) + @follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count + + return if @notifications.empty? + + I18n.with_locale(@me.user.locale || I18n.default_locale) do + mail to: @me.user.email, subject: I18n.t('notification_mailer.digest.subject', count: @notifications.size) + end + end end diff --git a/app/models/setting.rb b/app/models/setting.rb index 3796253d4..31e1ee198 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -2,7 +2,6 @@ class Setting < RailsSettings::Base source Rails.root.join('config/settings.yml') - namespace Rails.env def to_param var diff --git a/app/models/user.rb b/app/models/user.rb index 08aac2679..bf2916d90 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -14,9 +14,10 @@ class User < ApplicationRecord validates :locale, inclusion: I18n.available_locales.map(&:to_s), unless: 'locale.nil?' validates :email, email: true - scope :prolific, -> { joins('inner join statuses on statuses.account_id = users.account_id').select('users.*, count(statuses.id) as statuses_count').group('users.id').order('statuses_count desc') } - scope :recent, -> { order('id desc') } - scope :admins, -> { where(admin: true) } + scope :prolific, -> { joins('inner join statuses on statuses.account_id = users.account_id').select('users.*, count(statuses.id) as statuses_count').group('users.id').order('statuses_count desc') } + scope :recent, -> { order('id desc') } + scope :admins, -> { where(admin: true) } + scope :confirmed, -> { where.not(confirmed_at: nil) } def send_devise_notification(notification, *args) devise_mailer.send(notification, self, *args).deliver_later diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb index ae52173b5..21bf444c3 100644 --- a/app/views/layouts/mailer.text.erb +++ b/app/views/layouts/mailer.text.erb @@ -1,5 +1,5 @@ <%= yield %> - --- <%= t('application_mailer.signature', instance: Rails.configuration.x.local_domain) %> +<%= t('application_mailer.settings', link: settings_preferences_url) %> diff --git a/app/views/notification_mailer/_status.text.erb b/app/views/notification_mailer/_status.text.erb index b089a7b73..85a0136b7 100644 --- a/app/views/notification_mailer/_status.text.erb +++ b/app/views/notification_mailer/_status.text.erb @@ -1,3 +1,3 @@ -<%= strip_tags(@status.content) %> +<%= raw Formatter.instance.plaintext(status) %> -<%= web_url("statuses/#{@status.id}") %> +<%= raw t('application_mailer.view')%> <%= web_url("statuses/#{status.id}") %> diff --git a/app/views/notification_mailer/digest.text.erb b/app/views/notification_mailer/digest.text.erb new file mode 100644 index 000000000..95aed6793 --- /dev/null +++ b/app/views/notification_mailer/digest.text.erb @@ -0,0 +1,15 @@ +<%= display_name(@me) %>, + +<%= raw t('notification_mailer.digest.body', since: @since, instance: root_url) %> +<% @notifications.each do |notification| %> + +* <%= raw t('notification_mailer.digest.mention', name: notification.from_account.acct) %> + + <%= raw Formatter.instance.plaintext(notification.target_status) %> + + <%= raw t('application_mailer.view')%> <%= web_url("statuses/#{notification.target_status.id}") %> +<% end %> +<% if @follows_since > 0 %> + +<%= raw t('notification_mailer.digest.new_followers_summary', count: @follows_since) %> +<% end %> diff --git a/app/views/notification_mailer/favourite.text.erb b/app/views/notification_mailer/favourite.text.erb index b2e1e3e9e..99852592f 100644 --- a/app/views/notification_mailer/favourite.text.erb +++ b/app/views/notification_mailer/favourite.text.erb @@ -1,5 +1,5 @@ <%= display_name(@me) %>, -<%= t('notification_mailer.favourite.body', name: @account.acct) %> +<%= raw t('notification_mailer.favourite.body', name: @account.acct) %> -<%= render partial: 'status' %> +<%= render partial: 'status', locals: { status: @status } %> diff --git a/app/views/notification_mailer/follow.text.erb b/app/views/notification_mailer/follow.text.erb index 4b2ec142c..af41a3080 100644 --- a/app/views/notification_mailer/follow.text.erb +++ b/app/views/notification_mailer/follow.text.erb @@ -1,5 +1,5 @@ <%= display_name(@me) %>, -<%= t('notification_mailer.follow.body', name: @account.acct) %> +<%= raw t('notification_mailer.follow.body', name: @account.acct) %> -<%= web_url("accounts/#{@account.id}") %> +<%= raw t('application_mailer.view')%> <%= web_url("accounts/#{@account.id}") %> diff --git a/app/views/notification_mailer/follow_request.text.erb b/app/views/notification_mailer/follow_request.text.erb index c0d38ec67..49087a575 100644 --- a/app/views/notification_mailer/follow_request.text.erb +++ b/app/views/notification_mailer/follow_request.text.erb @@ -1,5 +1,5 @@ <%= display_name(@me) %>, -<%= t('notification_mailer.follow_request.body', name: @account.acct) %> +<%= raw t('notification_mailer.follow_request.body', name: @account.acct) %> -<%= web_url("follow_requests") %> +<%= raw t('application_mailer.view')%> <%= web_url("follow_requests") %> diff --git a/app/views/notification_mailer/mention.text.erb b/app/views/notification_mailer/mention.text.erb index 31a294bb9..c0d4be1d8 100644 --- a/app/views/notification_mailer/mention.text.erb +++ b/app/views/notification_mailer/mention.text.erb @@ -1,5 +1,5 @@ <%= display_name(@me) %>, -<%= t('notification_mailer.mention.body', name: @status.account.acct) %> +<%= raw t('notification_mailer.mention.body', name: @status.account.acct) %> -<%= render partial: 'status' %> +<%= render partial: 'status', locals: { status: @status } %> diff --git a/app/views/notification_mailer/reblog.text.erb b/app/views/notification_mailer/reblog.text.erb index 7af8052ca..c32b48650 100644 --- a/app/views/notification_mailer/reblog.text.erb +++ b/app/views/notification_mailer/reblog.text.erb @@ -1,5 +1,5 @@ <%= display_name(@me) %>, -<%= t('notification_mailer.reblog.body', name: @account.acct) %> +<%= raw t('notification_mailer.reblog.body', name: @account.acct) %> -<%= render partial: 'status' %> +<%= render partial: 'status', locals: { status: @status } %> diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index aee0540d2..a17279b1e 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -16,6 +16,7 @@ = ff.input :reblog, as: :boolean, wrapper: :with_label = ff.input :favourite, as: :boolean, wrapper: :with_label = ff.input :mention, as: :boolean, wrapper: :with_label + = ff.input :digest, as: :boolean, wrapper: :with_label = f.simple_fields_for :interactions, hash_to_object(current_user.settings.interactions) do |ff| = ff.input :must_be_follower, as: :boolean, wrapper: :with_label diff --git a/app/workers/digest_mailer_worker.rb b/app/workers/digest_mailer_worker.rb new file mode 100644 index 000000000..dedb21e4e --- /dev/null +++ b/app/workers/digest_mailer_worker.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class DigestMailerWorker + include Sidekiq::Worker + + sidekiq_options queue: 'mailers' + + def perform(user_id) + user = User.find(user_id) + return unless user.settings.notification_emails['digest'] + NotificationMailer.digest(user.account).deliver_now! + user.touch(:last_emailed_at) + end +end diff --git a/config/application.rb b/config/application.rb index 8da5ade3c..1ea65619c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -49,12 +49,5 @@ module Mastodon Doorkeeper::AuthorizedApplicationsController.layout 'admin' Doorkeeper::Application.send :include, ApplicationExtension end - - config.action_dispatch.default_headers = { - 'Server' => 'Mastodon', - 'X-Frame-Options' => 'DENY', - 'X-Content-Type-Options' => 'nosniff', - 'X-XSS-Protection' => '1; mode=block', - } end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 67ff63914..dc5dd4afd 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -109,4 +109,11 @@ Rails.application.configure do config.to_prepare do StatsD.backend = StatsD::Instrument::Backends::NullBackend.new if ENV['STATSD_ADDR'].blank? end + + config.action_dispatch.default_headers = { + 'Server' => 'Mastodon', + 'X-Frame-Options' => 'DENY', + 'X-Content-Type-Options' => 'nosniff', + 'X-XSS-Protection' => '1; mode=block', + } end diff --git a/config/locales/en.yml b/config/locales/en.yml index 6da30acda..f11a689e4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,6 +29,8 @@ en: unfollow: Unfollow application_mailer: signature: Mastodon notifications from %{instance} + settings: 'Change e-mail preferences: %{link}' + view: 'View:' applications: invalid_url: The provided URL is invalid auth: @@ -83,6 +85,15 @@ en: reblog: body: 'Your status was boosted by %{name}:' subject: "%{name} boosted your status" + digest: + subject: + one: "1 new notification since your last visit 🐘" + other: "%{count} new notifications since your last visit 🐘" + body: 'Here is a brief summary of what you missed on %{instance} since your last visit on %{since}:' + mention: "%{name} mentioned you in:" + new_followers_summary: + one: You have acquired one new follower! Yay! + other: You have gotten %{count} new followers! Amazing! pagination: next: Next prev: Prev diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 4d1758f82..170af01cf 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -34,6 +34,7 @@ en: follow_request: Send e-mail when someone requests to follow you mention: Send e-mail when someone mentions you reblog: Send e-mail when someone reblogs your status + digest: Send digest e-mails 'no': 'No' required: mark: "*" diff --git a/config/settings.yml b/config/settings.yml index 71ce12e63..6ae9217a4 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -11,6 +11,7 @@ defaults: &defaults favourite: false mention: false follow_request: true + digest: true interactions: must_be_follower: false must_be_following: false diff --git a/db/migrate/20170303212857_add_last_emailed_at_to_users.rb b/db/migrate/20170303212857_add_last_emailed_at_to_users.rb new file mode 100644 index 000000000..9ae3da4fb --- /dev/null +++ b/db/migrate/20170303212857_add_last_emailed_at_to_users.rb @@ -0,0 +1,5 @@ +class AddLastEmailedAtToUsers < ActiveRecord::Migration[5.0] + def change + add_column :users, :last_emailed_at, :datetime, null: true, default: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index c2d88ac13..8cc3bd8e3 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: 20170301222600) do +ActiveRecord::Schema.define(version: 20170303212857) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -283,6 +283,7 @@ ActiveRecord::Schema.define(version: 20170301222600) do t.string "encrypted_otp_secret_salt" t.integer "consumed_timestep" t.boolean "otp_required_for_login" + t.datetime "last_emailed_at" t.index ["account_id"], name: "index_users_on_account_id", using: :btree t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree t.index ["email"], name: "index_users_on_email", unique: true, using: :btree diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 8482d4124..bb10410b5 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -43,7 +43,7 @@ namespace :mastodon do namespace :feeds do desc 'Clear timelines of inactive users' task clear: :environment do - User.where('current_sign_in_at < ?', 14.days.ago).find_each do |user| + User.confirmed.where('current_sign_in_at < ?', 14.days.ago).find_each do |user| Redis.current.del(FeedManager.instance.key(:home, user.account_id)) end end @@ -53,4 +53,13 @@ namespace :mastodon do Redis.current.keys('feed:*').each { |key| Redis.current.del(key) } end end + + namespace :emails do + desc 'Send out digest e-mails' + task digest: :environment do + User.confirmed.joins(:account).where(accounts: { silenced: false, suspended: false }).where('current_sign_in_at < ?', 20.days.ago).find_each do |user| + DigestMailerWorker.perform_async(user.id) + end + end + end end diff --git a/spec/mailers/previews/notification_mailer_preview.rb b/spec/mailers/previews/notification_mailer_preview.rb index 8fc8d0d34..a08a80d17 100644 --- a/spec/mailers/previews/notification_mailer_preview.rb +++ b/spec/mailers/previews/notification_mailer_preview.rb @@ -1,24 +1,31 @@ # Preview all emails at http://localhost:3000/rails/mailers/notification_mailer class NotificationMailerPreview < ActionMailer::Preview - # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/mention def mention - # NotificationMailer.mention + m = Mention.last + NotificationMailer.mention(m.account, Notification.find_by(activity: m)) end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/follow def follow - # NotificationMailer.follow + f = Follow.last + NotificationMailer.follow(f.target_account, Notification.find_by(activity: f)) end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/favourite def favourite - # NotificationMailer.favourite + f = Favourite.last + NotificationMailer.favourite(f.status.account, Notification.find_by(activity: f)) end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/reblog def reblog - # NotificationMailer.reblog + r = Status.where.not(reblog_of_id: nil).first + NotificationMailer.reblog(r.reblog.account, Notification.find_by(activity: r)) end + # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/digest + def digest + NotificationMailer.digest(Account.first, since: 90.days.ago) + end end -- cgit From caf5b8e9757679b93b9a34b0c55f43cb47910201 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 4 Mar 2017 22:17:10 +0100 Subject: Fix #431 - convert gif to webm during upload. Web UI treats them like it did before. In the API, attachments now can be either image, video or gifv. Gifv is to be treated like images in terms of behaviour, but are videos by file type. --- .../javascripts/components/actions/accounts.jsx | 7 +- .../components/extended_video_player.jsx | 21 ++ .../components/components/media_gallery.jsx | 232 ++++++++++++++------- .../javascripts/components/components/status.jsx | 4 +- .../components/components/video_player.jsx | 4 +- .../features/status/components/detailed_status.jsx | 2 +- .../features/ui/containers/modal_container.jsx | 22 +- app/assets/stylesheets/stream_entries.scss | 31 ++- app/models/media_attachment.rb | 69 +++--- app/views/api/v1/media/create.rabl | 4 +- .../stream_entries/_detailed_status.html.haml | 6 +- app/views/stream_entries/_media.html.haml | 4 + app/views/stream_entries/_simple_status.html.haml | 15 +- config/application.rb | 5 +- ...20170304202101_add_type_to_media_attachments.rb | 12 ++ db/schema.rb | 3 +- lib/paperclip/gif_transcoder.rb | 21 ++ 17 files changed, 325 insertions(+), 137 deletions(-) create mode 100644 app/assets/javascripts/components/components/extended_video_player.jsx create mode 100644 app/views/stream_entries/_media.html.haml create mode 100644 db/migrate/20170304202101_add_type_to_media_attachments.rb create mode 100644 lib/paperclip/gif_transcoder.rb (limited to 'app/models') diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx index 8af0b15d8..05fa8e68d 100644 --- a/app/assets/javascripts/components/actions/accounts.jsx +++ b/app/assets/javascripts/components/actions/accounts.jsx @@ -75,11 +75,16 @@ export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; export function fetchAccount(id) { return (dispatch, getState) => { + dispatch(fetchRelationships([id])); + + if (getState().getIn(['accounts', id], null) !== null) { + return; + } + dispatch(fetchAccountRequest(id)); api(getState).get(`/api/v1/accounts/${id}`).then(response => { dispatch(fetchAccountSuccess(response.data)); - dispatch(fetchRelationships([id])); }).catch(error => { dispatch(fetchAccountFail(id, error)); }); diff --git a/app/assets/javascripts/components/components/extended_video_player.jsx b/app/assets/javascripts/components/components/extended_video_player.jsx new file mode 100644 index 000000000..66e5dee16 --- /dev/null +++ b/app/assets/javascripts/components/components/extended_video_player.jsx @@ -0,0 +1,21 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; + +const ExtendedVideoPlayer = React.createClass({ + + propTypes: { + src: React.PropTypes.string.isRequired + }, + + mixins: [PureRenderMixin], + + render () { + return ( +
+
+ ); + }, + +}); + +export default ExtendedVideoPlayer; diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx index b0e397e80..cd2394023 100644 --- a/app/assets/javascripts/components/components/media_gallery.jsx +++ b/app/assets/javascripts/components/components/media_gallery.jsx @@ -43,6 +43,141 @@ const spoilerButtonStyle = { zIndex: '100' }; +const itemStyle = { + boxSizing: 'border-box', + position: 'relative', + float: 'left', + border: 'none', + display: 'block' +}; + +const thumbStyle = { + display: 'block', + width: '100%', + height: '100%', + textDecoration: 'none', + backgroundSize: 'cover', + cursor: 'zoom-in' +}; + +const gifvThumbStyle = { + position: 'relative', + zIndex: '1', + width: '100%', + height: '100%', + objectFit: 'cover', + top: '50%', + transform: 'translateY(-50%)', + cursor: 'zoom-in' +}; + +const Item = React.createClass({ + + propTypes: { + attachment: ImmutablePropTypes.map.isRequired, + index: React.PropTypes.number.isRequired, + size: React.PropTypes.number.isRequired, + onClick: React.PropTypes.func.isRequired + }, + + mixins: [PureRenderMixin], + + handleClick (e) { + const { index, onClick } = this.props; + + if (e.button === 0) { + e.preventDefault(); + onClick(index); + } + + e.stopPropagation(); + }, + + render () { + const { attachment, index, size } = this.props; + + let width = 50; + let height = 100; + let top = 'auto'; + let left = 'auto'; + let bottom = 'auto'; + let right = 'auto'; + + if (size === 1) { + width = 100; + } + + if (size === 4 || (size === 3 && index > 0)) { + height = 50; + } + + if (size === 2) { + if (index === 0) { + right = '2px'; + } else { + left = '2px'; + } + } else if (size === 3) { + if (index === 0) { + right = '2px'; + } else if (index > 0) { + left = '2px'; + } + + if (index === 1) { + bottom = '2px'; + } else if (index > 1) { + top = '2px'; + } + } else if (size === 4) { + if (index === 0 || index === 2) { + right = '2px'; + } + + if (index === 1 || index === 3) { + left = '2px'; + } + + if (index < 2) { + bottom = '2px'; + } else { + top = '2px'; + } + } + + let thumbnail = ''; + + if (attachment.get('type') === 'image') { + thumbnail = ( + + ); + } else if (attachment.get('type') === 'gifv') { + thumbnail = ( + - ); - }); + children = media.take(4).map((attachment, i) => ); } return (
-
+
+ {children}
); diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx index 110d26c6d..fb49db069 100644 --- a/app/assets/javascripts/components/components/status.jsx +++ b/app/assets/javascripts/components/components/status.jsx @@ -74,8 +74,8 @@ const Status = React.createClass({ } if (status.get('media_attachments').size > 0 && !this.props.muted) { - if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = ; + if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) { + media = ; } else { media = ; } diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx index 5c2cef21a..df09da912 100644 --- a/app/assets/javascripts/components/components/video_player.jsx +++ b/app/assets/javascripts/components/components/video_player.jsx @@ -175,7 +175,7 @@ const VideoPlayer = React.createClass({ ); } else { return ( -
+
{spoilerButton} @@ -197,7 +197,7 @@ const VideoPlayer = React.createClass({
{spoilerButton} {muteButton} -
); } 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 caa46ff3c..623872953 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -38,7 +38,7 @@ const DetailedStatus = React.createClass({ let applicationLink = ''; if (status.get('media_attachments').size > 0) { - if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) { media = ; } else { media = ; 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 d8301b20f..e3c4281b9 100644 --- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx +++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx @@ -9,6 +9,7 @@ import ImageLoader from 'react-imageloader'; import LoadingIndicator from '../../../components/loading_indicator'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import ExtendedVideoPlayer from '../../../components/extended_video_player'; const mapStateToProps = state => ({ media: state.getIn(['modal', 'media']), @@ -131,27 +132,34 @@ const Modal = React.createClass({ return null; } - const url = media.get(index).get('url'); + const attachment = media.get(index); + const url = attachment.get('url'); - let leftNav, rightNav; + let leftNav, rightNav, content; - leftNav = rightNav = ''; + leftNav = rightNav = content = ''; if (media.size > 1) { leftNav =
; rightNav =
; } - return ( - - {leftNav} - + if (attachment.get('type') === 'image') { + content = ( + ); + } else if (attachment.get('type') === 'gifv') { + content = ; + } + return ( + + {leftNav} + {content} {rightNav} ); diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss index 3b2e88f6d..b9a9a1da3 100644 --- a/app/assets/stylesheets/stream_entries.scss +++ b/app/assets/stylesheets/stream_entries.scss @@ -104,8 +104,12 @@ overflow: hidden; width: 100%; box-sizing: border-box; - height: 110px; - display: flex; + position: relative; + + .status__attachments__inner { + display: flex; + height: 214px; + } } } @@ -184,8 +188,12 @@ overflow: hidden; width: 100%; box-sizing: border-box; - height: 300px; - display: flex; + position: relative; + + .status__attachments__inner { + display: flex; + height: 360px; + } } .video-player { @@ -231,11 +239,19 @@ text-decoration: none; cursor: zoom-in; } + + video { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + object-fit: cover; + top: 50%; + transform: translateY(-50%); + } } .video-item { - max-width: 196px; - a { cursor: pointer; } @@ -258,6 +274,9 @@ width: 100%; height: 100%; cursor: pointer; + position: absolute; + top: 0; + left: 0; display: flex; align-items: center; justify-content: center; diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 6925f9b0d..620a92dbc 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -1,15 +1,32 @@ # frozen_string_literal: true class MediaAttachment < ApplicationRecord + self.inheritance_column = nil + + enum type: [:image, :gifv, :video] + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze + IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze + VIDEO_STYLES = { + small: { + convert_options: { + output: { + vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', + }, + }, + format: 'png', + time: 0, + }, + }.freeze + belongs_to :account, inverse_of: :media_attachments belongs_to :status, inverse_of: :media_attachments has_attached_file :file, - styles: -> (f) { file_styles f }, - processors: -> (f) { f.video? ? [:transcoder] : [:thumbnail] }, + styles: ->(f) { file_styles f }, + processors: ->(f) { file_processors f }, convert_options: { all: '-quality 90 -strip' } validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES validates_attachment_size :file, less_than: 8.megabytes @@ -27,45 +44,45 @@ class MediaAttachment < ApplicationRecord self.file = URI.parse(url) end - def image? - IMAGE_MIME_TYPES.include? file_content_type - end - - def video? - VIDEO_MIME_TYPES.include? file_content_type - end - - def type - image? ? 'image' : 'video' - end - def to_param shortcode end before_create :set_shortcode + before_post_process :set_type class << self private def file_styles(f) - if f.instance.image? + if f.instance.file_content_type == 'image/gif' { - original: '1280x1280>', - small: '400x400>', - } - else - { - small: { + small: IMAGE_STYLES[:small], + original: { + format: 'webm', convert_options: { output: { - vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', + 'c:v' => 'libvpx', + 'crf' => 6, + 'b:v' => '500K', }, }, - format: 'png', - time: 1, }, } + elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type + IMAGE_STYLES + else + VIDEO_STYLES + end + end + + def file_processors(f) + if f.file_content_type == 'image/gif' + [:gif_transcoder] + elsif VIDEO_MIME_TYPES.include? f.file_content_type + [:transcoder] + else + [:thumbnail] end end end @@ -80,4 +97,8 @@ class MediaAttachment < ApplicationRecord break if MediaAttachment.find_by(shortcode: shortcode).nil? end end + + def set_type + self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image + end end diff --git a/app/views/api/v1/media/create.rabl b/app/views/api/v1/media/create.rabl index 0b42e6e3d..916217cbd 100644 --- a/app/views/api/v1/media/create.rabl +++ b/app/views/api/v1/media/create.rabl @@ -1,5 +1,5 @@ object @media attribute :id, :type -node(:url) { |media| full_asset_url(media.file.url( :original)) } -node(:preview_url) { |media| full_asset_url(media.file.url( :small)) } +node(:url) { |media| full_asset_url(media.file.url(:original)) } +node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } node(:text_url) { |media| medium_url(media) } diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 6c1c1ce84..8c0456b1f 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -22,9 +22,9 @@ .detailed-status__attachments - if status.sensitive? = render partial: 'stream_entries/content_spoiler' - - status.media_attachments.each do |media| - .media-item - = link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}" + .status__attachments__inner + - status.media_attachments.each do |media| + = render partial: 'stream_entries/media', locals: { media: media } %div.detailed-status__meta %data.dt-published{ value: status.created_at.to_time.iso8601 } diff --git a/app/views/stream_entries/_media.html.haml b/app/views/stream_entries/_media.html.haml new file mode 100644 index 000000000..cd7faa700 --- /dev/null +++ b/app/views/stream_entries/_media.html.haml @@ -0,0 +1,4 @@ +.media-item + = link_to media.remote_url.blank? ? media.file.url(:original) : media.remote_url, style: media.image? ? "background-image: url(#{media.file.url(:original)})" : "", target: '_blank', rel: 'noopener', class: "u-#{media.video? || media.gifv? ? 'video' : 'photo'}" do + - unless media.image? + %video{ src: media.file.url(:original), autoplay: true, loop: true }/ diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index 52ad39220..cb2c976ce 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -22,11 +22,12 @@ - if status.sensitive? = render partial: 'stream_entries/content_spoiler' - if status.media_attachments.first.video? - .video-item - = link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do - .video-item__play - = fa_icon('play') + .status__attachments__inner + .video-item + = link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do + .video-item__play + = fa_icon('play') - else - - status.media_attachments.each do |media| - .media-item - = link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}" + .status__attachments__inner + - status.media_attachments.each do |media| + = render partial: 'stream_entries/media', locals: { media: media } diff --git a/config/application.rb b/config/application.rb index 1ea65619c..30ed608c5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,12 +2,13 @@ require_relative 'boot' require 'rails/all' -require_relative '../app/lib/exceptions' - # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) +require_relative '../app/lib/exceptions' +require_relative '../lib/paperclip/gif_transcoder' + Dotenv::Railtie.load module Mastodon diff --git a/db/migrate/20170304202101_add_type_to_media_attachments.rb b/db/migrate/20170304202101_add_type_to_media_attachments.rb new file mode 100644 index 000000000..514079958 --- /dev/null +++ b/db/migrate/20170304202101_add_type_to_media_attachments.rb @@ -0,0 +1,12 @@ +class AddTypeToMediaAttachments < ActiveRecord::Migration[5.0] + def up + add_column :media_attachments, :type, :integer, default: 0, null: false + + MediaAttachment.where(file_content_type: MediaAttachment::IMAGE_MIME_TYPES).update_all(type: MediaAttachment.types[:image]) + MediaAttachment.where(file_content_type: MediaAttachment::VIDEO_MIME_TYPES).update_all(type: MediaAttachment.types[:video]) + end + + def down + remove_column :media_attachments, :type + end +end diff --git a/db/schema.rb b/db/schema.rb index 8cc3bd8e3..4ec85ef2b 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: 20170303212857) do +ActiveRecord::Schema.define(version: 20170304202101) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -98,6 +98,7 @@ ActiveRecord::Schema.define(version: 20170303212857) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "shortcode" + t.integer "type", default: 0, null: false t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true, using: :btree t.index ["status_id"], name: "index_media_attachments_on_status_id", using: :btree end diff --git a/lib/paperclip/gif_transcoder.rb b/lib/paperclip/gif_transcoder.rb new file mode 100644 index 000000000..33d2c4a01 --- /dev/null +++ b/lib/paperclip/gif_transcoder.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Paperclip + # This transcoder is only to be used for the MediaAttachment model + # to convert animated gifs to webm + class GifTranscoder < Paperclip::Processor + def make + num_frames = identify('-format %n :file', file: file.path).to_i + + return file unless options[:style] == :original && num_frames > 1 + + final_file = Paperclip::Transcoder.make(file, options, attachment) + + attachment.instance.file_file_name = 'media.webm' + attachment.instance.file_content_type = 'video/webm' + attachment.instance.type = MediaAttachment.types[:gifv] + + final_file + end + end +end -- cgit From 5f4e402204c9da289d1a6b5e3902bf2c9cfe61c1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Mar 2017 17:27:17 +0100 Subject: Improved /api/v1/accounts/:id/statuses with new params: only_media, exclude_replies Redirect /:username to /users/:username Redirect /:username/:id to /users/:username/updates/:id Updated API documentation and sponsors --- app/controllers/api/v1/accounts_controller.rb | 17 ++------------- app/models/status.rb | 9 +++++--- config/routes.rb | 4 +++- docs/Contributing-to-Mastodon/Sponsors.md | 2 +- docs/Using-the-API/API.md | 30 +++++++++++++++++++++++++-- 5 files changed, 40 insertions(+), 22 deletions(-) (limited to 'app/models') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index d691ac987..c666651b4 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -47,6 +47,8 @@ class Api::V1::AccountsController < ApiController def statuses @statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) + @statuses = @statuses.where(id: MediaAttachment.where(account: @account).where.not(status_id: nil).reorder('').select('distinct status_id')) if params[:only_media] + @statuses = @statuses.without_replies if params[:exclude_replies] @statuses = cache_collection(@statuses, Status) set_maps(@statuses) @@ -58,21 +60,6 @@ class Api::V1::AccountsController < ApiController set_pagination_headers(next_path, prev_path) end - def media_statuses - media_ids = MediaAttachment.where(account: @account).where.not(status_id: nil).reorder('').select('distinct status_id') - @statuses = @account.statuses.where(id: media_ids).permitted_for(@account, current_account).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) - @statuses = cache_collection(@statuses, Status) - - set_maps(@statuses) - set_counters_maps(@statuses) - - next_path = media_statuses_api_v1_account_url(max_id: @statuses.last.id) unless @statuses.empty? - prev_path = media_statuses_api_v1_account_url(since_id: @statuses.first.id) unless @statuses.empty? - - set_pagination_headers(next_path, prev_path) - render action: :statuses - end - def follow FollowService.new.call(current_user.account, @account.acct) set_relationship diff --git a/app/models/status.rb b/app/models/status.rb index e5e740360..663ac1e34 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -37,6 +37,9 @@ class Status < ApplicationRecord scope :remote, -> { where.not(uri: nil) } scope :local, -> { where(uri: nil) } + scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') } + scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') } + cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account def reply? @@ -109,8 +112,8 @@ class Status < ApplicationRecord def as_public_timeline(account = nil, local_only = false) query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') .where(visibility: :public) - .where('(statuses.reply = false OR statuses.in_reply_to_account_id = statuses.account_id)') - .where('statuses.reblog_of_id IS NULL') + .without_replies + .without_reblogs query = query.where('accounts.domain IS NULL') if local_only @@ -121,7 +124,7 @@ class Status < ApplicationRecord query = tag.statuses .joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') .where(visibility: :public) - .where('statuses.reblog_of_id IS NULL') + .without_reblogs query = query.where('accounts.domain IS NULL') if local_only diff --git a/config/routes.rb b/config/routes.rb index f6e2dce5c..a0c76f829 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -153,7 +153,6 @@ Rails.application.routes.draw do member do get :statuses - get 'statuses/media', to: 'accounts#media_statuses', as: :media_statuses get :followers get :following @@ -180,5 +179,8 @@ Rails.application.routes.draw do root 'home#index' + get '/:username', to: redirect('/users/%{username}') + get '/:username/:id', to: redirect('/users/%{username}/updates/%{id}') + match '*unmatched_route', via: :all, to: 'application#raise_not_found' end diff --git a/docs/Contributing-to-Mastodon/Sponsors.md b/docs/Contributing-to-Mastodon/Sponsors.md index 9cdd5b03d..475791684 100644 --- a/docs/Contributing-to-Mastodon/Sponsors.md +++ b/docs/Contributing-to-Mastodon/Sponsors.md @@ -32,7 +32,7 @@ These people make the development of Mastodon possible through [Patreon](https:/ - [C418](https://mastodon.social/users/C418) - [halcy](https://icosahedron.website/users/halcy) - [Extropic](https://gnusocial.no/extropic) -- TBD +- [Pat Monaghan](http://iwrite.software/) - TBD - TBD - TBD diff --git a/docs/Using-the-API/API.md b/docs/Using-the-API/API.md index 07c1b25a9..af7858286 100644 --- a/docs/Using-the-API/API.md +++ b/docs/Using-the-API/API.md @@ -75,6 +75,10 @@ Query parameters: - `max_id` (optional): Skip statuses younger than ID (e.g. navigate backwards in time) - `since_id` (optional): Skip statuses older than ID (e.g. check for updates) +Query parameters for public and tag timelines only: + +- `local` (optional): Only return statuses originating from this instance + ### Notifications **GET /api/v1/notifications** @@ -115,7 +119,14 @@ Returns authenticated user's account. **GET /api/v1/accounts/:id/statuses** -Returns statuses by user. Same options as timeline are permitted. +Returns statuses by user. + +Query parameters: + +- `max_id` (optional): Skip statuses younger than ID (e.g. navigate backwards in time) +- `since_id` (optional): Skip statuses older than ID (e.g. check for updates) +- `only_media` (optional): Only return statuses that have media attachments +- `exclude_replies` (optional): Skip statuses that reply to other statuses **GET /api/v1/accounts/:id/following** @@ -127,7 +138,7 @@ Returns users the given user is followed by. **GET /api/v1/accounts/relationships** -Returns relationships (`following`, `followed_by`, `blocking`) of the current user to a list of given accounts. +Returns relationships (`following`, `followed_by`, `blocking`, `muting`, `requested`) of the current user to a list of given accounts. Query parameters: @@ -146,6 +157,14 @@ Query parameters: Returns accounts blocked by authenticated user. +**GET /api/v1/mutes** + +Returns accounts muted by authenticated user. + +**GET /api/v1/follow_requests** + +Returns accounts that want to follow the authenticated user but are waiting for approval. + **GET /api/v1/favourites** Returns statuses favourited by authenticated user. @@ -207,6 +226,13 @@ Returns the updated relationship to the user. Returns the updated relationship to the user. +# Muting and unmuting users + +**POST /api/v1/accounts/:id/mute** +**POST /api/v1/accounts/:id/unmute** + +Returns the updated relationship to the user. + ### OAuth apps **POST /api/v1/apps** -- cgit From 4fb95c91fbf808bafa581b8976d94ec36eee8619 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Mar 2017 18:08:19 +0100 Subject: Fix wrongful matching of last period in extended usernames Fix anchor tags in some wikipedia URLs being matches as a hashtag --- app/models/account.rb | 2 +- app/models/tag.rb | 2 +- spec/models/account_spec.rb | 9 ++++++++- spec/models/tag_spec.rb | 10 ++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) (limited to 'app/models') diff --git a/app/models/account.rb b/app/models/account.rb index 2fa6bab71..078078945 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -4,7 +4,7 @@ class Account < ApplicationRecord include Targetable include PgSearch - MENTION_RE = /(?:^|[^\/\w])@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/i + MENTION_RE = /(?:^|[^\/\w])@([a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze # Local users diff --git a/app/models/tag.rb b/app/models/tag.rb index 77a73cce8..0d2fe43b8 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -3,7 +3,7 @@ class Tag < ApplicationRecord has_and_belongs_to_many :statuses - HASHTAG_RE = /(?:^|[^\/\w])#([[:word:]_]*[[:alpha:]_][[:word:]_]*)/i + HASHTAG_RE = /(?:^|[^\/\)\w])#([[:word:]_]*[[:alpha:]_][[:word:]_]*)/i validates :name, presence: true, uniqueness: true diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 287f389ac..91c8d75cf 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -178,7 +178,6 @@ RSpec.describe Account, type: :model do end end - describe 'MENTION_RE' do subject { Account::MENTION_RE } @@ -190,6 +189,14 @@ RSpec.describe Account, type: :model do expect(subject.match('@alice Hey how are you?')[1]).to eq 'alice' end + it 'matches full usernames' do + expect(subject.match('@alice@example.com')[1]).to eq 'alice@example.com' + end + + it 'matches full usernames with a dot at the end' do + expect(subject.match('Hello @alice@example.com.')[1]).to eq 'alice@example.com' + end + it 'matches dot-prepended usernames' do expect(subject.match('.@alice I want everybody to see this')[1]).to eq 'alice' end diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 9a7f481e4..360bbc16d 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -1,5 +1,15 @@ require 'rails_helper' RSpec.describe Tag, type: :model do + describe 'HASHTAG_RE' do + subject { Tag::HASHTAG_RE } + it 'does not match URLs with anchors with non-hashtag characters' do + expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil + end + + it 'does not match URLs with hashtag-like anchors' do + expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit')).to be_nil + end + end end -- cgit From 85fce04d1b4353c3645c15967d222cdcdb488dfd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Mar 2017 22:55:24 +0100 Subject: Detect videos with no sound, handle them like gifv --- app/models/media_attachment.rb | 2 +- config/application.rb | 1 + lib/paperclip/video_transcoder.rb | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 lib/paperclip/video_transcoder.rb (limited to 'app/models') diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 620a92dbc..d560bd673 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -80,7 +80,7 @@ class MediaAttachment < ApplicationRecord if f.file_content_type == 'image/gif' [:gif_transcoder] elsif VIDEO_MIME_TYPES.include? f.file_content_type - [:transcoder] + [:video_transcoder] else [:thumbnail] end diff --git a/config/application.rb b/config/application.rb index 30ed608c5..cb009b24c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,6 +8,7 @@ Bundler.require(*Rails.groups) require_relative '../app/lib/exceptions' require_relative '../lib/paperclip/gif_transcoder' +require_relative '../lib/paperclip/video_transcoder' Dotenv::Railtie.load diff --git a/lib/paperclip/video_transcoder.rb b/lib/paperclip/video_transcoder.rb new file mode 100644 index 000000000..c3504c17c --- /dev/null +++ b/lib/paperclip/video_transcoder.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Paperclip + # This transcoder is only to be used for the MediaAttachment model + # to check when uploaded videos are actually gifv's + class VideoTranscoder < Paperclip::Processor + def make + meta = ::Av.cli.identify(@file.path) + attachment.instance.type = MediaAttachment.types[:gifv] unless meta[:audio_encode] + + Paperclip::Transcoder.make(file, options, attachment) + end + end +end -- cgit From 79b08c5f0a05844d2465e1e99c5cde4d7b4f6d0e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Mar 2017 23:13:21 +0100 Subject: Higher quality webms? Might improve conversation of some types of gifs --- app/models/media_attachment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/models') diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index d560bd673..f300c127c 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -63,7 +63,7 @@ class MediaAttachment < ApplicationRecord convert_options: { output: { 'c:v' => 'libvpx', - 'crf' => 6, + 'crf' => 4, 'b:v' => '500K', }, }, -- cgit From 6382ef2bc6f54f01a107fc921a8f29ec2eb9878c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Mar 2017 23:27:25 +0100 Subject: Add vsync -cfr option when converting gif to webm --- app/models/media_attachment.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'app/models') diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index f300c127c..30a76f3ec 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -62,9 +62,10 @@ class MediaAttachment < ApplicationRecord format: 'webm', convert_options: { output: { - 'c:v' => 'libvpx', - 'crf' => 4, - 'b:v' => '500K', + 'c:v' => 'libvpx', + 'crf' => 4, + 'b:v' => '500K', + 'vsync' => 'cfr', }, }, }, -- cgit From 8d93f0ca563322764ee4b5395598af542eb678e7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Mar 2017 23:43:58 +0100 Subject: Increase max bitrate of converted webms, slightly optimized counter queries (Because postgres can tell that count(*) needs no extra checks, but counting a specific column requires them) --- app/controllers/api_controller.rb | 10 +++++----- app/models/media_attachment.rb | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'app/models') diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index db16f82e5..c43011754 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -102,14 +102,14 @@ class ApiController < ApplicationController def set_counters_maps(statuses) # rubocop:disable Style/AccessorMethodName status_ids = statuses.compact.map { |s| s.reblog? ? s.reblog_of_id : s.id }.uniq - @favourites_counts_map = Favourite.select('status_id, COUNT(id) AS favourites_count').group('status_id').where(status_id: status_ids).map { |f| [f.status_id, f.favourites_count] }.to_h - @reblogs_counts_map = Status.select('statuses.id, COUNT(reblogs.id) AS reblogs_count').joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.id = reblogs.reblog_of_id').where(id: status_ids).group('statuses.id').map { |r| [r.id, r.reblogs_count] }.to_h + @favourites_counts_map = Favourite.select('status_id, COUNT(*) AS favourites_count').group('status_id').where(status_id: status_ids).map { |f| [f.status_id, f.favourites_count] }.to_h + @reblogs_counts_map = Status.select('statuses.id, COUNT(*) AS reblogs_count').joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.id = reblogs.reblog_of_id').where(id: status_ids).group('statuses.id').map { |r| [r.id, r.reblogs_count] }.to_h end def set_account_counters_maps(accounts) # rubocop:disable Style/AccessorMethodName account_ids = accounts.compact.map(&:id).uniq - @followers_counts_map = Follow.unscoped.select('target_account_id, COUNT(account_id) AS followers_count').group('target_account_id').where(target_account_id: account_ids).map { |f| [f.target_account_id, f.followers_count] }.to_h - @following_counts_map = Follow.unscoped.select('account_id, COUNT(target_account_id) AS following_count').group('account_id').where(account_id: account_ids).map { |f| [f.account_id, f.following_count] }.to_h - @statuses_counts_map = Status.unscoped.select('account_id, COUNT(id) AS statuses_count').group('account_id').where(account_id: account_ids).map { |s| [s.account_id, s.statuses_count] }.to_h + @followers_counts_map = Follow.unscoped.select('target_account_id, COUNT(*) AS followers_count').group('target_account_id').where(target_account_id: account_ids).map { |f| [f.target_account_id, f.followers_count] }.to_h + @following_counts_map = Follow.unscoped.select('account_id, COUNT(*) AS following_count').group('account_id').where(account_id: account_ids).map { |f| [f.account_id, f.following_count] }.to_h + @statuses_counts_map = Status.unscoped.select('account_id, COUNT(*) AS statuses_count').group('account_id').where(account_id: account_ids).map { |s| [s.account_id, s.statuses_count] }.to_h end end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 30a76f3ec..5241d9515 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -64,7 +64,7 @@ class MediaAttachment < ApplicationRecord output: { 'c:v' => 'libvpx', 'crf' => 4, - 'b:v' => '500K', + 'b:v' => '1300K', 'vsync' => 'cfr', }, }, -- cgit From 03a857f59a773483924a942ac3bda1b290f82a2e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 6 Mar 2017 00:30:03 +0100 Subject: Use more widely supported MP4 format for gifv's --- app/models/media_attachment.rb | 12 +++++++----- lib/paperclip/gif_transcoder.rb | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'app/models') diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 5241d9515..1090f1b39 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -59,13 +59,15 @@ class MediaAttachment < ApplicationRecord { small: IMAGE_STYLES[:small], original: { - format: 'webm', + format: 'mp4', convert_options: { output: { - 'c:v' => 'libvpx', - 'crf' => 4, - 'b:v' => '1300K', - 'vsync' => 'cfr', + 'movflags' => 'faststart', + 'pix_fmt' => 'yuv420p', + 'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'', + 'vsync' => 'cfr', + 'b:v' => '1300K', + 'crf' => 4, }, }, }, diff --git a/lib/paperclip/gif_transcoder.rb b/lib/paperclip/gif_transcoder.rb index 33d2c4a01..8337448b2 100644 --- a/lib/paperclip/gif_transcoder.rb +++ b/lib/paperclip/gif_transcoder.rb @@ -11,8 +11,8 @@ module Paperclip final_file = Paperclip::Transcoder.make(file, options, attachment) - attachment.instance.file_file_name = 'media.webm' - attachment.instance.file_content_type = 'video/webm' + attachment.instance.file_file_name = 'media.mp4' + attachment.instance.file_content_type = 'video/mp4' attachment.instance.type = MediaAttachment.types[:gifv] final_file -- cgit From d6cb4bbe99f16b40c5962130d19ca3336124af1b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 6 Mar 2017 01:50:35 +0100 Subject: Performance improvement for profiles --- app/controllers/api/v1/accounts_controller.rb | 1 + app/models/media_attachment.rb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'app/models') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index c666651b4..9c84e0a1b 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -53,6 +53,7 @@ class Api::V1::AccountsController < ApiController set_maps(@statuses) set_counters_maps(@statuses) + set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq) next_path = statuses_api_v1_account_url(max_id: @statuses.last.id) unless @statuses.empty? prev_path = statuses_api_v1_account_url(since_id: @statuses.first.id) unless @statuses.empty? diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 1090f1b39..818190214 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -67,7 +67,8 @@ class MediaAttachment < ApplicationRecord 'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'', 'vsync' => 'cfr', 'b:v' => '1300K', - 'crf' => 4, + 'maxrate' => '500K', + 'crf' => 6, }, }, }, -- cgit