Merge pull request #490 from glitch-soc/merge-upstream
Merge with tootsuite @ 1de7400dfc
			
			
This commit is contained in:
		
						commit
						6a831cd957
					
				
					 32 changed files with 148 additions and 51 deletions
				
			
		|  | @ -37,7 +37,7 @@ addons: | ||||||
| 
 | 
 | ||||||
| rvm: | rvm: | ||||||
|   - 2.4.3 |   - 2.4.3 | ||||||
|   - 2.5.0 |   - 2.5.1 | ||||||
| 
 | 
 | ||||||
| services: | services: | ||||||
|   - redis-server |   - redis-server | ||||||
|  | @ -47,6 +47,10 @@ install: | ||||||
|   - bundle install --path=vendor/bundle --with pam_authentication --without development production --retry=3 --jobs=16 |   - bundle install --path=vendor/bundle --with pam_authentication --without development production --retry=3 --jobs=16 | ||||||
|   - yarn install |   - yarn install | ||||||
| 
 | 
 | ||||||
|  | # https://github.com/travis-ci/travis-ci/issues/9333 | ||||||
|  | before_install: | ||||||
|  |   - gem install bundler | ||||||
|  | 
 | ||||||
| before_script: | before_script: | ||||||
|   - travis_wait ./bin/rails parallel:create parallel:load_schema parallel:prepare assets:precompile |   - travis_wait ./bin/rails parallel:create parallel:load_schema parallel:prepare assets:precompile | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,6 +19,8 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def load_accounts |   def load_accounts | ||||||
|  |     return [] if @account.user_hides_network? && current_account.id != @account.id | ||||||
|  | 
 | ||||||
|     default_accounts.merge(paginated_follows).to_a |     default_accounts.merge(paginated_follows).to_a | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,6 +19,8 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def load_accounts |   def load_accounts | ||||||
|  |     return [] if @account.user_hides_network? && current_account.id != @account.id | ||||||
|  | 
 | ||||||
|     default_accounts.merge(paginated_follows).to_a |     default_accounts.merge(paginated_follows).to_a | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,11 +8,15 @@ class FollowerAccountsController < ApplicationController | ||||||
|       format.html do |       format.html do | ||||||
|         use_pack 'public' |         use_pack 'public' | ||||||
| 
 | 
 | ||||||
|  |         next if @account.user_hides_network? | ||||||
|  | 
 | ||||||
|         follows |         follows | ||||||
|         @relationships = AccountRelationshipsPresenter.new(follows.map(&:account_id), current_user.account_id) if user_signed_in? |         @relationships = AccountRelationshipsPresenter.new(follows.map(&:account_id), current_user.account_id) if user_signed_in? | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       format.json do |       format.json do | ||||||
|  |         raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? | ||||||
|  | 
 | ||||||
|         render json: collection_presenter, |         render json: collection_presenter, | ||||||
|                serializer: ActivityPub::CollectionSerializer, |                serializer: ActivityPub::CollectionSerializer, | ||||||
|                adapter: ActivityPub::Adapter, |                adapter: ActivityPub::Adapter, | ||||||
|  |  | ||||||
|  | @ -8,11 +8,15 @@ class FollowingAccountsController < ApplicationController | ||||||
|       format.html do |       format.html do | ||||||
|         use_pack 'public' |         use_pack 'public' | ||||||
| 
 | 
 | ||||||
|  |         next if @account.user_hides_network? | ||||||
|  | 
 | ||||||
|         follows |         follows | ||||||
|         @relationships = AccountRelationshipsPresenter.new(follows.map(&:target_account_id), current_user.account_id) if user_signed_in? |         @relationships = AccountRelationshipsPresenter.new(follows.map(&:target_account_id), current_user.account_id) if user_signed_in? | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       format.json do |       format.json do | ||||||
|  |         raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? | ||||||
|  | 
 | ||||||
|         render json: collection_presenter, |         render json: collection_presenter, | ||||||
|                serializer: ActivityPub::CollectionSerializer, |                serializer: ActivityPub::CollectionSerializer, | ||||||
|                adapter: ActivityPub::Adapter, |                adapter: ActivityPub::Adapter, | ||||||
|  |  | ||||||
|  | @ -40,6 +40,7 @@ class Settings::PreferencesController < Settings::BaseController | ||||||
|       :setting_reduce_motion, |       :setting_reduce_motion, | ||||||
|       :setting_system_font_ui, |       :setting_system_font_ui, | ||||||
|       :setting_noindex, |       :setting_noindex, | ||||||
|  |       :setting_hide_network, | ||||||
|       notification_emails: %i(follow follow_request reblog favourite mention digest), |       notification_emails: %i(follow follow_request reblog favourite mention digest), | ||||||
|       interactions: %i(must_be_follower must_be_following) |       interactions: %i(must_be_follower must_be_following) | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  | @ -76,9 +76,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) { | ||||||
| 
 | 
 | ||||||
| const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); | const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); | ||||||
| 
 | 
 | ||||||
| export function expandNotifications({ maxId } = {}) { | const noOp = () => {}; | ||||||
|  | 
 | ||||||
|  | export function expandNotifications({ maxId } = {}, done = noOp) { | ||||||
|   return (dispatch, getState) => { |   return (dispatch, getState) => { | ||||||
|     if (getState().getIn(['notifications', 'isLoading'])) { |     const notifications = getState().get('notifications'); | ||||||
|  | 
 | ||||||
|  |     if (notifications.get('isLoading')) { | ||||||
|  |       done(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -87,6 +92,10 @@ export function expandNotifications({ maxId } = {}) { | ||||||
|       exclude_types: excludeTypesFromSettings(getState()), |       exclude_types: excludeTypesFromSettings(getState()), | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |     if (!maxId && notifications.get('items').size > 0) { | ||||||
|  |       params.since_id = notifications.getIn(['items', 0]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     dispatch(expandNotificationsRequest()); |     dispatch(expandNotificationsRequest()); | ||||||
| 
 | 
 | ||||||
|     api(getState).get('/api/v1/notifications', { params }).then(response => { |     api(getState).get('/api/v1/notifications', { params }).then(response => { | ||||||
|  | @ -97,8 +106,10 @@ export function expandNotifications({ maxId } = {}) { | ||||||
| 
 | 
 | ||||||
|       dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); |       dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); | ||||||
|       fetchRelatedRelationships(dispatch, response.data); |       fetchRelatedRelationships(dispatch, response.data); | ||||||
|  |       done(); | ||||||
|     }).catch(error => { |     }).catch(error => { | ||||||
|       dispatch(expandNotificationsFail(error)); |       dispatch(expandNotificationsFail(error)); | ||||||
|  |       done(); | ||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -36,10 +36,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null) | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function refreshHomeTimelineAndNotification (dispatch) { | const refreshHomeTimelineAndNotification = (dispatch, done) => { | ||||||
|   dispatch(expandHomeTimeline()); |   dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done)))); | ||||||
|   dispatch(expandNotifications()); | }; | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); | export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); | ||||||
| export const connectCommunityStream = () => connectTimelineStream('community', 'public:local'); | export const connectCommunityStream = () => connectTimelineStream('community', 'public:local'); | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { importFetchedStatus, importFetchedStatuses } from './importer'; | import { importFetchedStatus, importFetchedStatuses } from './importer'; | ||||||
| import api, { getLinks } from '../api'; | import api, { getLinks } from '../api'; | ||||||
| import { Map as ImmutableMap } from 'immutable'; | import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; | ||||||
| 
 | 
 | ||||||
| export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE'; | export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE'; | ||||||
| export const TIMELINE_DELETE  = 'TIMELINE_DELETE'; | export const TIMELINE_DELETE  = 'TIMELINE_DELETE'; | ||||||
|  | @ -64,35 +64,44 @@ export function deleteFromTimelines(id) { | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export function expandTimeline(timelineId, path, params = {}) { | const noOp = () => {}; | ||||||
|  | 
 | ||||||
|  | export function expandTimeline(timelineId, path, params = {}, done = noOp) { | ||||||
|   return (dispatch, getState) => { |   return (dispatch, getState) => { | ||||||
|     const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); |     const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); | ||||||
| 
 | 
 | ||||||
|     if (timeline.get('isLoading')) { |     if (timeline.get('isLoading')) { | ||||||
|  |       done(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (!params.max_id && timeline.get('items', ImmutableList()).size > 0) { | ||||||
|  |       params.since_id = timeline.getIn(['items', 0]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     dispatch(expandTimelineRequest(timelineId)); |     dispatch(expandTimelineRequest(timelineId)); | ||||||
| 
 | 
 | ||||||
|     api(getState).get(path, { params }).then(response => { |     api(getState).get(path, { params }).then(response => { | ||||||
|       const next = getLinks(response).refs.find(link => link.rel === 'next'); |       const next = getLinks(response).refs.find(link => link.rel === 'next'); | ||||||
|       dispatch(importFetchedStatuses(response.data)); |       dispatch(importFetchedStatuses(response.data)); | ||||||
|       dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206)); |       dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206)); | ||||||
|  |       done(); | ||||||
|     }).catch(error => { |     }).catch(error => { | ||||||
|       dispatch(expandTimelineFail(timelineId, error)); |       dispatch(expandTimelineFail(timelineId, error)); | ||||||
|  |       done(); | ||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const expandHomeTimeline         = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }); | export const expandHomeTimeline         = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); | ||||||
| export const expandPublicTimeline       = ({ maxId } = {}) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId }); | export const expandPublicTimeline       = ({ maxId } = {}, done = noOp) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId }, done); | ||||||
| export const expandCommunityTimeline    = ({ maxId } = {}) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId }); | export const expandCommunityTimeline    = ({ maxId } = {}, done = noOp) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId }, done); | ||||||
| export const expandDirectTimeline       = ({ maxId } = {}) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }); | export const expandDirectTimeline       = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); | ||||||
| export const expandAccountTimeline      = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); | export const expandAccountTimeline      = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); | ||||||
| export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); | export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); | ||||||
| export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); | export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); | ||||||
| export const expandHashtagTimeline      = (hashtag, { maxId } = {}) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }); | export const expandHashtagTimeline      = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done); | ||||||
| export const expandListTimeline         = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }); | export const expandListTimeline         = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); | ||||||
| 
 | 
 | ||||||
| export function expandTimelineRequest(timeline) { | export function expandTimelineRequest(timeline) { | ||||||
|   return { |   return { | ||||||
|  |  | ||||||
|  | @ -1,21 +1,24 @@ | ||||||
| import WebSocketClient from 'websocket.js'; | import WebSocketClient from 'websocket.js'; | ||||||
| 
 | 
 | ||||||
|  | const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max)); | ||||||
|  | 
 | ||||||
| export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onDisconnect() {}, onReceive() {} })) { | export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onDisconnect() {}, onReceive() {} })) { | ||||||
|   return (dispatch, getState) => { |   return (dispatch, getState) => { | ||||||
|     const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); |     const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); | ||||||
|     const accessToken = getState().getIn(['meta', 'access_token']); |     const accessToken = getState().getIn(['meta', 'access_token']); | ||||||
|     const { onDisconnect, onReceive } = callbacks(dispatch, getState); |     const { onDisconnect, onReceive } = callbacks(dispatch, getState); | ||||||
|  | 
 | ||||||
|     let polling = null; |     let polling = null; | ||||||
| 
 | 
 | ||||||
|     const setupPolling = () => { |     const setupPolling = () => { | ||||||
|       polling = setInterval(() => { |       pollingRefresh(dispatch, () => { | ||||||
|         pollingRefresh(dispatch); |         polling = setTimeout(() => setupPolling(), 20000 + randomIntUpTo(20000)); | ||||||
|       }, 20000); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const clearPolling = () => { |     const clearPolling = () => { | ||||||
|       if (polling) { |       if (polling) { | ||||||
|         clearInterval(polling); |         clearTimeout(polling); | ||||||
|         polling = null; |         polling = null; | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|  | @ -29,8 +32,9 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ | ||||||
| 
 | 
 | ||||||
|       disconnected () { |       disconnected () { | ||||||
|         if (pollingRefresh) { |         if (pollingRefresh) { | ||||||
|           setupPolling(); |           polling = setTimeout(() => setupPolling(), randomIntUpTo(40000)); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         onDisconnect(); |         onDisconnect(); | ||||||
|       }, |       }, | ||||||
| 
 | 
 | ||||||
|  | @ -51,6 +55,7 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ | ||||||
|       if (subscription) { |       if (subscription) { | ||||||
|         subscription.close(); |         subscription.close(); | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|       clearPolling(); |       clearPolling(); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -322,6 +322,15 @@ | ||||||
|   z-index: 2; |   z-index: 2; | ||||||
|   position: relative; |   position: relative; | ||||||
| 
 | 
 | ||||||
|  |   &.empty img { | ||||||
|  |     position: absolute; | ||||||
|  |     opacity: 0.2; | ||||||
|  |     height: 200px; | ||||||
|  |     left: 0; | ||||||
|  |     bottom: 0; | ||||||
|  |     pointer-events: none; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   @media screen and (max-width: 740px) { |   @media screen and (max-width: 740px) { | ||||||
|     border-radius: 0; |     border-radius: 0; | ||||||
|     box-shadow: none; |     box-shadow: none; | ||||||
|  | @ -438,8 +447,8 @@ | ||||||
|   font-size: 14px; |   font-size: 14px; | ||||||
|   font-weight: 500; |   font-weight: 500; | ||||||
|   text-align: center; |   text-align: center; | ||||||
|   padding: 60px 0; |   padding: 130px 0; | ||||||
|   padding-top: 55px; |   padding-top: 125px; | ||||||
|   margin: 0 auto; |   margin: 0 auto; | ||||||
|   cursor: default; |   cursor: default; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
|   font-size: 12px; |   font-size: 12px; | ||||||
|   color: $darker-text-color; |   color: $darker-text-color; | ||||||
| 
 | 
 | ||||||
|   .domain { |   .footer__domain { | ||||||
|     font-weight: 500; |     font-weight: 500; | ||||||
| 
 | 
 | ||||||
|     a { |     a { | ||||||
|  |  | ||||||
|  | @ -118,4 +118,13 @@ class ActivityPub::Activity | ||||||
|   def delete_later!(uri) |   def delete_later!(uri) | ||||||
|     redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri) |     redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri) | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def fetch_remote_original_status | ||||||
|  |     if object_uri.start_with?('http') | ||||||
|  |       return if ActivityPub::TagManager.instance.local_uri?(object_uri) | ||||||
|  |       ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first) | ||||||
|  |     elsif @object['url'].present? | ||||||
|  |       ::FetchRemoteStatusService.new.call(@object['url']) | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -5,8 +5,9 @@ class ActivityPub::Activity::Add < ActivityPub::Activity | ||||||
|     return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url |     return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url | ||||||
| 
 | 
 | ||||||
|     status   = status_from_uri(object_uri) |     status   = status_from_uri(object_uri) | ||||||
|  |     status ||= fetch_remote_original_status | ||||||
| 
 | 
 | ||||||
|     return unless status.account_id == @account.id && !@account.pinned?(status) |     return unless !status.nil? && status.account_id == @account.id && !@account.pinned?(status) | ||||||
| 
 | 
 | ||||||
|     StatusPin.create!(account: @account, status: status) |     StatusPin.create!(account: @account, status: status) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -26,16 +26,6 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def fetch_remote_original_status |  | ||||||
|     if object_uri.start_with?('http') |  | ||||||
|       return if ActivityPub::TagManager.instance.local_uri?(object_uri) |  | ||||||
| 
 |  | ||||||
|       ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first) |  | ||||||
|     elsif @object['url'].present? |  | ||||||
|       ::FetchRemoteStatusService.new.call(@object['url']) |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def announceable?(status) |   def announceable?(status) | ||||||
|     status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? |     status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ class ActivityPub::Activity::Remove < ActivityPub::Activity | ||||||
| 
 | 
 | ||||||
|     status = status_from_uri(object_uri) |     status = status_from_uri(object_uri) | ||||||
| 
 | 
 | ||||||
|     return unless status.account_id == @account.id |     return unless !status.nil? && status.account_id == @account.id | ||||||
| 
 | 
 | ||||||
|     pin = StatusPin.find_by(account: @account, status: status) |     pin = StatusPin.find_by(account: @account, status: status) | ||||||
|     pin&.destroy! |     pin&.destroy! | ||||||
|  |  | ||||||
|  | @ -58,7 +58,7 @@ class Request | ||||||
| 
 | 
 | ||||||
|   def set_common_headers! |   def set_common_headers! | ||||||
|     @headers[REQUEST_TARGET]    = "#{@verb} #{@url.path}" |     @headers[REQUEST_TARGET]    = "#{@verb} #{@url.path}" | ||||||
|     @headers['User-Agent']      = user_agent |     @headers['User-Agent']      = Mastodon::Version.user_agent | ||||||
|     @headers['Host']            = @url.host |     @headers['Host']            = @url.host | ||||||
|     @headers['Date']            = Time.now.utc.httpdate |     @headers['Date']            = Time.now.utc.httpdate | ||||||
|     @headers['Accept-Encoding'] = 'gzip' if @verb != :head |     @headers['Accept-Encoding'] = 'gzip' if @verb != :head | ||||||
|  | @ -83,10 +83,6 @@ class Request | ||||||
|     @headers.keys.join(' ').downcase |     @headers.keys.join(' ').downcase | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def user_agent |  | ||||||
|     @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +#{root_url})" |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def key_id |   def key_id | ||||||
|     case @key_id_format |     case @key_id_format | ||||||
|     when :acct |     when :acct | ||||||
|  |  | ||||||
|  | @ -30,6 +30,7 @@ class UserSettingsDecorator | ||||||
|     user.settings['noindex']                 = noindex_preference if change?('setting_noindex') |     user.settings['noindex']                 = noindex_preference if change?('setting_noindex') | ||||||
|     user.settings['flavour']                 = flavour_preference if change?('setting_flavour') |     user.settings['flavour']                 = flavour_preference if change?('setting_flavour') | ||||||
|     user.settings['skin']                    = skin_preference if change?('setting_skin') |     user.settings['skin']                    = skin_preference if change?('setting_skin') | ||||||
|  |     user.settings['hide_network']            = hide_network_preference if change?('setting_hide_network') | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def merged_notification_emails |   def merged_notification_emails | ||||||
|  | @ -92,6 +93,10 @@ class UserSettingsDecorator | ||||||
|     settings['setting_skin'] |     settings['setting_skin'] | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def hide_network_preference | ||||||
|  |     boolean_cast_setting 'setting_hide_network' | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def boolean_cast_setting(key) |   def boolean_cast_setting(key) | ||||||
|     ActiveModel::Type::Boolean.new.cast(settings[key]) |     ActiveModel::Type::Boolean.new.cast(settings[key]) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -139,6 +139,7 @@ class Account < ApplicationRecord | ||||||
|            :moderator?, |            :moderator?, | ||||||
|            :staff?, |            :staff?, | ||||||
|            :locale, |            :locale, | ||||||
|  |            :hides_network?, | ||||||
|            to: :user, |            to: :user, | ||||||
|            prefix: true, |            prefix: true, | ||||||
|            allow_nil: true |            allow_nil: true | ||||||
|  |  | ||||||
|  | @ -86,7 +86,7 @@ class User < ApplicationRecord | ||||||
|   has_many :session_activations, dependent: :destroy |   has_many :session_activations, dependent: :destroy | ||||||
| 
 | 
 | ||||||
|   delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal, |   delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal, | ||||||
|            :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_sensitive_media, |            :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_sensitive_media, :hide_network, | ||||||
|            to: :settings, prefix: :setting, allow_nil: false |            to: :settings, prefix: :setting, allow_nil: false | ||||||
| 
 | 
 | ||||||
|   attr_accessor :invite_code |   attr_accessor :invite_code | ||||||
|  | @ -219,6 +219,10 @@ class User < ApplicationRecord | ||||||
|     settings.notification_emails['digest'] |     settings.notification_emails['digest'] | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def hides_network? | ||||||
|  |     @hides_network ||= settings.hide_network | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def token_for_app(a) |   def token_for_app(a) | ||||||
|     return nil if a.nil? || a.owner != self |     return nil if a.nil? || a.owner != self | ||||||
|     Doorkeeper::AccessToken |     Doorkeeper::AccessToken | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| .accounts-grid | .accounts-grid{ class: accounts.empty? ? 'empty' : '' } | ||||||
|   - if accounts.empty? |   - if accounts.empty? | ||||||
|  |     = image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational' | ||||||
|     = render partial: 'accounts/nothing_here' |     = render partial: 'accounts/nothing_here' | ||||||
|   - else |   - else | ||||||
|     = render partial: 'accounts/grid_card', collection: accounts, as: :account, cached: !user_signed_in? |     = render partial: 'accounts/grid_card', collection: accounts, as: :account, cached: !user_signed_in? | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								app/views/accounts/_follow_grid_hidden.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								app/views/accounts/_follow_grid_hidden.html.haml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | .accounts-grid.empty | ||||||
|  |   = image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational' | ||||||
|  |   %p.nothing-here= t('accounts.network_hidden') | ||||||
|  | @ -7,4 +7,7 @@ | ||||||
| 
 | 
 | ||||||
| = render 'accounts/header', account: @account | = render 'accounts/header', account: @account | ||||||
| 
 | 
 | ||||||
| = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:account) | - if @account.user_hides_network? | ||||||
|  |   = render 'accounts/follow_grid_hidden' | ||||||
|  | - else | ||||||
|  |   = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:account) | ||||||
|  |  | ||||||
|  | @ -7,4 +7,7 @@ | ||||||
| 
 | 
 | ||||||
| = render 'accounts/header', account: @account | = render 'accounts/header', account: @account | ||||||
| 
 | 
 | ||||||
| = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:target_account) | - if @account.user_hides_network? | ||||||
|  |   = render 'accounts/follow_grid_hidden' | ||||||
|  | - else | ||||||
|  |   = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:target_account) | ||||||
|  |  | ||||||
|  | @ -5,9 +5,9 @@ | ||||||
|       %span.single-user-login |       %span.single-user-login | ||||||
|         = link_to t('auth.login'), new_user_session_path |         = link_to t('auth.login'), new_user_session_path | ||||||
|         — |         — | ||||||
|       %span.domain= link_to site_hostname, about_path |       %span.footer__domain= link_to site_hostname, about_path | ||||||
|     - else |     - else | ||||||
|       %span.domain= link_to site_hostname, root_path |       %span.footer__domain= link_to site_hostname, root_path | ||||||
|     %span.powered-by |     %span.powered-by | ||||||
|       != t('generic.powered_by', link: link_to('Mastodon', 'https://joinmastodon.org')) |       != t('generic.powered_by', link: link_to('Mastodon', 'https://joinmastodon.org')) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -26,6 +26,9 @@ | ||||||
|   .fields-group |   .fields-group | ||||||
|     = f.input :setting_noindex, as: :boolean, wrapper: :with_label |     = f.input :setting_noindex, as: :boolean, wrapper: :with_label | ||||||
| 
 | 
 | ||||||
|  |   .fields-group | ||||||
|  |     = f.input :setting_hide_network, as: :boolean, wrapper: :with_label | ||||||
|  | 
 | ||||||
|   %h4= t 'preferences.web' |   %h4= t 'preferences.web' | ||||||
| 
 | 
 | ||||||
|   .fields-group |   .fields-group | ||||||
|  |  | ||||||
|  | @ -18,7 +18,8 @@ module Goldfinger | ||||||
|   def self.finger(uri, opts = {}) |   def self.finger(uri, opts = {}) | ||||||
|     to_hidden = /\.(onion|i2p)(:\d+)?$/.match(uri) |     to_hidden = /\.(onion|i2p)(:\d+)?$/.match(uri) | ||||||
|     raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && to_hidden |     raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && to_hidden | ||||||
|     opts = opts.merge(Rails.configuration.x.http_client_proxy).merge(ssl: !to_hidden) |     opts = { ssl: !to_hidden, headers: {} }.merge(Rails.configuration.x.http_client_proxy).merge(opts) | ||||||
|  |     opts[:headers]['User-Agent'] ||= Mastodon::Version.user_agent | ||||||
|     Goldfinger::Client.new(uri, opts).finger |     Goldfinger::Client.new(uri, opts).finger | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -40,6 +40,7 @@ en: | ||||||
|     following: Following |     following: Following | ||||||
|     media: Media |     media: Media | ||||||
|     moved_html: "%{name} has moved to %{new_profile_link}:" |     moved_html: "%{name} has moved to %{new_profile_link}:" | ||||||
|  |     network_hidden: This information is not available | ||||||
|     nothing_here: There is nothing here! |     nothing_here: There is nothing here! | ||||||
|     people_followed_by: People whom %{name} follows |     people_followed_by: People whom %{name} follows | ||||||
|     people_who_follow: People who follow %{name} |     people_who_follow: People who follow %{name} | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ en: | ||||||
|         note: |         note: | ||||||
|           one: <span class="note-counter">1</span> character left |           one: <span class="note-counter">1</span> character left | ||||||
|           other: <span class="note-counter">%{count}</span> characters left |           other: <span class="note-counter">%{count}</span> characters left | ||||||
|  |         setting_hide_network: Who you follow and who follows you will not be shown on your profile | ||||||
|         setting_noindex: Affects your public profile and status pages |         setting_noindex: Affects your public profile and status pages | ||||||
|         setting_skin: Reskins the selected Mastodon flavour |         setting_skin: Reskins the selected Mastodon flavour | ||||||
|       imports: |       imports: | ||||||
|  | @ -55,6 +56,7 @@ en: | ||||||
|         setting_delete_modal: Show confirmation dialog before deleting a toot |         setting_delete_modal: Show confirmation dialog before deleting a toot | ||||||
|         setting_display_sensitive_media: Always show media marked as sensitive |         setting_display_sensitive_media: Always show media marked as sensitive | ||||||
|         setting_favourite_modal: Show confirmation dialog before favouriting |         setting_favourite_modal: Show confirmation dialog before favouriting | ||||||
|  |         setting_hide_network: Hide your network | ||||||
|         setting_noindex: Opt-out of search engine indexing |         setting_noindex: Opt-out of search engine indexing | ||||||
|         setting_reduce_motion: Reduce motion in animations |         setting_reduce_motion: Reduce motion in animations | ||||||
|         setting_skin: Skin |         setting_skin: Skin | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ defaults: &defaults | ||||||
|   min_invite_role: 'admin' |   min_invite_role: 'admin' | ||||||
|   show_staff_badge: true |   show_staff_badge: true | ||||||
|   default_sensitive: false |   default_sensitive: false | ||||||
|  |   hide_network: false | ||||||
|   unfollow_modal: false |   unfollow_modal: false | ||||||
|   boost_modal: false |   boost_modal: false | ||||||
|   favourite_modal: false |   favourite_modal: false | ||||||
|  |  | ||||||
|  | @ -48,5 +48,9 @@ module Mastodon | ||||||
|         source_base_url |         source_base_url | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     def user_agent | ||||||
|  |       @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Version}; +http#{Rails.configuration.x.use_https ? 's' : ''}://#{Rails.configuration.x.web_domain}/)" | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -18,12 +18,31 @@ RSpec.describe ActivityPub::Activity::Add do | ||||||
|   describe '#perform' do |   describe '#perform' do | ||||||
|     subject { described_class.new(json, sender) } |     subject { described_class.new(json, sender) } | ||||||
| 
 | 
 | ||||||
|     before do |     it 'creates a pin' do | ||||||
|       subject.perform |       subject.perform | ||||||
|  |       expect(sender.pinned?(status)).to be true | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'creates a pin' do |     context 'when status was not known before' do | ||||||
|       expect(sender.pinned?(status)).to be true |       let(:json) do | ||||||
|  |         { | ||||||
|  |           '@context': 'https://www.w3.org/ns/activitystreams', | ||||||
|  |           id: 'foo', | ||||||
|  |           type: 'Add', | ||||||
|  |           actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |           object: 'https://example.com/unknown', | ||||||
|  |           target: sender.featured_collection_url, | ||||||
|  |         }.with_indifferent_access | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         stub_request(:get, 'https://example.com/unknown').to_return(status: 410) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'fetches the status' do | ||||||
|  |         subject.perform | ||||||
|  |         expect(a_request(:get, 'https://example.com/unknown')).to have_been_made.at_least_once | ||||||
|  |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue