about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml1
-rw-r--r--app/assets/javascripts/components/features/status/components/detailed_status.jsx10
-rw-r--r--app/assets/stylesheets/components.scss35
-rw-r--r--app/lib/application_extension.rb9
-rw-r--r--app/lib/url_validator.rb14
-rw-r--r--app/models/concerns/application.rb8
-rw-r--r--app/models/status.rb2
-rw-r--r--app/services/post_status_service.rb9
-rw-r--r--app/views/api/v1/apps/show.rabl2
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml10
-rw-r--r--config/application.rb1
-rw-r--r--config/locales/en.yml6
-rw-r--r--spec/controllers/api/v1/statuses_controller_spec.rb3
-rw-r--r--spec/fabricators/application_fabricator.rb5
14 files changed, 78 insertions, 37 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 28c735913..ab28c0fe1 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -87,3 +87,4 @@ AllCops:
   - 'bin/*'
   - 'Rakefile'
   - 'node_modules/**/*'
+  - 'Vagrantfile'
diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
index 7cbca4633..14a504c7c 100644
--- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx
+++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
@@ -32,7 +32,9 @@ const DetailedStatus = React.createClass({
 
   render () {
     const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
-    let media    = '';
+
+    let media           = '';
+    let applicationLink = '';
 
     if (status.get('media_attachments').size > 0) {
       if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
@@ -42,6 +44,10 @@ const DetailedStatus = React.createClass({
       }
     }
 
+    if (status.get('application')) {
+      applicationLink = <span> · <a className='detailed-status__application' style={{ color: 'inherit' }} href={status.getIn(['application', 'website'])} target='_blank' rel='nooopener'>{status.getIn(['application', 'name'])}</a></span>;
+    }
+
     return (
       <div style={{ background: '#2f3441', padding: '14px 10px' }} className='detailed-status'>
         <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
@@ -54,7 +60,7 @@ const DetailedStatus = React.createClass({
         {media}
 
         <div style={{ marginTop: '15px', color: '#616b86', fontSize: '14px', lineHeight: '18px' }}>
-          <a className='detailed-status__datetime' style={{ color: 'inherit' }} href={status.get('url')} target='_blank' rel='noopener'><FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /></a> · <a className='detailed-status__application' style={{ color: 'inherit' }} href={status.getIn(['application', 'website'])} target='_blank' rel='nooopener'>{status.getIn(['application', 'name'])}</a> · <Link to={`/statuses/${status.get('id')}/reblogs`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-retweet' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('reblogs_count')} /></span></Link> · <Link to={`/statuses/${status.get('id')}/favourites`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-star' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('favourites_count')} /></span></Link>
+          <a className='detailed-status__datetime' style={{ color: 'inherit' }} href={status.get('url')} target='_blank' rel='noopener'><FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /></a>{applicationLink} · <Link to={`/statuses/${status.get('id')}/reblogs`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-retweet' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('reblogs_count')} /></span></Link> · <Link to={`/statuses/${status.get('id')}/favourites`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-star' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('favourites_count')} /></span></Link>
         </div>
       </div>
     );
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index f4d822dcf..2d99fcfe8 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -663,20 +663,21 @@
   }
 }
 
-button i.fa-retweet {
-  height: 19px;
-  width: 24px;
-  background: image-url('boost_sprite.png') no-repeat;
-  background-position: 0 0;
-  transition: background-position 0.9s steps(11);
-  transition-duration: 0s;
-
-  &::before {
-    display: none !important;
-  }
-}
-
-button.active i.fa-retweet {
-  transition-duration: 0.9s;
-  background-position: 0 -209px;
-}
+// Commented out until sprite matches non-sprite icon visually
+// button i.fa-retweet {
+//   height: 19px;
+//   width: 24px;
+//   background: image-url('boost_sprite.png') no-repeat;
+//   background-position: 0 0;
+//   transition: background-position 0.9s steps(11);
+//   transition-duration: 0s;
+
+//   &::before {
+//     display: none !important;
+//   }
+// }
+
+// button.active i.fa-retweet {
+//   transition-duration: 0.9s;
+//   background-position: 0 -209px;
+// }
diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb
new file mode 100644
index 000000000..93c0f42f0
--- /dev/null
+++ b/app/lib/application_extension.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ApplicationExtension
+  extend ActiveSupport::Concern
+
+  included do
+    validates :website, url: true, unless: 'website.blank?'
+  end
+end
diff --git a/app/lib/url_validator.rb b/app/lib/url_validator.rb
new file mode 100644
index 000000000..4a5c4ef3f
--- /dev/null
+++ b/app/lib/url_validator.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class UrlValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    record.errors.add(attribute, I18n.t('applications.invalid_url')) unless compliant?(value)
+  end
+
+  private
+
+  def compliant?(url)
+    parsed_url = Addressable::URI.parse(url)
+    !parsed_url.nil? && %w(http https).include?(parsed_url.scheme) && parsed_url.host
+  end
+end
diff --git a/app/models/concerns/application.rb b/app/models/concerns/application.rb
deleted file mode 100644
index 613be34ee..000000000
--- a/app/models/concerns/application.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module ApplicationExtension
-  extend ActiveSupport::Concern
-  included do
-    validates :website
-  end
-end
-
-Doorkeeper::Application.send :include, ApplicationExtension
\ No newline at end of file
diff --git a/app/models/status.rb b/app/models/status.rb
index 8301ae16e..5710f9cca 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -35,7 +35,7 @@ class Status < ApplicationRecord
   scope :remote, -> { where.not(uri: nil) }
   scope :local, -> { where(uri: nil) }
 
-  cache_associated :account, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account
+  cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account
 
   def local?
     uri.nil?
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 86a84f512..af31c923f 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -7,10 +7,17 @@ class PostStatusService < BaseService
   # @param [Status] in_reply_to Optional status to reply to
   # @param [Hash] options
   # @option [Boolean] :sensitive
+  # @option [String] :visibility
   # @option [Enumerable] :media_ids Optional array of media IDs to attach
+  # @option [Doorkeeper::Application] :application
   # @return [Status]
   def call(account, text, in_reply_to = nil, options = {})
-    status = account.statuses.create!(text: text, thread: in_reply_to, sensitive: options[:sensitive], visibility: options[:visibility], application: options[:application])
+    status = account.statuses.create!(text:        text,
+                                      thread:      in_reply_to,
+                                      sensitive:   options[:sensitive],
+                                      visibility:  options[:visibility],
+                                      application: options[:application])
+
     attach_media(status, options[:media_ids])
     process_mentions_service.call(status)
     process_hashtags_service.call(status)
diff --git a/app/views/api/v1/apps/show.rabl b/app/views/api/v1/apps/show.rabl
index 30cfd81ab..6d9e607db 100644
--- a/app/views/api/v1/apps/show.rabl
+++ b/app/views/api/v1/apps/show.rabl
@@ -1,3 +1,3 @@
 object @application
 
-attributes :id, :name, :website
\ No newline at end of file
+attributes :name, :website
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index 946adbd8e..bc09d3597 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -29,13 +29,15 @@
       %span= l(status.created_at)
     ·
     - if status.application
-      = link_to status.application.website, class: 'detailed-status__application', target: @external_links ? '_blank' : nil, rel: 'noopener' do
-        %span= status.application.name
+      - if status.application.website.blank?
+        %strong.detailed-status__application= status.application.name
+      - else
+        = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener'
       ·
-    %span
+    %span<
       = fa_icon('retweet')
       %span= status.reblogs.count
     ·
-    %span
+    %span<
       = fa_icon('star')
       %span= status.favourites.count
diff --git a/config/application.rb b/config/application.rb
index 79ace8521..e561d0473 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -46,6 +46,7 @@ module Mastodon
 
     config.to_prepare do
       Doorkeeper::AuthorizationsController.layout 'public'
+      Doorkeeper::Application.send :include, ApplicationExtension
     end
 
     config.action_dispatch.default_headers = {
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 128a4d40e..f7d7ed729 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -8,6 +8,7 @@ en:
     domain_count_after: other instances
     domain_count_before: Connected to
     get_started: Get started
+    learn_more: Learn more
     links: Links
     source_code: Source code
     status_count_after: statuses
@@ -15,7 +16,6 @@ en:
     terms: Terms
     user_count_after: users
     user_count_before: Home to
-    learn_more: Learn more
   accounts:
     follow: Follow
     followers: Followers
@@ -28,6 +28,8 @@ en:
     unfollow: Unfollow
   application_mailer:
     signature: Mastodon notifications from %{instance}
+  applications:
+    invalid_url: The provided URL is invalid
   auth:
     change_password: Change password
     didnt_get_confirmation: Didn't receive confirmation instructions?
@@ -88,9 +90,9 @@ en:
     proceed: Proceed to follow
     prompt: 'You are going to follow:'
   settings:
+    back: Back to Mastodon
     edit_profile: Edit profile
     preferences: Preferences
-    back: Back to Mastodon
   stream_entries:
     click_to_show: Click to show
     favourited: favourited a post by
diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb
index d9c73f952..669956659 100644
--- a/spec/controllers/api/v1/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses_controller_spec.rb
@@ -4,7 +4,8 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { double acceptable?: true, resource_owner_id: user.id }
+  let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
+  let(:token) { double acceptable?: true, resource_owner_id: user.id, application: app }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
diff --git a/spec/fabricators/application_fabricator.rb b/spec/fabricators/application_fabricator.rb
new file mode 100644
index 000000000..42b7009dc
--- /dev/null
+++ b/spec/fabricators/application_fabricator.rb
@@ -0,0 +1,5 @@
+Fabricator(:application, from: Doorkeeper::Application) do
+  name         'Example'
+  website      'http://example.com'
+  redirect_uri 'http://example.com/callback'
+end