Fixing image upload limits, allowing webm, merge/unmerge events trigger
timeline reload in UI, other small fixes
This commit is contained in:
		| @@ -2,7 +2,7 @@ FROM ruby:2.2.4 | ||||
|  | ||||
| ENV RAILS_ENV=production | ||||
|  | ||||
| RUN apt-get update -qq && apt-get install -y build-essential libpq-dev libxml2-dev libxslt1-dev nodejs nodejs-legacy npm && rm -rf /var/lib/apt/lists/* | ||||
| RUN apt-get update -qq && apt-get install -y build-essential libpq-dev libxml2-dev libxslt1-dev nodejs nodejs-legacy npm ffmpeg && rm -rf /var/lib/apt/lists/* | ||||
| RUN mkdir /mastodon | ||||
|  | ||||
| WORKDIR /mastodon | ||||
|   | ||||
							
								
								
									
										4
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Gemfile
									
									
									
									
									
								
							| @@ -16,6 +16,7 @@ gem 'dotenv-rails' | ||||
| gem 'font-awesome-rails' | ||||
|  | ||||
| gem 'paperclip', '~> 4.3' | ||||
| gem 'paperclip-av-transcoder' | ||||
|  | ||||
| gem 'http' | ||||
| gem 'addressable' | ||||
| @@ -31,12 +32,11 @@ gem 'hiredis' | ||||
| gem 'redis', '~>3.2' | ||||
| gem 'fast_blank' | ||||
| gem 'htmlentities' | ||||
| gem 'onebox' | ||||
| gem 'simple_form' | ||||
| gem 'will_paginate' | ||||
| gem 'rack-attack' | ||||
| gem 'sidekiq' | ||||
| gem 'sinatra', require: nil, github: 'sinatra' | ||||
| gem 'sinatra', require: nil, git: 'https://github.com/sinatra/sinatra.git' | ||||
|  | ||||
| gem 'react-rails' | ||||
| gem 'browserify-rails' | ||||
|   | ||||
							
								
								
									
										33
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @@ -1,13 +1,13 @@ | ||||
| GIT | ||||
|   remote: git://github.com/sinatra/sinatra.git | ||||
|   revision: 6b5a0ef3a4598366138fefe3f2b696ddeb371f3c | ||||
|   remote: https://github.com/sinatra/sinatra.git | ||||
|   revision: 1b0edc0aeaaf4839cadfcec1b21da86e6af1d4c0 | ||||
|   specs: | ||||
|     rack-protection (2.0.0) | ||||
|     rack-protection (2.0.0.beta2) | ||||
|       rack | ||||
|     sinatra (2.0.0.pre.alpha) | ||||
|       mustermann (~> 0.4) | ||||
|     sinatra (2.0.0.beta2) | ||||
|       mustermann (= 1.0.0.beta2) | ||||
|       rack (~> 2.0) | ||||
|       rack-protection (~> 2.0) | ||||
|       rack-protection (= 2.0.0.beta2) | ||||
|       tilt (~> 2.0) | ||||
|  | ||||
| GEM | ||||
| @@ -54,6 +54,8 @@ GEM | ||||
|     addressable (2.4.0) | ||||
|     arel (7.1.1) | ||||
|     ast (2.3.0) | ||||
|     av (0.9.0) | ||||
|       cocaine (~> 0.5.3) | ||||
|     babel-source (5.8.35) | ||||
|     babel-transpiler (0.7.0) | ||||
|       babel-source (>= 4.0, < 6) | ||||
| @@ -174,22 +176,13 @@ GEM | ||||
|     mimemagic (0.3.0) | ||||
|     mini_portile2 (2.1.0) | ||||
|     minitest (5.9.0) | ||||
|     moneta (0.8.0) | ||||
|     multi_json (1.12.1) | ||||
|     mustache (1.0.3) | ||||
|     mustermann (0.4.0) | ||||
|       tool (~> 0.2) | ||||
|     mustermann (1.0.0.beta2) | ||||
|     nio4r (1.2.1) | ||||
|     nokogiri (1.6.8) | ||||
|       mini_portile2 (~> 2.1.0) | ||||
|       pkg-config (~> 1.1.7) | ||||
|     oj (2.17.3) | ||||
|     onebox (1.5.48) | ||||
|       htmlentities (~> 4.3.4) | ||||
|       moneta (~> 0.8) | ||||
|       multi_json (~> 1.11) | ||||
|       mustache | ||||
|       nokogiri (~> 1.6.6) | ||||
|     orm_adapter (0.5.0) | ||||
|     ostatus2 (0.1.1) | ||||
|       addressable (~> 2.4) | ||||
| @@ -201,6 +194,9 @@ GEM | ||||
|       cocaine (~> 0.5.5) | ||||
|       mime-types | ||||
|       mimemagic (= 0.3.0) | ||||
|     paperclip-av-transcoder (0.6.4) | ||||
|       av (~> 0.9.0) | ||||
|       paperclip (>= 2.5.2) | ||||
|     parser (2.3.1.2) | ||||
|       ast (~> 2.2) | ||||
|     pg (0.18.4) | ||||
| @@ -336,7 +332,6 @@ GEM | ||||
|     thor (0.19.1) | ||||
|     thread_safe (0.3.5) | ||||
|     tilt (2.0.5) | ||||
|     tool (0.2.3) | ||||
|     tzinfo (1.2.2) | ||||
|       thread_safe (~> 0.1) | ||||
|     uglifier (3.0.1) | ||||
| @@ -386,9 +381,9 @@ DEPENDENCIES | ||||
|   lograge | ||||
|   nokogiri | ||||
|   oj | ||||
|   onebox | ||||
|   ostatus2 | ||||
|   paperclip (~> 4.3) | ||||
|   paperclip-av-transcoder | ||||
|   pg | ||||
|   pry-rails | ||||
|   puma | ||||
| @@ -414,4 +409,4 @@ DEPENDENCIES | ||||
|   will_paginate | ||||
|  | ||||
| BUNDLED WITH | ||||
|    1.12.5 | ||||
|    1.13.0 | ||||
|   | ||||
| @@ -1,6 +1,12 @@ | ||||
| export const TIMELINE_SET    = 'TIMELINE_SET'; | ||||
| export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; | ||||
| export const TIMELINE_DELETE = 'TIMELINE_DELETE'; | ||||
| import api from '../api' | ||||
|  | ||||
| export const TIMELINE_SET     = 'TIMELINE_SET'; | ||||
| export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE'; | ||||
| export const TIMELINE_DELETE  = 'TIMELINE_DELETE'; | ||||
|  | ||||
| export const TIMELINE_REFRESH_REQUEST = 'TIMELINE_REFRESH_REQUEST'; | ||||
| export const TIMELINE_REFRESH_SUCCESS = 'TIMELINE_REFRESH_SUCCESS'; | ||||
| export const TIMELINE_REFRESH_FAIL    = 'TIMELINE_REFRESH_FAIL'; | ||||
|  | ||||
| export function setTimeline(timeline, statuses) { | ||||
|   return { | ||||
| @@ -24,3 +30,36 @@ export function deleteFromTimelines(id) { | ||||
|     id: id | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function refreshTimelineRequest(timeline) { | ||||
|   return { | ||||
|     type: TIMELINE_REFRESH_REQUEST, | ||||
|     timeline: timeline | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function refreshTimeline(timeline) { | ||||
|   return function (dispatch, getState) { | ||||
|     dispatch(refreshTimelineRequest(timeline)); | ||||
|  | ||||
|     api(getState).get(`/api/statuses/${timeline}`).then(function (response) { | ||||
|       dispatch(refreshTimelineSuccess(timeline, response.data)); | ||||
|     }).catch(function (error) { | ||||
|       dispatch(refreshTimelineFail(timeline, error)); | ||||
|     }); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function refreshTimelineSuccess(timeline, statuses) { | ||||
|   return function (dispatch) { | ||||
|     dispatch(setTimeline(timeline, statuses)); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function refreshTimelineFail(timeline, error) { | ||||
|   return { | ||||
|     type: TIMELINE_REFRESH_FAIL, | ||||
|     timeline: timeline, | ||||
|     error: error | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ const Avatar = React.createClass({ | ||||
|  | ||||
|   render () { | ||||
|     return ( | ||||
|       <div style={{ width: `${this.props.size}px`, height: `${this.props.size}px` }}> | ||||
|       <div style={{ width: `${this.props.size}px`, height: `${this.props.size}px`, borderRadius: '4px', overflow: 'hidden' }} className='transparent-background'> | ||||
|         <img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ display: 'block', borderRadius: '4px' }} /> | ||||
|       </div> | ||||
|     ); | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { Provider }                                         from 'react-redux'; | ||||
| import configureStore                                       from '../store/configureStore'; | ||||
| import Frontend                                             from '../components/frontend'; | ||||
| import { setTimeline, updateTimeline, deleteFromTimelines } from '../actions/timelines'; | ||||
| import { setAccessToken }                                   from '../actions/meta'; | ||||
| import PureRenderMixin                                      from 'react-addons-pure-render-mixin'; | ||||
| import { Router, Route, createMemoryHistory }               from 'react-router'; | ||||
| import AccountRoute                                         from '../routes/account_route'; | ||||
| import StatusRoute                                          from '../routes/status_route'; | ||||
| import { Provider }                                                          from 'react-redux'; | ||||
| import configureStore                                                        from '../store/configureStore'; | ||||
| import Frontend                                                              from '../components/frontend'; | ||||
| import { setTimeline, updateTimeline, deleteFromTimelines, refreshTimeline } from '../actions/timelines'; | ||||
| import { setAccessToken }                                                    from '../actions/meta'; | ||||
| import PureRenderMixin                                                       from 'react-addons-pure-render-mixin'; | ||||
| import { Router, Route, createMemoryHistory }                                from 'react-router'; | ||||
| import AccountRoute                                                          from '../routes/account_route'; | ||||
| import StatusRoute                                                           from '../routes/status_route'; | ||||
|  | ||||
| const store   = configureStore(); | ||||
| const history = createMemoryHistory(); | ||||
| @@ -36,10 +36,14 @@ const Root = React.createClass({ | ||||
|         disconnected: function() {}, | ||||
|  | ||||
|         received: function(data) { | ||||
|           if (data.type === 'update') { | ||||
|             return store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message))); | ||||
|           } else if (data.type === 'delete') { | ||||
|             return store.dispatch(deleteFromTimelines(data.id)); | ||||
|           switch(data.type) { | ||||
|             case 'update': | ||||
|               return store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message))); | ||||
|             case 'delete': | ||||
|               return store.dispatch(deleteFromTimelines(data.id)); | ||||
|             case 'merge': | ||||
|             case 'unmerge': | ||||
|               return store.dispatch(refreshTimeline('home')); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|   | ||||
| @@ -10,7 +10,9 @@ module ApplicationCable | ||||
|     protected | ||||
|  | ||||
|     def find_verified_user | ||||
|       if verified_user = env['warden'].user | ||||
|       verified_user = env['warden'].user | ||||
|  | ||||
|       if verified_user | ||||
|         verified_user | ||||
|       else | ||||
|         reject_unauthorized_connection | ||||
|   | ||||
| @@ -17,7 +17,11 @@ class FeedManager | ||||
|   def push(timeline_type, account, status) | ||||
|     redis.zadd(key(timeline_type, account.id), status.id, status.id) | ||||
|     trim(timeline_type, account.id) | ||||
|     ActionCable.server.broadcast("timeline:#{account.id}", type: 'update', timeline: timeline_type, message: inline_render(account, status)) | ||||
|     broadcast(account.id, type: 'update', timeline: timeline_type, message: inline_render(account, status)) | ||||
|   end | ||||
|  | ||||
|   def broadcast(account_id, options = {}) | ||||
|     ActionCable.server.broadcast("timeline:#{account_id}", options) | ||||
|   end | ||||
|  | ||||
|   def trim(type, account_id) | ||||
|   | ||||
| @@ -35,7 +35,7 @@ class Formatter | ||||
|   def link_mentions(html, mentions) | ||||
|     html.gsub(Account::MENTION_RE) do |match| | ||||
|       acct    = Account::MENTION_RE.match(match)[1] | ||||
|       mention = mentions.find { |mention| mention.account.acct.eql?(acct) } | ||||
|       mention = mentions.find { |item| item.account.acct.eql?(acct) } | ||||
|  | ||||
|       mention.nil? ? match : mention_html(match, mention.account) | ||||
|     end | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| class Account < ApplicationRecord | ||||
|   include Targetable | ||||
|  | ||||
|   MENTION_RE = /(?:^|\s|\.|>)@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/i | ||||
|   IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'] | ||||
|  | ||||
|   # Local users | ||||
|   has_one :user, inverse_of: :account | ||||
|   validates :username, presence: true, format: { with: /\A[a-z0-9_]+\z/i, message: 'only letters, numbers and underscores' }, uniqueness: { scope: :domain, case_sensitive: false }, if: 'local?' | ||||
| @@ -8,12 +11,12 @@ class Account < ApplicationRecord | ||||
|  | ||||
|   # Avatar upload | ||||
|   has_attached_file :avatar, styles: { large: '300x300#', medium: '96x96#', small: '48x48#' } | ||||
|   validates_attachment_content_type :avatar, content_type: /\Aimage\/.*\Z/ | ||||
|   validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES | ||||
|   validates_attachment_size :avatar, less_than: 2.megabytes | ||||
|  | ||||
|   # Header upload | ||||
|   has_attached_file :header, styles: { medium: '700x335#' } | ||||
|   validates_attachment_content_type :header, content_type: /\Aimage\/.*\Z/ | ||||
|   validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES | ||||
|   validates_attachment_size :header, less_than: 2.megabytes | ||||
|  | ||||
|   # Local user profile validations | ||||
| @@ -35,8 +38,6 @@ class Account < ApplicationRecord | ||||
|  | ||||
|   has_many :media_attachments, dependent: :destroy | ||||
|  | ||||
|   MENTION_RE = /(?:^|\s|\.|>)@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/i | ||||
|  | ||||
|   def follow!(other_account) | ||||
|     self.active_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) | ||||
|   end | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| class MediaAttachment < ApplicationRecord | ||||
|   IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'] | ||||
|   VIDEO_MIME_TYPES = ['video/webm'] | ||||
|  | ||||
|   belongs_to :account, inverse_of: :media_attachments | ||||
|   belongs_to :status,  inverse_of: :media_attachments | ||||
|  | ||||
|   has_attached_file :file, styles: { small: '510x680>' } | ||||
|   validates_attachment_content_type :file, content_type: /\Aimage\/.*\z/ | ||||
|   has_attached_file :file, styles: lambda { |f| f.instance.image? ? { small: '510x680>' } : { small: { format: 'webm' } } }, processors: lambda { |f| f.video? ? [:transcoder] : [:thumbnail] } | ||||
|   validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES | ||||
|   validates_attachment_size :file, less_than: 4.megabytes | ||||
|  | ||||
|   validates :account, presence: true | ||||
| @@ -15,4 +18,12 @@ class MediaAttachment < ApplicationRecord | ||||
|   def file_remote_url=(url) | ||||
|     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 | ||||
| end | ||||
|   | ||||
| @@ -30,6 +30,7 @@ class FollowService < BaseService | ||||
|     end | ||||
|  | ||||
|     FeedManager.instance.trim(:home, into_account.id) | ||||
|     FeedManager.instance.broadcast(into_account.id, type: 'merge') | ||||
|   end | ||||
|  | ||||
|   def redis | ||||
|   | ||||
| @@ -16,6 +16,8 @@ class UnfollowService < BaseService | ||||
|     from_account.statuses.find_each do |status| | ||||
|       redis.zrem(timeline_key, status.id) | ||||
|     end | ||||
|  | ||||
|     FeedManager.instance.broadcast(into_account.id, type: 'unmerge') | ||||
|   end | ||||
|  | ||||
|   def redis | ||||
|   | ||||
| @@ -11,24 +11,48 @@ RSpec.describe Api::MediaController, type: :controller do | ||||
|   end | ||||
|  | ||||
|   describe 'POST #create' do | ||||
|     before do | ||||
|       post :create, params: { file: fixture_file_upload('files/attachment.jpg', 'image/jpeg') } | ||||
|     context 'image/jpeg' do | ||||
|       before do | ||||
|         post :create, params: { file: fixture_file_upload('files/attachment.jpg', 'image/jpeg') } | ||||
|       end | ||||
|  | ||||
|       it 'returns http success' do | ||||
|         expect(response).to have_http_status(:success) | ||||
|       end | ||||
|  | ||||
|       it 'creates a media attachment' do | ||||
|         expect(MediaAttachment.first).to_not be_nil | ||||
|       end | ||||
|  | ||||
|       it 'uploads a file' do | ||||
|         expect(MediaAttachment.first).to have_attached_file(:file) | ||||
|       end | ||||
|  | ||||
|       it 'returns media ID in JSON' do | ||||
|         expect(body_as_json[:id]).to eq MediaAttachment.first.id | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     it 'returns http success' do | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
|     context 'video/webm' do | ||||
|       before do | ||||
|         post :create, params: { file: fixture_file_upload('files/attachment.webm', 'video/webm') } | ||||
|       end | ||||
|  | ||||
|     it 'creates a media attachment' do | ||||
|       expect(MediaAttachment.first).to_not be_nil | ||||
|     end | ||||
|       xit 'returns http success' do | ||||
|         expect(response).to have_http_status(:success) | ||||
|       end | ||||
|  | ||||
|     it 'uploads a file' do | ||||
|       expect(MediaAttachment.first).to have_attached_file(:file) | ||||
|     end | ||||
|       xit 'creates a media attachment' do | ||||
|         expect(MediaAttachment.first).to_not be_nil | ||||
|       end | ||||
|  | ||||
|     it 'returns media ID in JSON' do | ||||
|       expect(body_as_json[:id]).to eq MediaAttachment.first.id | ||||
|       xit 'uploads a file' do | ||||
|         expect(MediaAttachment.first).to have_attached_file(:file) | ||||
|       end | ||||
|  | ||||
|       xit 'returns media ID in JSON' do | ||||
|         expect(body_as_json[:id]).to eq MediaAttachment.first.id | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								spec/fixtures/files/attachment.webm
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								spec/fixtures/files/attachment.webm
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user