mirror of
https://github.com/mastodon/mastodon.git
synced 2024-11-20 03:25:17 +01:00
Change public accounts pages to mount the web UI (#19319)
* Change public accounts pages to mount the web UI * Fix handling of remote usernames in routes - When logged in, serve web app - When logged out, redirect to permalink - Fix `app-body` class not being set sometimes due to name conflict * Fix missing `multiColumn` prop * Fix failing test * Use `discoverable` attribute to control indexing directives * Fix `<ColumnLoading />` not using `multiColumn` * Add `noindex` to accounts in REST API * Change noindex directive to not be rendered by default before a route is mounted * Add loading indicator for detailed status in web UI * Fix missing indicator appearing while account is loading in web UI
This commit is contained in:
parent
b0e3f0312c
commit
839f893168
@ -5,7 +5,15 @@ class AboutController < ApplicationController
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :set_instance_presenter
|
||||
|
||||
def show
|
||||
expires_in 0, public: true unless user_signed_in?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
end
|
||||
|
@ -1,12 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountFollowController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
def create
|
||||
FollowService.new.call(current_user.account, @account, with_rate_limit: true)
|
||||
redirect_to account_path(@account)
|
||||
end
|
||||
end
|
@ -1,12 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountUnfollowController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
def create
|
||||
UnfollowService.new.call(current_user.account, @account)
|
||||
redirect_to account_path(@account)
|
||||
end
|
||||
end
|
@ -9,7 +9,6 @@ class AccountsController < ApplicationController
|
||||
|
||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_cache_headers
|
||||
before_action :set_body_classes
|
||||
|
||||
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
@ -18,24 +17,6 @@ class AccountsController < ApplicationController
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
expires_in 0, public: true unless user_signed_in?
|
||||
|
||||
@pinned_statuses = []
|
||||
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
|
||||
@featured_hashtags = @account.featured_tags.order(statuses_count: :desc)
|
||||
|
||||
if current_account && @account.blocking?(current_account)
|
||||
@statuses = []
|
||||
return
|
||||
end
|
||||
|
||||
@pinned_statuses = cached_filtered_status_pins if show_pinned_statuses?
|
||||
@statuses = cached_filtered_status_page
|
||||
@rss_url = rss_url
|
||||
|
||||
unless @statuses.empty?
|
||||
@older_url = older_url if @statuses.last.id > filtered_statuses.last.id
|
||||
@newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id
|
||||
end
|
||||
end
|
||||
|
||||
format.rss do
|
||||
@ -55,18 +36,6 @@ class AccountsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'with-modals'
|
||||
end
|
||||
|
||||
def show_pinned_statuses?
|
||||
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
|
||||
end
|
||||
|
||||
def filtered_pinned_statuses
|
||||
@account.pinned_statuses.where(visibility: [:public, :unlisted])
|
||||
end
|
||||
|
||||
def filtered_statuses
|
||||
default_statuses.tap do |statuses|
|
||||
statuses.merge!(hashtag_scope) if tag_requested?
|
||||
@ -113,26 +82,6 @@ class AccountsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def older_url
|
||||
pagination_url(max_id: @statuses.last.id)
|
||||
end
|
||||
|
||||
def newer_url
|
||||
pagination_url(min_id: @statuses.first.id)
|
||||
end
|
||||
|
||||
def pagination_url(max_id: nil, min_id: nil)
|
||||
if tag_requested?
|
||||
short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id)
|
||||
elsif media_requested?
|
||||
short_account_media_url(@account, max_id: max_id, min_id: min_id)
|
||||
elsif replies_requested?
|
||||
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
|
||||
else
|
||||
short_account_url(@account, max_id: max_id, min_id: min_id)
|
||||
end
|
||||
end
|
||||
|
||||
def media_requested?
|
||||
request.path.split('.').first.end_with?('/media') && !tag_requested?
|
||||
end
|
||||
@ -145,13 +94,6 @@ class AccountsController < ApplicationController
|
||||
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||
end
|
||||
|
||||
def cached_filtered_status_pins
|
||||
cache_collection(
|
||||
filtered_pinned_statuses,
|
||||
Status
|
||||
)
|
||||
end
|
||||
|
||||
def cached_filtered_status_page
|
||||
cache_collection_paginated_by_id(
|
||||
filtered_statuses,
|
||||
|
@ -3,13 +3,12 @@
|
||||
module AccountControllerConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include WebAppControllerConcern
|
||||
include AccountOwnedConcern
|
||||
|
||||
FOLLOW_PER_PAGE = 12
|
||||
|
||||
included do
|
||||
layout 'public'
|
||||
|
||||
before_action :set_instance_presenter
|
||||
before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
|
||||
end
|
||||
|
@ -4,15 +4,24 @@ module WebAppControllerConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_body_classes
|
||||
before_action :redirect_unauthenticated_to_permalinks!
|
||||
before_action :set_app_body_class
|
||||
before_action :set_referrer_policy_header
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
def set_app_body_class
|
||||
@body_classes = 'app-body'
|
||||
end
|
||||
|
||||
def set_referrer_policy_header
|
||||
response.headers['Referrer-Policy'] = 'origin'
|
||||
end
|
||||
|
||||
def redirect_unauthenticated_to_permalinks!
|
||||
return if user_signed_in?
|
||||
|
||||
redirect_path = PermalinkRedirector.new(request.path).redirect_path
|
||||
|
||||
redirect_to(redirect_path) if redirect_path.present?
|
||||
end
|
||||
end
|
||||
|
@ -3,6 +3,7 @@
|
||||
class FollowerAccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
include WebAppControllerConcern
|
||||
|
||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_cache_headers
|
||||
@ -14,10 +15,6 @@ class FollowerAccountsController < ApplicationController
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
expires_in 0, public: true unless user_signed_in?
|
||||
|
||||
next if @account.hide_collections?
|
||||
|
||||
follows
|
||||
end
|
||||
|
||||
format.json do
|
||||
|
@ -3,6 +3,7 @@
|
||||
class FollowingAccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
include WebAppControllerConcern
|
||||
|
||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_cache_headers
|
||||
@ -14,10 +15,6 @@ class FollowingAccountsController < ApplicationController
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
expires_in 0, public: true unless user_signed_in?
|
||||
|
||||
next if @account.hide_collections?
|
||||
|
||||
follows
|
||||
end
|
||||
|
||||
format.json do
|
||||
|
@ -3,21 +3,14 @@
|
||||
class HomeController < ApplicationController
|
||||
include WebAppControllerConcern
|
||||
|
||||
before_action :redirect_unauthenticated_to_permalinks!
|
||||
before_action :set_instance_presenter
|
||||
|
||||
def index; end
|
||||
def index
|
||||
expires_in 0, public: true unless user_signed_in?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def redirect_unauthenticated_to_permalinks!
|
||||
return if user_signed_in?
|
||||
|
||||
redirect_path = PermalinkRedirector.new(request.path).redirect_path
|
||||
|
||||
redirect_to(redirect_path) if redirect_path.present?
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
@ -5,7 +5,15 @@ class PrivacyController < ApplicationController
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :set_instance_presenter
|
||||
|
||||
def show
|
||||
expires_in 0, public: true if current_account.nil?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
end
|
||||
|
@ -1,41 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RemoteFollowController < ApplicationController
|
||||
include AccountOwnedConcern
|
||||
|
||||
layout 'modal'
|
||||
|
||||
before_action :set_body_classes
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def new
|
||||
@remote_follow = RemoteFollow.new(session_params)
|
||||
end
|
||||
|
||||
def create
|
||||
@remote_follow = RemoteFollow.new(resource_params)
|
||||
|
||||
if @remote_follow.valid?
|
||||
session[:remote_follow] = @remote_follow.acct
|
||||
redirect_to @remote_follow.subscribe_address_for(@account)
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.require(:remote_follow).permit(:acct)
|
||||
end
|
||||
|
||||
def session_params
|
||||
{ acct: session[:remote_follow] || current_account&.username }
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'modal-layout'
|
||||
@hide_header = true
|
||||
end
|
||||
end
|
@ -1,55 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RemoteInteractionController < ApplicationController
|
||||
include Authorization
|
||||
|
||||
layout 'modal'
|
||||
|
||||
before_action :authenticate_user!, if: :whitelist_mode?
|
||||
before_action :set_interaction_type
|
||||
before_action :set_status
|
||||
before_action :set_body_classes
|
||||
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
|
||||
def new
|
||||
@remote_follow = RemoteFollow.new(session_params)
|
||||
end
|
||||
|
||||
def create
|
||||
@remote_follow = RemoteFollow.new(resource_params)
|
||||
|
||||
if @remote_follow.valid?
|
||||
session[:remote_follow] = @remote_follow.acct
|
||||
redirect_to @remote_follow.interact_address_for(@status)
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.require(:remote_follow).permit(:acct)
|
||||
end
|
||||
|
||||
def session_params
|
||||
{ acct: session[:remote_follow] || current_account&.username }
|
||||
end
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'modal-layout'
|
||||
@hide_header = true
|
||||
end
|
||||
|
||||
def set_interaction_type
|
||||
@interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply'
|
||||
end
|
||||
end
|
@ -1,11 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StatusesController < ApplicationController
|
||||
include WebAppControllerConcern
|
||||
include StatusControllerConcern
|
||||
include SignatureAuthentication
|
||||
include Authorization
|
||||
include AccountOwnedConcern
|
||||
include WebAppControllerConcern
|
||||
|
||||
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_status
|
||||
|
@ -2,18 +2,16 @@
|
||||
|
||||
class TagsController < ApplicationController
|
||||
include SignatureVerification
|
||||
include WebAppControllerConcern
|
||||
|
||||
PAGE_SIZE = 20
|
||||
PAGE_SIZE_MAX = 200
|
||||
|
||||
layout 'public'
|
||||
|
||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :authenticate_user!, if: :whitelist_mode?
|
||||
before_action :set_local
|
||||
before_action :set_tag
|
||||
before_action :set_statuses
|
||||
before_action :set_body_classes
|
||||
before_action :set_instance_presenter
|
||||
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
@ -21,7 +19,7 @@ class TagsController < ApplicationController
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to web_path("tags/#{@tag.name}")
|
||||
expires_in 0, public: true unless user_signed_in?
|
||||
end
|
||||
|
||||
format.rss do
|
||||
@ -54,10 +52,6 @@ class TagsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'with-modals'
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
@ -20,54 +20,10 @@ module AccountsHelper
|
||||
end
|
||||
|
||||
def account_action_button(account)
|
||||
if user_signed_in?
|
||||
if account.id == current_user.account_id
|
||||
link_to settings_profile_url, class: 'button logo-button' do
|
||||
safe_join([logo_as_symbol, t('settings.edit_profile')])
|
||||
end
|
||||
elsif current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
|
||||
safe_join([logo_as_symbol, t('accounts.unfollow')])
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
|
||||
safe_join([logo_as_symbol, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
|
||||
safe_join([logo_as_symbol, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
end
|
||||
return if account.memorial? || account.moved?
|
||||
|
||||
def minimal_account_action_button(account)
|
||||
if user_signed_in?
|
||||
return if account.id == current_user.account_id
|
||||
|
||||
if current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
|
||||
fa_icon('user-times fw')
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def account_badge(account)
|
||||
if account.bot?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||
elsif account.group?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles')
|
||||
elsif account.user_role&.highlighted?
|
||||
content_tag(:div, content_tag(:div, account.user_role.name, class: "account-role user-role-#{account.user_role.id}"), class: 'roles')
|
||||
link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do
|
||||
safe_join([logo_as_symbol, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { version, source_url } from 'mastodon/initial_state';
|
||||
import StackTrace from 'stacktrace-js';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
export default class ErrorBoundary extends React.PureComponent {
|
||||
|
||||
@ -84,6 +85,7 @@ export default class ErrorBoundary extends React.PureComponent {
|
||||
<FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' />
|
||||
)}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{ likelyBrowserAddonIssue ? (
|
||||
<FormattedMessage id='error.unexpected_crash.next_steps_addons' defaultMessage='Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
|
||||
@ -91,8 +93,13 @@ export default class ErrorBoundary extends React.PureComponent {
|
||||
<FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
|
||||
)}
|
||||
</p>
|
||||
|
||||
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import illustration from 'mastodon/../images/elephant_ui_disappointed.svg';
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const MissingIndicator = ({ fullPage }) => (
|
||||
<div className={classNames('regeneration-indicator', { 'regeneration-indicator--without-header': fullPage })}>
|
||||
@ -14,6 +15,10 @@ const MissingIndicator = ({ fullPage }) => (
|
||||
<FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' />
|
||||
<FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' />
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -78,7 +78,7 @@ export default class Mastodon extends React.PureComponent {
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<ReduxProvider store={store}>
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter basename='/web'>
|
||||
<BrowserRouter>
|
||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||
<Route path='/' component={UI} />
|
||||
</ScrollContext>
|
||||
|
@ -94,6 +94,7 @@ class About extends React.PureComponent {
|
||||
}),
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
@ -108,11 +109,11 @@ class About extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, server, extendedDescription, domainBlocks } = this.props;
|
||||
const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props;
|
||||
const isLoading = server.get('isLoading');
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
|
||||
<div className='scrollable about'>
|
||||
<div className='about__header'>
|
||||
<Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
|
||||
@ -212,6 +213,7 @@ class About extends React.PureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='all' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -270,7 +270,9 @@ class Header extends ImmutablePureComponent {
|
||||
const content = { __html: account.get('note_emojified') };
|
||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||
const fields = account.get('fields');
|
||||
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
||||
const isLocal = account.get('acct').indexOf('@') === -1;
|
||||
const acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
||||
const isIndexable = !account.get('noindex');
|
||||
|
||||
let badge;
|
||||
|
||||
@ -373,6 +375,7 @@ class Header extends ImmutablePureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{titleFromAccount(account)}</title>
|
||||
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
||||
</Helmet>
|
||||
</div>
|
||||
);
|
||||
|
@ -142,7 +142,13 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
render () {
|
||||
const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
if (isLoading && statusIds.isEmpty()) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
} else if (!isLoading && !isAccount) {
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton multiColumn={multiColumn} />
|
||||
@ -151,14 +157,6 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (!statusIds && isLoading) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
let emptyMessage;
|
||||
|
||||
const forceEmptyState = suspended || blockedBy || hidden;
|
||||
|
@ -99,6 +99,7 @@ class Bookmarks extends ImmutablePureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -151,6 +151,7 @@ class CommunityTimeline extends React.PureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -18,6 +18,7 @@ import { mascot } from '../../initial_state';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { logOut } from 'mastodon/utils/log_out';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
@ -145,6 +146,10 @@ class Compose extends React.PureComponent {
|
||||
<Column onFocus={this.onFocus}>
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
<ComposeFormContainer />
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ class DirectTimeline extends React.PureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -169,6 +169,7 @@ class Directory extends React.PureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -11,6 +11,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import DomainContainer from '../../containers/domain_container';
|
||||
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
|
||||
@ -59,6 +60,7 @@ class Blocks extends ImmutablePureComponent {
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='domain_blocks'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
@ -70,6 +72,10 @@ class Blocks extends ImmutablePureComponent {
|
||||
<DomainContainer key={domain} domain={domain} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ class Explore extends React.PureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content={isSearching ? 'noindex' : 'all'} />
|
||||
</Helmet>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
@ -99,6 +99,7 @@ class Favourites extends ImmutablePureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -11,6 +11,7 @@ import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import AccountContainer from 'mastodon/containers/account_container';
|
||||
import Column from 'mastodon/features/ui/components/column';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
|
||||
@ -80,6 +81,10 @@ class Favourites extends ImmutablePureComponent {
|
||||
<AccountContainer key={id} id={id} withNote={false} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import Column from 'mastodon/features/ui/components/column';
|
||||
import Account from './components/account';
|
||||
import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg';
|
||||
import Button from 'mastodon/components/button';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
suggestions: state.getIn(['suggestions', 'items']),
|
||||
@ -104,6 +105,10 @@ class FollowRecommendations extends ImmutablePureComponent {
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import AccountAuthorizeContainer from './containers/account_authorize_container'
|
||||
import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { me } from '../../initial_state';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' },
|
||||
@ -87,6 +88,10 @@ class FollowRequests extends ImmutablePureComponent {
|
||||
<AccountAuthorizeContainer key={id} id={id} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
@ -138,6 +138,7 @@ class GettingStarted extends ImmutablePureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.menu)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -228,6 +228,7 @@ class HashtagTimeline extends React.PureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>#{id}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -20,7 +20,7 @@ const messages = defineMessages({
|
||||
title: { id: 'column.home', defaultMessage: 'Home' },
|
||||
show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' },
|
||||
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
|
||||
});
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
||||
@ -167,6 +167,7 @@ class HomeTimeline extends React.PureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -4,6 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
|
||||
@ -164,6 +165,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
@ -212,6 +212,7 @@ class ListTimeline extends React.PureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -80,6 +80,7 @@ class Lists extends ImmutablePureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -11,6 +11,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import { fetchMutes, expandMutes } from '../../actions/mutes';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.mutes', defaultMessage: 'Muted users' },
|
||||
@ -72,6 +73,10 @@ class Mutes extends ImmutablePureComponent {
|
||||
<AccountContainer key={id} id={id} defaultAction='mute' />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
@ -281,6 +281,7 @@ class Notifications extends React.PureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import StatusList from '../../components/status_list';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.pins', defaultMessage: 'Pinned post' },
|
||||
@ -54,6 +55,9 @@ class PinnedStatuses extends ImmutablePureComponent {
|
||||
hasMore={hasMore}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ class PrivacyPolicy extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -32,11 +33,11 @@ class PrivacyPolicy extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl } = this.props;
|
||||
const { intl, multiColumn } = this.props;
|
||||
const { isLoading, content, lastUpdated } = this.state;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
|
||||
<div className='scrollable privacy-policy'>
|
||||
<div className='column-title'>
|
||||
<h3><FormattedMessage id='privacy_policy.title' defaultMessage='Privacy Policy' /></h3>
|
||||
@ -51,6 +52,7 @@ class PrivacyPolicy extends React.PureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='all' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -153,6 +153,7 @@ class PublicTimeline extends React.PureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -11,6 +11,7 @@ import Column from '../ui/components/column';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
|
||||
@ -80,6 +81,10 @@ class Reblogs extends ImmutablePureComponent {
|
||||
<AccountContainer key={id} id={id} withNote={false} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchStatus } from '../../actions/statuses';
|
||||
import MissingIndicator from '../../components/missing_indicator';
|
||||
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||
import DetailedStatus from './components/detailed_status';
|
||||
import ActionBar from './components/action_bar';
|
||||
import Column from '../ui/components/column';
|
||||
@ -145,6 +146,7 @@ const makeMapStateToProps = () => {
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading: state.getIn(['statuses', props.params.statusId, 'isLoading']),
|
||||
status,
|
||||
ancestorsIds,
|
||||
descendantsIds,
|
||||
@ -187,6 +189,7 @@ class Status extends ImmutablePureComponent {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
status: ImmutablePropTypes.map,
|
||||
isLoading: PropTypes.bool,
|
||||
ancestorsIds: ImmutablePropTypes.list,
|
||||
descendantsIds: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
@ -566,9 +569,17 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
render () {
|
||||
let ancestors, descendants;
|
||||
const { status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
|
||||
const { isLoading, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
|
||||
const { fullscreen } = this.state;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === null) {
|
||||
return (
|
||||
<Column>
|
||||
@ -586,6 +597,9 @@ class Status extends ImmutablePureComponent {
|
||||
descendants = <div>{this.renderChildren(descendantsIds)}</div>;
|
||||
}
|
||||
|
||||
const isLocal = status.getIn(['account', 'acct'], '').indexOf('@') === -1;
|
||||
const isIndexable = !status.getIn(['account', 'noindex']);
|
||||
|
||||
const handlers = {
|
||||
moveUp: this.handleHotkeyMoveUp,
|
||||
moveDown: this.handleHotkeyMoveDown,
|
||||
@ -659,6 +673,7 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
<Helmet>
|
||||
<title>{titleFromStatus(status)}</title>
|
||||
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
|
@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import Column from './column';
|
||||
import ColumnHeader from './column_header';
|
||||
import ColumnBackButtonSlim from '../../../components/column_back_button_slim';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' },
|
||||
@ -18,6 +17,7 @@ class BundleColumnError extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onRetry: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
}
|
||||
|
||||
handleRetry = () => {
|
||||
@ -25,16 +25,25 @@ class BundleColumnError extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
const { multiColumn, intl: { formatMessage } } = this.props;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnHeader icon='exclamation-circle' type={formatMessage(messages.title)} />
|
||||
<ColumnBackButtonSlim />
|
||||
<Column bindToDocument={!multiColumn} label={formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
icon='exclamation-circle'
|
||||
title={formatMessage(messages.title)}
|
||||
showBackButton
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
|
||||
<div className='error-column'>
|
||||
<IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} />
|
||||
{formatMessage(messages.body)}
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ export default class ColumnLoading extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
title: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
icon: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -18,10 +19,11 @@ export default class ColumnLoading extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
let { title, icon } = this.props;
|
||||
let { title, icon, multiColumn } = this.props;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} placeholder />
|
||||
<ColumnHeader icon={icon} title={title} multiColumn={multiColumn} focusable={false} placeholder />
|
||||
<div className='scrollable' />
|
||||
</Column>
|
||||
);
|
||||
|
@ -139,11 +139,11 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
renderLoading = columnId => () => {
|
||||
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />;
|
||||
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
|
||||
}
|
||||
|
||||
renderError = (props) => {
|
||||
return <BundleColumnError {...props} />;
|
||||
return <BundleColumnError multiColumn {...props} />;
|
||||
}
|
||||
|
||||
render () {
|
||||
|
@ -11,9 +11,7 @@ import VideoModal from './video_modal';
|
||||
import BoostModal from './boost_modal';
|
||||
import AudioModal from './audio_modal';
|
||||
import ConfirmationModal from './confirmation_modal';
|
||||
import SubscribedLanguagesModal from 'mastodon/features/subscribed_languages_modal';
|
||||
import FocalPointModal from './focal_point_modal';
|
||||
import InteractionModal from 'mastodon/features/interaction_modal';
|
||||
import {
|
||||
MuteModal,
|
||||
BlockModal,
|
||||
@ -23,7 +21,10 @@ import {
|
||||
ListAdder,
|
||||
CompareHistoryModal,
|
||||
FilterModal,
|
||||
InteractionModal,
|
||||
SubscribedLanguagesModal,
|
||||
} from 'mastodon/features/ui/util/async-components';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const MODAL_COMPONENTS = {
|
||||
'MEDIA': () => Promise.resolve({ default: MediaModal }),
|
||||
@ -41,8 +42,8 @@ const MODAL_COMPONENTS = {
|
||||
'LIST_ADDER': ListAdder,
|
||||
'COMPARE_HISTORY': CompareHistoryModal,
|
||||
'FILTER': FilterModal,
|
||||
'SUBSCRIBED_LANGUAGES': () => Promise.resolve({ default: SubscribedLanguagesModal }),
|
||||
'INTERACTION': () => Promise.resolve({ default: InteractionModal }),
|
||||
'SUBSCRIBED_LANGUAGES': SubscribedLanguagesModal,
|
||||
'INTERACTION': InteractionModal,
|
||||
};
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
@ -111,9 +112,15 @@ export default class ModalRoot extends React.PureComponent {
|
||||
return (
|
||||
<Base backgroundColor={backgroundColor} onClose={this.handleClose} ignoreFocus={ignoreFocus}>
|
||||
{visible && (
|
||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
||||
{(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />}
|
||||
</BundleContainer>
|
||||
<>
|
||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
||||
{(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />}
|
||||
</BundleContainer>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</>
|
||||
)}
|
||||
</Base>
|
||||
);
|
||||
|
@ -197,8 +197,8 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
|
||||
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
|
||||
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
|
||||
<WrappedRoute path={['/@:acct/followers', '/accounts/:id/followers']} component={Followers} content={children} />
|
||||
<WrappedRoute path={['/@:acct/following', '/accounts/:id/following']} component={Following} content={children} />
|
||||
<WrappedRoute path={['/accounts/:id/followers', '/users/:acct/followers', '/@:acct/followers']} component={Followers} content={children} />
|
||||
<WrappedRoute path={['/accounts/:id/following', '/users/:acct/following', '/@:acct/following']} component={Following} content={children} />
|
||||
<WrappedRoute path={['/@:acct/media', '/accounts/:id/media']} component={AccountGallery} content={children} />
|
||||
<WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
|
||||
<WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
|
||||
|
@ -166,6 +166,14 @@ export function FilterModal () {
|
||||
return import(/*webpackChunkName: "modals/filter_modal" */'../components/filter_modal');
|
||||
}
|
||||
|
||||
export function InteractionModal () {
|
||||
return import(/*webpackChunkName: "modals/interaction_modal" */'../../interaction_modal');
|
||||
}
|
||||
|
||||
export function SubscribedLanguagesModal () {
|
||||
return import(/*webpackChunkName: "modals/subscribed_languages_modal" */'../../subscribed_languages_modal');
|
||||
}
|
||||
|
||||
export function About () {
|
||||
return import(/*webpackChunkName: "features/about" */'../../about');
|
||||
}
|
||||
|
@ -53,7 +53,9 @@ export class WrappedRoute extends React.Component {
|
||||
}
|
||||
|
||||
renderLoading = () => {
|
||||
return <ColumnLoading />;
|
||||
const { multiColumn } = this.props;
|
||||
|
||||
return <ColumnLoading multiColumn={multiColumn} />;
|
||||
}
|
||||
|
||||
renderError = (props) => {
|
||||
|
@ -12,14 +12,6 @@ const perf = require('mastodon/performance');
|
||||
function main() {
|
||||
perf.start('main()');
|
||||
|
||||
if (window.history && history.replaceState) {
|
||||
const { pathname, search, hash } = window.location;
|
||||
const path = pathname + search + hash;
|
||||
if (!(/^\/web($|\/)/).test(path)) {
|
||||
history.replaceState(null, document.title, `/web${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
return ready(async () => {
|
||||
const mountNode = document.getElementById('mastodon');
|
||||
const props = JSON.parse(mountNode.getAttribute('data-props'));
|
||||
|
@ -15,6 +15,8 @@ import {
|
||||
STATUS_COLLAPSE,
|
||||
STATUS_TRANSLATE_SUCCESS,
|
||||
STATUS_TRANSLATE_UNDO,
|
||||
STATUS_FETCH_REQUEST,
|
||||
STATUS_FETCH_FAIL,
|
||||
} from '../actions/statuses';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
||||
@ -37,6 +39,10 @@ const initialState = ImmutableMap();
|
||||
|
||||
export default function statuses(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case STATUS_FETCH_REQUEST:
|
||||
return state.setIn([action.id, 'isLoading'], true);
|
||||
case STATUS_FETCH_FAIL:
|
||||
return state.delete(action.id);
|
||||
case STATUS_IMPORT:
|
||||
return importStatus(state, action.status);
|
||||
case STATUSES_IMPORT:
|
||||
|
@ -41,7 +41,7 @@ export const makeGetStatus = () => {
|
||||
],
|
||||
|
||||
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
|
||||
if (!statusBase) {
|
||||
if (!statusBase || statusBase.get('isLoading')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ const notify = options =>
|
||||
icon: '/android-chrome-192x192.png',
|
||||
tag: GROUP_TAG,
|
||||
data: {
|
||||
url: (new URL('/web/notifications', self.location)).href,
|
||||
url: (new URL('/notifications', self.location)).href,
|
||||
count: notifications.length + 1,
|
||||
preferred_locale: options.data.preferred_locale,
|
||||
},
|
||||
@ -90,7 +90,7 @@ export const handlePush = (event) => {
|
||||
options.tag = notification.id;
|
||||
options.badge = '/badge.png';
|
||||
options.image = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined;
|
||||
options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/@${notification.account.acct}/${notification.status.id}` : `/web/@${notification.account.acct}` };
|
||||
options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.acct}/${notification.status.id}` : `/@${notification.account.acct}` };
|
||||
|
||||
if (notification.status && notification.status.spoiler_text || notification.status.sensitive) {
|
||||
options.data.hiddenBody = htmlToPlainText(notification.status.content);
|
||||
@ -115,7 +115,7 @@ export const handlePush = (event) => {
|
||||
tag: notification_id,
|
||||
timestamp: new Date(),
|
||||
badge: '/badge.png',
|
||||
data: { access_token, preferred_locale, url: '/web/notifications' },
|
||||
data: { access_token, preferred_locale, url: '/notifications' },
|
||||
});
|
||||
}),
|
||||
);
|
||||
@ -166,24 +166,10 @@ const removeActionFromNotification = (notification, action) => {
|
||||
|
||||
const openUrl = url =>
|
||||
self.clients.matchAll({ type: 'window' }).then(clientList => {
|
||||
if (clientList.length !== 0) {
|
||||
const webClients = clientList.filter(client => /\/web\//.test(client.url));
|
||||
if (clientList.length !== 0 && 'navigate' in clientList[0]) { // Chrome 42-48 does not support navigate
|
||||
const client = findBestClient(clientList);
|
||||
|
||||
if (webClients.length !== 0) {
|
||||
const client = findBestClient(webClients);
|
||||
const { pathname } = new URL(url, self.location);
|
||||
|
||||
if (pathname.startsWith('/web/')) {
|
||||
return client.focus().then(client => client.postMessage({
|
||||
type: 'navigate',
|
||||
path: pathname.slice('/web/'.length - 1),
|
||||
}));
|
||||
}
|
||||
} else if ('navigate' in clientList[0]) { // Chrome 42-48 does not support navigate
|
||||
const client = findBestClient(clientList);
|
||||
|
||||
return client.navigate(url).then(client => client.focus());
|
||||
}
|
||||
return client.navigate(url).then(client => client.focus());
|
||||
}
|
||||
|
||||
return self.clients.openWindow(url);
|
||||
|
@ -33,7 +33,6 @@ function main() {
|
||||
const { messages } = getLocale();
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
const Rellax = require('rellax');
|
||||
const { createBrowserHistory } = require('history');
|
||||
|
||||
const scrollToDetailedStatus = () => {
|
||||
@ -112,12 +111,6 @@ function main() {
|
||||
scrollToDetailedStatus();
|
||||
}
|
||||
|
||||
const parallaxComponents = document.querySelectorAll('.parallax');
|
||||
|
||||
if (parallaxComponents.length > 0 ) {
|
||||
new Rellax('.parallax', { speed: -1 });
|
||||
}
|
||||
|
||||
delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => {
|
||||
const password = document.getElementById('registration_user_password');
|
||||
const confirmation = document.getElementById('registration_user_password_confirmation');
|
||||
@ -168,28 +161,6 @@ function main() {
|
||||
});
|
||||
});
|
||||
|
||||
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
|
||||
if (button !== 0) {
|
||||
return true;
|
||||
}
|
||||
window.location.href = target.href;
|
||||
return false;
|
||||
});
|
||||
|
||||
delegate(document, '.modal-button', 'click', e => {
|
||||
e.preventDefault();
|
||||
|
||||
let href;
|
||||
|
||||
if (e.target.nodeName !== 'A') {
|
||||
href = e.target.parentNode.href;
|
||||
} else {
|
||||
href = e.target.href;
|
||||
}
|
||||
|
||||
window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
||||
});
|
||||
|
||||
delegate(document, '#account_display_name', 'input', ({ target }) => {
|
||||
const name = document.querySelector('.card .display-name strong');
|
||||
if (name) {
|
||||
|
@ -8,7 +8,6 @@
|
||||
@import 'mastodon/branding';
|
||||
@import 'mastodon/containers';
|
||||
@import 'mastodon/lists';
|
||||
@import 'mastodon/footer';
|
||||
@import 'mastodon/widgets';
|
||||
@import 'mastodon/forms';
|
||||
@import 'mastodon/accounts';
|
||||
|
@ -68,10 +68,6 @@
|
||||
color: $darker-text-color;
|
||||
}
|
||||
|
||||
.public-layout .public-account-header__tabs__tabs .counter.active::after {
|
||||
border-bottom: 4px solid $ui-highlight-color;
|
||||
}
|
||||
|
||||
.compose-form .autosuggest-textarea__textarea::placeholder,
|
||||
.compose-form .spoiler-input__input::placeholder {
|
||||
color: $inverted-text-color;
|
||||
|
@ -655,95 +655,6 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
.public-layout {
|
||||
.account__section-headline {
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header,
|
||||
.public-account-header,
|
||||
.public-account-bio {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.public-account-bio,
|
||||
.hero-widget__text {
|
||||
background: $account-background-color;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: $ui-base-color;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.brand {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.public-account-header {
|
||||
&__image {
|
||||
background: lighten($ui-base-color, 12%);
|
||||
|
||||
&::after {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__bar {
|
||||
&::before {
|
||||
background: $account-background-color;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
border-color: $account-background-color;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
background: $account-background-color;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
&__name {
|
||||
h1,
|
||||
h1 small {
|
||||
color: $white;
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__extra {
|
||||
.public-account-bio {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.public-account-bio .account__header__fields {
|
||||
border-color: lighten($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification__filter-bar button.active::after,
|
||||
.account__section-headline a.active::after {
|
||||
border-color: transparent transparent $white;
|
||||
|
@ -104,785 +104,3 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
grid-auto-columns: 25%;
|
||||
grid-auto-rows: max-content;
|
||||
|
||||
.column-0 {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.column-1 {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.column-2 {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.column-3 {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
grid-gap: 0;
|
||||
grid-template-columns: minmax(0, 100%);
|
||||
|
||||
.column-0 {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.column-1 {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.column-2 {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.column-3 {
|
||||
grid-column: 1;
|
||||
grid-row: 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid-4 {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-auto-columns: 25%;
|
||||
grid-auto-rows: max-content;
|
||||
|
||||
.column-0 {
|
||||
grid-column: 1 / 5;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.column-1 {
|
||||
grid-column: 1 / 4;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.column-2 {
|
||||
grid-column: 4;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.column-3 {
|
||||
grid-column: 2 / 5;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.column-4 {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.landing-page__call-to-action {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.flash-message {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 738px) {
|
||||
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
|
||||
|
||||
.landing-page__call-to-action {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.row__information-board {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row__mascot {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
grid-gap: 0;
|
||||
grid-template-columns: minmax(0, 100%);
|
||||
|
||||
.column-0 {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.column-1 {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.column-2 {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.column-3 {
|
||||
grid-column: 1;
|
||||
grid-row: 5;
|
||||
}
|
||||
|
||||
.column-4 {
|
||||
grid-column: 1;
|
||||
grid-row: 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.public-layout {
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
padding-top: 48px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 960px;
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
||||
border-radius: 4px;
|
||||
height: 48px;
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
z-index: 110;
|
||||
}
|
||||
|
||||
& > div {
|
||||
flex: 1 1 33.3%;
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.nav-center {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: block;
|
||||
padding: 15px;
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
height: 18px;
|
||||
width: auto;
|
||||
position: relative;
|
||||
bottom: -2px;
|
||||
fill: $primary-text-color;
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: lighten($ui-base-color, 12%);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: $darker-text-color;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
&.optional {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
background: lighten($ui-base-color, 16%);
|
||||
margin: 8px;
|
||||
margin-left: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
background: lighten($ui-base-color, 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$no-columns-breakpoint: 600px;
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: minmax(300px, 3fr) minmax(298px, 1fr);
|
||||
grid-auto-columns: 25%;
|
||||
grid-auto-rows: max-content;
|
||||
|
||||
.column-0 {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.column-1 {
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
grid-template-columns: 100%;
|
||||
grid-gap: 0;
|
||||
|
||||
.column-1 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.public-account-header {
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
||||
|
||||
&.inactive {
|
||||
opacity: 0.5;
|
||||
|
||||
.public-account-header__image,
|
||||
.avatar {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.logo-button {
|
||||
background-color: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-button {
|
||||
padding: 3px 15px;
|
||||
}
|
||||
|
||||
&__image {
|
||||
border-radius: 4px 4px 0 0;
|
||||
overflow: hidden;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
background: darken($ui-base-color, 12%);
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: inset 0 -1px 1px 1px rgba($base-shadow-color, 0.15);
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
&--no-bar {
|
||||
margin-bottom: 0;
|
||||
|
||||
.public-account-header__image,
|
||||
.public-account-header__image img {
|
||||
border-radius: 4px;
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
margin-bottom: 0;
|
||||
box-shadow: none;
|
||||
|
||||
&__image::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__image,
|
||||
&__image img {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__bar {
|
||||
position: relative;
|
||||
margin-top: -80px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
padding-left: 20px - 4px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
border-radius: 50%;
|
||||
border: 4px solid lighten($ui-base-color, 4%);
|
||||
background: darken($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
margin-top: 0;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border-radius: 0 0 4px 4px;
|
||||
padding: 5px;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
padding: 7px 0;
|
||||
padding-left: 10px;
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 360px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
flex: 1 1 auto;
|
||||
margin-left: 20px;
|
||||
|
||||
&__name {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
line-height: 18px * 1.5;
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 1px 1px 1px $base-shadow-color;
|
||||
|
||||
small {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: $primary-text-color;
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
margin-left: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__name {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
h1 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
text-shadow: none;
|
||||
|
||||
small {
|
||||
color: $darker-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
height: 58px;
|
||||
|
||||
.details-counters {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
.details-counters {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.counter {
|
||||
min-width: 33.3%;
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
color: $darker-text-color;
|
||||
padding: 10px;
|
||||
border-right: 1px solid lighten($ui-base-color, 4%);
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-bottom: 4px solid $ui-primary-color;
|
||||
opacity: 0.5;
|
||||
transition: all 400ms ease;
|
||||
}
|
||||
|
||||
&.active {
|
||||
&::after {
|
||||
border-bottom: 4px solid $highlight-text-color;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.inactive::after {
|
||||
border-bottom-color: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
opacity: 1;
|
||||
transition-duration: 100ms;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.counter-label {
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.counter-number {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
margin-bottom: 5px;
|
||||
color: $primary-text-color;
|
||||
font-family: $font-display, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
padding: 7px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__extra {
|
||||
display: none;
|
||||
margin-top: 4px;
|
||||
|
||||
.public-account-bio {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
margin: 0 -5px;
|
||||
|
||||
.account__header__fields {
|
||||
border-top: 1px solid lighten($ui-base-color, 12%);
|
||||
}
|
||||
|
||||
.roles {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__links {
|
||||
margin-top: -15px;
|
||||
font-size: 14px;
|
||||
color: $darker-text-color;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
color: $darker-text-color;
|
||||
text-decoration: none;
|
||||
padding: 15px;
|
||||
font-weight: 500;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
display: block;
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account__section-headline {
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-status__meta {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.public-account-bio {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
box-shadow: none;
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.account__header__fields {
|
||||
margin: 0;
|
||||
border-top: 0;
|
||||
|
||||
a {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
|
||||
dl:first-child .verified {
|
||||
border-radius: 0 4px 0 0;
|
||||
}
|
||||
|
||||
.verified a {
|
||||
color: $valid-value-color;
|
||||
}
|
||||
}
|
||||
|
||||
.account__header__content {
|
||||
padding: 20px;
|
||||
padding-bottom: 0;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
|
||||
&__extra,
|
||||
.roles {
|
||||
padding: 20px;
|
||||
font-size: 14px;
|
||||
color: $darker-text-color;
|
||||
}
|
||||
|
||||
.roles {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.directory__list {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
|
||||
|
||||
.account-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: block;
|
||||
|
||||
.account-card {
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
min-width: 100%;
|
||||
margin: 0 -5px;
|
||||
|
||||
& > div {
|
||||
box-sizing: border-box;
|
||||
flex: 1 0 auto;
|
||||
width: 300px;
|
||||
padding: 0 5px;
|
||||
margin-bottom: 10px;
|
||||
max-width: 33.333%;
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
margin: 0;
|
||||
border-top: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.card__bar {
|
||||
background: $ui-base-color;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,152 +0,0 @@
|
||||
.public-layout {
|
||||
.footer {
|
||||
text-align: left;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 60px;
|
||||
font-size: 12px;
|
||||
color: lighten($ui-base-color, 34%);
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: 1fr 1fr 2fr 1fr 1fr;
|
||||
|
||||
.column-0 {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.column-1 {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.column-2 {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
|
||||
h4 a {
|
||||
color: lighten($ui-base-color, 34%);
|
||||
}
|
||||
}
|
||||
|
||||
.column-3 {
|
||||
grid-column: 4;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.column-4 {
|
||||
grid-column: 5;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 690px) {
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
|
||||
.column-0,
|
||||
.column-1 {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.column-1 {
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.column-2 {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.column-3,
|
||||
.column-4 {
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
.column-4 {
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.column-1 {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
.column-0,
|
||||
.column-1,
|
||||
.column-3,
|
||||
.column-4 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.column-2 h4 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.legal-xs {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding-top: 20px;
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
color: $darker-text-color;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul a,
|
||||
.legal-xs a {
|
||||
text-decoration: none;
|
||||
color: lighten($ui-base-color, 34%);
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.brand {
|
||||
.logo {
|
||||
display: block;
|
||||
height: 36px;
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
color: lighten($ui-base-color, 34%);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
.logo {
|
||||
color: lighten($ui-base-color, 38%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -53,16 +53,6 @@ body.rtl {
|
||||
right: -26px;
|
||||
}
|
||||
|
||||
.landing-page__logo {
|
||||
margin-right: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.landing-page .features-list .features-list__row .visual {
|
||||
margin-left: 0;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.column-link__icon,
|
||||
.column-header__icon {
|
||||
margin-right: 0;
|
||||
@ -350,44 +340,6 @@ body.rtl {
|
||||
margin-left: 45px;
|
||||
}
|
||||
|
||||
.landing-page .header-wrapper .mascot {
|
||||
right: 60px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.landing-page__call-to-action .row__information-board {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.landing-page .header .hero .floats .float-1 {
|
||||
left: -120px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.landing-page .header .hero .floats .float-2 {
|
||||
left: 210px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.landing-page .header .hero .floats .float-3 {
|
||||
left: 110px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.landing-page .header .links .brand img {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.landing-page .fa-external-link {
|
||||
padding-right: 5px;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.landing-page .features #mastodon-timeline {
|
||||
margin-right: 0;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 631px) {
|
||||
.column,
|
||||
.drawer {
|
||||
@ -415,32 +367,6 @@ body.rtl {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.public-layout {
|
||||
.header {
|
||||
.nav-button {
|
||||
margin-left: 8px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.public-account-header__tabs {
|
||||
margin-left: 0;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.landing-page__information {
|
||||
.account__display-name {
|
||||
margin-right: 0;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.account__avatar-wrapper {
|
||||
margin-left: 12px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card__bar .display-name {
|
||||
margin-left: 0;
|
||||
margin-right: 15px;
|
||||
|
@ -137,8 +137,7 @@ a.button.logo-button {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.embed,
|
||||
.public-layout {
|
||||
.embed {
|
||||
.status__content[data-spoiler="folded"] {
|
||||
.e-content {
|
||||
display: none;
|
||||
|
@ -8,16 +8,14 @@ class PermalinkRedirector
|
||||
end
|
||||
|
||||
def redirect_path
|
||||
if path_segments[0] == 'web'
|
||||
if path_segments[1].present? && path_segments[1].start_with?('@') && path_segments[2] =~ /\d/
|
||||
find_status_url_by_id(path_segments[2])
|
||||
elsif path_segments[1].present? && path_segments[1].start_with?('@')
|
||||
find_account_url_by_name(path_segments[1])
|
||||
elsif path_segments[1] == 'statuses' && path_segments[2] =~ /\d/
|
||||
find_status_url_by_id(path_segments[2])
|
||||
elsif path_segments[1] == 'accounts' && path_segments[2] =~ /\d/
|
||||
find_account_url_by_id(path_segments[2])
|
||||
end
|
||||
if path_segments[0].present? && path_segments[0].start_with?('@') && path_segments[1] =~ /\d/
|
||||
find_status_url_by_id(path_segments[1])
|
||||
elsif path_segments[0].present? && path_segments[0].start_with?('@')
|
||||
find_account_url_by_name(path_segments[0])
|
||||
elsif path_segments[0] == 'statuses' && path_segments[1] =~ /\d/
|
||||
find_status_url_by_id(path_segments[1])
|
||||
elsif path_segments[0] == 'accounts' && path_segments[1] =~ /\d/
|
||||
find_account_url_by_id(path_segments[1])
|
||||
end
|
||||
end
|
||||
|
||||
@ -29,18 +27,12 @@ class PermalinkRedirector
|
||||
|
||||
def find_status_url_by_id(id)
|
||||
status = Status.find_by(id: id)
|
||||
|
||||
return unless status&.distributable?
|
||||
|
||||
ActivityPub::TagManager.instance.url_for(status)
|
||||
ActivityPub::TagManager.instance.url_for(status) if status&.distributable? && !status.account.local?
|
||||
end
|
||||
|
||||
def find_account_url_by_id(id)
|
||||
account = Account.find_by(id: id)
|
||||
|
||||
return unless account
|
||||
|
||||
ActivityPub::TagManager.instance.url_for(account)
|
||||
ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local?
|
||||
end
|
||||
|
||||
def find_account_url_by_name(name)
|
||||
@ -48,12 +40,6 @@ class PermalinkRedirector
|
||||
domain = nil if TagManager.instance.local_domain?(domain)
|
||||
account = Account.find_remote(username, domain)
|
||||
|
||||
return unless account
|
||||
|
||||
ActivityPub::TagManager.instance.url_for(account)
|
||||
end
|
||||
|
||||
def find_tag_url_by_name(name)
|
||||
tag_path(CGI.unescape(name))
|
||||
ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local?
|
||||
end
|
||||
end
|
||||
|
@ -134,6 +134,7 @@ class Account < ApplicationRecord
|
||||
:role,
|
||||
:locale,
|
||||
:shows_application?,
|
||||
:prefers_noindex?,
|
||||
to: :user,
|
||||
prefix: true,
|
||||
allow_nil: true
|
||||
|
@ -281,6 +281,10 @@ class User < ApplicationRecord
|
||||
save!
|
||||
end
|
||||
|
||||
def prefers_noindex?
|
||||
setting_noindex
|
||||
end
|
||||
|
||||
def preferred_posting_language
|
||||
valid_locale_cascade(settings.default_language, locale, I18n.locale)
|
||||
end
|
||||
|
@ -14,6 +14,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
||||
|
||||
attribute :suspended, if: :suspended?
|
||||
attribute :silenced, key: :limited, if: :silenced?
|
||||
attribute :noindex, if: :local?
|
||||
|
||||
class FieldSerializer < ActiveModel::Serializer
|
||||
include FormattingHelper
|
||||
@ -103,7 +104,11 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
||||
object.silenced?
|
||||
end
|
||||
|
||||
delegate :suspended?, :silenced?, to: :object
|
||||
def noindex
|
||||
object.user_prefers_noindex?
|
||||
end
|
||||
|
||||
delegate :suspended?, :silenced?, :local?, to: :object
|
||||
|
||||
def moved_and_not_nested?
|
||||
object.moved? && object.moved_to_account.moved_to_account_id.nil?
|
||||
|
@ -1,4 +1,7 @@
|
||||
- content_for :page_title do
|
||||
= t('about.title')
|
||||
|
||||
- content_for :header_tags do
|
||||
= render partial: 'shared/og'
|
||||
|
||||
= render partial: 'shared/web_app'
|
||||
|
@ -1,21 +0,0 @@
|
||||
- fields = account.fields
|
||||
|
||||
.public-account-bio
|
||||
- unless fields.empty?
|
||||
.account__header__fields
|
||||
- fields.each do |field|
|
||||
%dl
|
||||
%dt.emojify{ title: field.name }= prerender_custom_emojis(h(field.name), account.emojis)
|
||||
%dd{ title: field.value, class: custom_field_classes(field) }
|
||||
- if field.verified?
|
||||
%span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) }
|
||||
= fa_icon 'check'
|
||||
= prerender_custom_emojis(account_field_value_format(field), account.emojis)
|
||||
|
||||
= account_badge(account)
|
||||
|
||||
- if account.note.present?
|
||||
.account__header__content.emojify= prerender_custom_emojis(account_bio_format(account), account.emojis)
|
||||
|
||||
.public-account-bio__extra
|
||||
= t 'accounts.joined', date: l(account.created_at, format: :month)
|
@ -1,43 +0,0 @@
|
||||
.public-account-header{:class => ("inactive" if account.moved?)}
|
||||
.public-account-header__image
|
||||
= image_tag (prefers_autoplay? ? account.header_original_url : account.header_static_url), class: 'parallax'
|
||||
.public-account-header__bar
|
||||
= link_to short_account_url(account), class: 'avatar' do
|
||||
= image_tag (prefers_autoplay? ? account.avatar_original_url : account.avatar_static_url), id: 'profile_page_avatar', data: { original: full_asset_url(account.avatar_original_url), static: full_asset_url(account.avatar_static_url), autoplay: prefers_autoplay? }
|
||||
.public-account-header__tabs
|
||||
.public-account-header__tabs__name
|
||||
%h1
|
||||
= display_name(account, custom_emojify: true)
|
||||
%small
|
||||
= acct(account)
|
||||
= fa_icon('lock') if account.locked?
|
||||
.public-account-header__tabs__tabs
|
||||
.details-counters
|
||||
.counter{ class: active_nav_class(short_account_url(account), short_account_with_replies_url(account), short_account_media_url(account)) }
|
||||
= link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do
|
||||
%span.counter-number= friendly_number_to_human account.statuses_count
|
||||
%span.counter-label= t('accounts.posts', count: account.statuses_count)
|
||||
|
||||
.counter{ class: active_nav_class(account_following_index_url(account)) }
|
||||
= link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do
|
||||
%span.counter-number= friendly_number_to_human account.following_count
|
||||
%span.counter-label= t('accounts.following', count: account.following_count)
|
||||
|
||||
.counter{ class: active_nav_class(account_followers_url(account)) }
|
||||
= link_to account_followers_url(account), title: number_with_delimiter(account.followers_count) do
|
||||
%span.counter-number= friendly_number_to_human account.followers_count
|
||||
%span.counter-label= t('accounts.followers', count: account.followers_count)
|
||||
.spacer
|
||||
.public-account-header__tabs__tabs__buttons
|
||||
= account_action_button(account)
|
||||
|
||||
.public-account-header__extra
|
||||
= render 'accounts/bio', account: account
|
||||
|
||||
.public-account-header__extra__links
|
||||
= link_to account_following_index_url(account) do
|
||||
%strong= friendly_number_to_human account.following_count
|
||||
= t('accounts.following', count: account.following_count)
|
||||
= link_to account_followers_url(account) do
|
||||
%strong= friendly_number_to_human account.followers_count
|
||||
= t('accounts.followers', count: account.followers_count)
|
@ -1,20 +0,0 @@
|
||||
- moved_to_account = account.moved_to_account
|
||||
|
||||
.moved-account-widget
|
||||
.moved-account-widget__message
|
||||
= fa_icon 'suitcase'
|
||||
= t('accounts.moved_html', name: content_tag(:bdi, content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify)), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.pretty_acct)])), ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'mention'))
|
||||
|
||||
.moved-account-widget__card
|
||||
= link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener noreferrer' do
|
||||
.detailed-status__display-avatar
|
||||
.account__avatar-overlay
|
||||
.account__avatar-overlay-base
|
||||
= image_tag moved_to_account.avatar_static_url
|
||||
.account__avatar-overlay-overlay
|
||||
= image_tag account.avatar_static_url
|
||||
|
||||
%span.display-name
|
||||
%bdi
|
||||
%strong.emojify= display_name(moved_to_account, custom_emojify: true)
|
||||
%span @#{moved_to_account.pretty_acct}
|
@ -2,85 +2,13 @@
|
||||
= "#{display_name(@account)} (#{acct(@account)})"
|
||||
|
||||
- content_for :header_tags do
|
||||
- if @account.user&.setting_noindex
|
||||
- if @account.user_prefers_noindex?
|
||||
%meta{ name: 'robots', content: 'noindex, noarchive' }/
|
||||
|
||||
%link{ rel: 'alternate', type: 'application/rss+xml', href: @rss_url }/
|
||||
%link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
|
||||
|
||||
- if @older_url
|
||||
%link{ rel: 'next', href: @older_url }/
|
||||
- if @newer_url
|
||||
%link{ rel: 'prev', href: @newer_url }/
|
||||
|
||||
= opengraph 'og:type', 'profile'
|
||||
= render 'og', account: @account, url: short_account_url(@account, only_path: false)
|
||||
|
||||
|
||||
= render 'header', account: @account, with_bio: true
|
||||
|
||||
.grid
|
||||
.column-0
|
||||
.h-feed
|
||||
%data.p-name{ value: "#{@account.username} on #{site_hostname}" }/
|
||||
|
||||
.account__section-headline
|
||||
= active_link_to t('accounts.posts_tab_heading'), short_account_url(@account)
|
||||
= active_link_to t('accounts.posts_with_replies'), short_account_with_replies_url(@account)
|
||||
= active_link_to t('accounts.media'), short_account_media_url(@account)
|
||||
|
||||
- if user_signed_in? && @account.blocking?(current_account)
|
||||
.nothing-here.nothing-here--under-tabs= t('accounts.unavailable')
|
||||
- elsif @statuses.empty?
|
||||
= nothing_here 'nothing-here--under-tabs'
|
||||
- else
|
||||
.activity-stream.activity-stream--under-tabs
|
||||
- if params[:page].to_i.zero?
|
||||
= render partial: 'statuses/status', collection: @pinned_statuses, as: :status, locals: { pinned: true }
|
||||
|
||||
- if @newer_url
|
||||
.entry= link_to_newer @newer_url
|
||||
|
||||
= render partial: 'statuses/status', collection: @statuses, as: :status
|
||||
|
||||
- if @older_url
|
||||
.entry= link_to_older @older_url
|
||||
|
||||
.column-1
|
||||
- if @account.memorial?
|
||||
.memoriam-widget= t('in_memoriam_html')
|
||||
- elsif @account.moved?
|
||||
= render 'moved', account: @account
|
||||
|
||||
= render 'bio', account: @account
|
||||
|
||||
- if @endorsed_accounts.empty? && @account.id == current_account&.id
|
||||
.placeholder-widget= t('accounts.endorsements_hint')
|
||||
- elsif !@endorsed_accounts.empty?
|
||||
.endorsements-widget
|
||||
%h4= t 'accounts.choices_html', name: content_tag(:bdi, display_name(@account, custom_emojify: true))
|
||||
|
||||
- @endorsed_accounts.each do |account|
|
||||
= account_link_to account
|
||||
|
||||
- if @featured_hashtags.empty? && @account.id == current_account&.id
|
||||
.placeholder-widget
|
||||
= t('accounts.featured_tags_hint')
|
||||
= link_to settings_featured_tags_path do
|
||||
= t('featured_tags.add_new')
|
||||
= fa_icon 'chevron-right fw'
|
||||
- else
|
||||
- @featured_hashtags.each do |featured_tag|
|
||||
.directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil }
|
||||
= link_to short_account_tag_path(@account, featured_tag.tag) do
|
||||
%h4
|
||||
= fa_icon 'hashtag'
|
||||
= featured_tag.display_name
|
||||
%small
|
||||
- if featured_tag.last_status_at.nil?
|
||||
= t('accounts.nothing_here')
|
||||
- else
|
||||
%time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
|
||||
.trends__item__current= friendly_number_to_human featured_tag.statuses_count
|
||||
|
||||
= render 'application/sidebar'
|
||||
= render partial: 'shared/web_app'
|
||||
|
@ -1,20 +1,6 @@
|
||||
- content_for :page_title do
|
||||
= t('accounts.people_who_follow', name: display_name(@account))
|
||||
|
||||
- content_for :header_tags do
|
||||
%meta{ name: 'robots', content: 'noindex' }/
|
||||
|
||||
= render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
|
||||
|
||||
= render 'accounts/header', account: @account
|
||||
|
||||
- if @account.hide_collections?
|
||||
.nothing-here= t('accounts.network_hidden')
|
||||
- elsif user_signed_in? && @account.blocking?(current_account)
|
||||
.nothing-here= t('accounts.unavailable')
|
||||
- elsif @follows.empty?
|
||||
= nothing_here
|
||||
- else
|
||||
.card-grid
|
||||
= render partial: 'application/card', collection: @follows.map(&:account), as: :account
|
||||
|
||||
= paginate @follows
|
||||
= render 'shared/web_app'
|
||||
|
@ -1,20 +1,6 @@
|
||||
- content_for :page_title do
|
||||
= t('accounts.people_followed_by', name: display_name(@account))
|
||||
|
||||
- content_for :header_tags do
|
||||
%meta{ name: 'robots', content: 'noindex' }/
|
||||
|
||||
= render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
|
||||
|
||||
= render 'accounts/header', account: @account
|
||||
|
||||
- if @account.hide_collections?
|
||||
.nothing-here= t('accounts.network_hidden')
|
||||
- elsif user_signed_in? && @account.blocking?(current_account)
|
||||
.nothing-here= t('accounts.unavailable')
|
||||
- elsif @follows.empty?
|
||||
= nothing_here
|
||||
- else
|
||||
.card-grid
|
||||
= render partial: 'application/card', collection: @follows.map(&:target_account), as: :account
|
||||
|
||||
= paginate @follows
|
||||
= render 'shared/web_app'
|
||||
|
@ -1,4 +1,7 @@
|
||||
- content_for :header_tags do
|
||||
- unless request.path == '/'
|
||||
%meta{ name: 'robots', content: 'noindex' }/
|
||||
|
||||
= render partial: 'shared/og'
|
||||
|
||||
= render 'shared/web_app'
|
||||
|
@ -1,60 +0,0 @@
|
||||
- content_for :header_tags do
|
||||
= render_initial_state
|
||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
||||
|
||||
- content_for :content do
|
||||
.public-layout
|
||||
- unless @hide_navbar
|
||||
.container
|
||||
%nav.header
|
||||
.nav-left
|
||||
= link_to root_url, class: 'brand' do
|
||||
= logo_as_symbol(:wordmark)
|
||||
|
||||
- unless whitelist_mode?
|
||||
= link_to t('about.about_this'), about_more_path, class: 'nav-link optional'
|
||||
= link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional'
|
||||
|
||||
.nav-center
|
||||
|
||||
.nav-right
|
||||
- if user_signed_in?
|
||||
= link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'
|
||||
- else
|
||||
= link_to_login t('auth.login'), class: 'webapp-btn nav-link nav-button'
|
||||
= link_to t('auth.register'), available_sign_up_path, class: 'webapp-btn nav-link nav-button'
|
||||
|
||||
.container= yield
|
||||
|
||||
.container
|
||||
.footer
|
||||
.grid
|
||||
.column-0
|
||||
%h4= t 'footer.resources'
|
||||
%ul
|
||||
%li= link_to t('about.privacy_policy'), privacy_policy_path
|
||||
.column-1
|
||||
%h4= t 'footer.developers'
|
||||
%ul
|
||||
%li= link_to t('about.documentation'), 'https://docs.joinmastodon.org/'
|
||||
%li= link_to t('about.api'), 'https://docs.joinmastodon.org/client/intro/'
|
||||
.column-2
|
||||
%h4= link_to t('about.what_is_mastodon'), 'https://joinmastodon.org/'
|
||||
= link_to logo_as_symbol, root_url, class: 'brand'
|
||||
.column-3
|
||||
%h4= site_hostname
|
||||
%ul
|
||||
- unless whitelist_mode?
|
||||
%li= link_to t('about.about_this'), about_more_path
|
||||
%li= "v#{Mastodon::Version.to_s}"
|
||||
.column-4
|
||||
%h4= t 'footer.more'
|
||||
%ul
|
||||
%li= link_to t('about.source_code'), Mastodon::Version.source_url
|
||||
%li= link_to t('about.apps'), 'https://joinmastodon.org/apps'
|
||||
.legal-xs
|
||||
= link_to "v#{Mastodon::Version.to_s}", Mastodon::Version.source_url
|
||||
·
|
||||
= link_to t('about.privacy_policy'), privacy_policy_path
|
||||
|
||||
= render template: 'layouts/application'
|
@ -1,4 +1,7 @@
|
||||
- content_for :page_title do
|
||||
= t('privacy_policy.title')
|
||||
|
||||
- content_for :header_tags do
|
||||
= render partial: 'shared/og'
|
||||
|
||||
= render 'shared/web_app'
|
||||
|
@ -1,20 +0,0 @@
|
||||
- content_for :header_tags do
|
||||
%meta{ name: 'robots', content: 'noindex' }/
|
||||
|
||||
.form-container
|
||||
.follow-prompt
|
||||
%h2= t('remote_follow.prompt')
|
||||
|
||||
= render partial: 'application/card', locals: { account: @account }
|
||||
|
||||
= simple_form_for @remote_follow, as: :remote_follow, url: account_remote_follow_path(@account) do |f|
|
||||
= render 'shared/error_messages', object: @remote_follow
|
||||
|
||||
= f.input :acct, placeholder: t('remote_follow.acct'), input_html: { autocapitalize: 'none', autocorrect: 'off' }
|
||||
|
||||
.actions
|
||||
= f.button :button, t('remote_follow.proceed'), type: :submit
|
||||
|
||||
%p.hint.subtle-hint
|
||||
= t('remote_follow.reason_html', instance: site_hostname)
|
||||
= t('remote_follow.no_account_html', sign_up_path: available_sign_up_path)
|
@ -1,24 +0,0 @@
|
||||
- content_for :header_tags do
|
||||
%meta{ name: 'robots', content: 'noindex' }/
|
||||
|
||||
.form-container
|
||||
.follow-prompt
|
||||
%h2= t("remote_interaction.#{@interaction_type}.prompt")
|
||||
|
||||
.public-layout
|
||||
.activity-stream.activity-stream--highlighted
|
||||
= render 'statuses/status', status: @status
|
||||
|
||||
= simple_form_for @remote_follow, as: :remote_follow, url: remote_interaction_path(@status) do |f|
|
||||
= render 'shared/error_messages', object: @remote_follow
|
||||
|
||||
= hidden_field_tag :type, @interaction_type
|
||||
|
||||
= f.input :acct, placeholder: t('remote_follow.acct'), input_html: { autocapitalize: 'none', autocorrect: 'off' }
|
||||
|
||||
.actions
|
||||
= f.button :button, t("remote_interaction.#{@interaction_type}.proceed"), type: :submit
|
||||
|
||||
%p.hint.subtle-hint
|
||||
= t('remote_follow.reason_html', instance: site_hostname)
|
||||
= t('remote_follow.no_account_html', sign_up_path: available_sign_up_path)
|
@ -56,7 +56,7 @@
|
||||
- else
|
||||
= link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener noreferrer'
|
||||
·
|
||||
= link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do
|
||||
%span.detailed-status__link
|
||||
- if status.in_reply_to_id.nil?
|
||||
= fa_icon('reply')
|
||||
- else
|
||||
@ -65,12 +65,12 @@
|
||||
= " "
|
||||
·
|
||||
- if status.public_visibility? || status.unlisted_visibility?
|
||||
= link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do
|
||||
%span.detailed-status__link
|
||||
= fa_icon('retweet')
|
||||
%span.detailed-status__reblogs>= friendly_number_to_human status.reblogs_count
|
||||
= " "
|
||||
·
|
||||
= link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do
|
||||
%span.detailed-status__link
|
||||
= fa_icon('star')
|
||||
%span.detailed-status__favorites>= friendly_number_to_human status.favourites_count
|
||||
= " "
|
||||
|
@ -53,18 +53,18 @@
|
||||
= t 'statuses.show_thread'
|
||||
|
||||
.status__action-bar
|
||||
= link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button icon-button--with-counter modal-button' do
|
||||
%span.status__action-bar-button.icon-button.icon-button--with-counter
|
||||
- if status.in_reply_to_id.nil?
|
||||
= fa_icon 'reply fw'
|
||||
- else
|
||||
= fa_icon 'reply-all fw'
|
||||
%span.icon-button__counter= obscured_counter status.replies_count
|
||||
= link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button' do
|
||||
%span.status__action-bar-button.icon-button
|
||||
- if status.distributable?
|
||||
= fa_icon 'retweet fw'
|
||||
- elsif status.private_visibility? || status.limited_visibility?
|
||||
= fa_icon 'lock fw'
|
||||
- else
|
||||
= fa_icon 'at fw'
|
||||
= link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button' do
|
||||
%span.status__action-bar-button.icon-button
|
||||
= fa_icon 'star fw'
|
||||
|
@ -2,7 +2,7 @@
|
||||
= t('statuses.title', name: display_name(@account), quote: truncate(@status.spoiler_text.presence || @status.text, length: 50, omission: '…', escape: false))
|
||||
|
||||
- content_for :header_tags do
|
||||
- if @account.user&.setting_noindex
|
||||
- if @account.user_prefers_noindex?
|
||||
%meta{ name: 'robots', content: 'noindex, noarchive' }/
|
||||
|
||||
%link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: short_account_status_url(@account, @status), format: 'json') }/
|
||||
|
5
app/views/tags/show.html.haml
Normal file
5
app/views/tags/show.html.haml
Normal file
@ -0,0 +1,5 @@
|
||||
- content_for :header_tags do
|
||||
%meta{ name: 'robots', content: 'noindex' }/
|
||||
= render partial: 'shared/og'
|
||||
|
||||
= render partial: 'shared/web_app'
|
@ -2,47 +2,26 @@
|
||||
en:
|
||||
about:
|
||||
about_mastodon_html: 'The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with Mastodon!'
|
||||
api: API
|
||||
apps: Mobile apps
|
||||
contact_missing: Not set
|
||||
contact_unavailable: N/A
|
||||
documentation: Documentation
|
||||
hosted_on: Mastodon hosted on %{domain}
|
||||
privacy_policy: Privacy Policy
|
||||
source_code: Source code
|
||||
title: About
|
||||
what_is_mastodon: What is Mastodon?
|
||||
accounts:
|
||||
choices_html: "%{name}'s choices:"
|
||||
endorsements_hint: You can endorse people you follow from the web interface, and they will show up here.
|
||||
featured_tags_hint: You can feature specific hashtags that will be displayed here.
|
||||
follow: Follow
|
||||
followers:
|
||||
one: Follower
|
||||
other: Followers
|
||||
following: Following
|
||||
instance_actor_flash: This account is a virtual actor used to represent the server itself and not any individual user. It is used for federation purposes and should not be suspended.
|
||||
joined: Joined %{date}
|
||||
last_active: last active
|
||||
link_verified_on: Ownership of this link was checked on %{date}
|
||||
media: Media
|
||||
moved_html: "%{name} has moved to %{new_profile_link}:"
|
||||
network_hidden: This information is not available
|
||||
nothing_here: There is nothing here!
|
||||
people_followed_by: People whom %{name} follows
|
||||
people_who_follow: People who follow %{name}
|
||||
pin_errors:
|
||||
following: You must be already following the person you want to endorse
|
||||
posts:
|
||||
one: Post
|
||||
other: Posts
|
||||
posts_tab_heading: Posts
|
||||
posts_with_replies: Posts and replies
|
||||
roles:
|
||||
bot: Bot
|
||||
group: Group
|
||||
unavailable: Profile unavailable
|
||||
unfollow: Unfollow
|
||||
admin:
|
||||
account_actions:
|
||||
action: Perform action
|
||||
@ -1176,9 +1155,6 @@ en:
|
||||
hint: This filter applies to select individual posts regardless of other criteria. You can add more posts to this filter from the web interface.
|
||||
title: Filtered posts
|
||||
footer:
|
||||
developers: Developers
|
||||
more: More…
|
||||
resources: Resources
|
||||
trending_now: Trending now
|
||||
generic:
|
||||
all: All
|
||||
@ -1221,7 +1197,6 @@ en:
|
||||
following: Following list
|
||||
muting: Muting list
|
||||
upload: Upload
|
||||
in_memoriam_html: In Memoriam.
|
||||
invites:
|
||||
delete: Deactivate
|
||||
expired: Expired
|
||||
@ -1402,22 +1377,7 @@ en:
|
||||
remove_selected_follows: Unfollow selected users
|
||||
status: Account status
|
||||
remote_follow:
|
||||
acct: Enter your username@domain you want to act from
|
||||
missing_resource: Could not find the required redirect URL for your account
|
||||
no_account_html: Don't have an account? You can <a href='%{sign_up_path}' target='_blank'>sign up here</a>
|
||||
proceed: Proceed to follow
|
||||
prompt: 'You are going to follow:'
|
||||
reason_html: "<strong>Why is this step necessary?</strong> <code>%{instance}</code> might not be the server where you are registered, so we need to redirect you to your home server first."
|
||||
remote_interaction:
|
||||
favourite:
|
||||
proceed: Proceed to favourite
|
||||
prompt: 'You want to favourite this post:'
|
||||
reblog:
|
||||
proceed: Proceed to boost
|
||||
prompt: 'You want to boost this post:'
|
||||
reply:
|
||||
proceed: Proceed to reply
|
||||
prompt: 'You want to reply to this post:'
|
||||
reports:
|
||||
errors:
|
||||
invalid_rules: does not reference valid rules
|
||||
|
@ -3,6 +3,31 @@
|
||||
require 'sidekiq_unique_jobs/web'
|
||||
require 'sidekiq-scheduler/web'
|
||||
|
||||
# Paths of routes on the web app that to not require to be indexed or
|
||||
# have alternative format representations requiring separate controllers
|
||||
WEB_APP_PATHS = %w(
|
||||
/getting-started
|
||||
/keyboard-shortcuts
|
||||
/home
|
||||
/public
|
||||
/public/local
|
||||
/conversations
|
||||
/lists/(*any)
|
||||
/notifications
|
||||
/favourites
|
||||
/bookmarks
|
||||
/pinned
|
||||
/start
|
||||
/directory
|
||||
/explore/(*any)
|
||||
/search
|
||||
/publish
|
||||
/follow_requests
|
||||
/blocks
|
||||
/domain_blocks
|
||||
/mutes
|
||||
).freeze
|
||||
|
||||
Rails.application.routes.draw do
|
||||
root 'home#index'
|
||||
|
||||
@ -59,9 +84,6 @@ Rails.application.routes.draw do
|
||||
get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" }
|
||||
|
||||
resources :accounts, path: 'users', only: [:show], param: :username do
|
||||
get :remote_follow, to: 'remote_follow#new'
|
||||
post :remote_follow, to: 'remote_follow#create'
|
||||
|
||||
resources :statuses, only: [:show] do
|
||||
member do
|
||||
get :activity
|
||||
@ -85,16 +107,21 @@ Rails.application.routes.draw do
|
||||
|
||||
resource :inbox, only: [:create], module: :activitypub
|
||||
|
||||
get '/@:username', to: 'accounts#show', as: :short_account
|
||||
get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
|
||||
get '/@:username/media', to: 'accounts#show', as: :short_account_media
|
||||
get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag
|
||||
get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status
|
||||
get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
|
||||
constraints(username: /[^@\/.]+/) do
|
||||
get '/@:username', to: 'accounts#show', as: :short_account
|
||||
get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
|
||||
get '/@:username/media', to: 'accounts#show', as: :short_account_media
|
||||
get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag
|
||||
end
|
||||
|
||||
get '/interact/:id', to: 'remote_interaction#new', as: :remote_interaction
|
||||
post '/interact/:id', to: 'remote_interaction#create'
|
||||
constraints(account_username: /[^@\/.]+/) do
|
||||
get '/@:account_username/following', to: 'following_accounts#index'
|
||||
get '/@:account_username/followers', to: 'follower_accounts#index'
|
||||
get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status
|
||||
get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
|
||||
end
|
||||
|
||||
get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: /([^\/])+?/ }, format: false
|
||||
get '/settings', to: redirect('/settings/profile')
|
||||
|
||||
namespace :settings do
|
||||
@ -187,9 +214,6 @@ Rails.application.routes.draw do
|
||||
resource :relationships, only: [:show, :update]
|
||||
resource :statuses_cleanup, controller: :statuses_cleanup, only: [:show, :update]
|
||||
|
||||
get '/explore', to: redirect('/web/explore')
|
||||
get '/public', to: redirect('/web/public')
|
||||
get '/public/local', to: redirect('/web/public/local')
|
||||
get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy
|
||||
|
||||
resource :authorize_interaction, only: [:show, :create]
|
||||
@ -642,8 +666,11 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
get '/web/(*any)', to: 'home#index', as: :web
|
||||
WEB_APP_PATHS.each do |path|
|
||||
get path, to: 'home#index'
|
||||
end
|
||||
|
||||
get '/web/(*any)', to: redirect('/%{any}', status: 302), as: :web
|
||||
get '/about', to: 'about#show'
|
||||
get '/about/more', to: redirect('/about')
|
||||
|
||||
|
@ -115,7 +115,6 @@
|
||||
"redux-immutable": "^4.0.0",
|
||||
"redux-thunk": "^2.4.1",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"rellax": "^1.12.1",
|
||||
"requestidlecallback": "^0.3.0",
|
||||
"reselect": "^4.1.6",
|
||||
"rimraf": "^3.0.2",
|
||||
|
@ -1,64 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe AccountFollowController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
describe 'POST #create' do
|
||||
let(:service) { double }
|
||||
|
||||
subject { post :create, params: { account_username: alice.username } }
|
||||
|
||||
before do
|
||||
allow(FollowService).to receive(:new).and_return(service)
|
||||
allow(service).to receive(:call)
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
alice.deletion_request.destroy
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed out' do
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it 'does not follow' do
|
||||
expect(FollowService).not_to receive(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
subject
|
||||
end
|
||||
|
||||
it 'redirects to account path' do
|
||||
expect(service).to have_received(:call).with(user.account, alice, with_rate_limit: true)
|
||||
expect(response).to redirect_to(account_path(alice))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,64 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe AccountUnfollowController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
describe 'POST #create' do
|
||||
let(:service) { double }
|
||||
|
||||
subject { post :create, params: { account_username: alice.username } }
|
||||
|
||||
before do
|
||||
allow(UnfollowService).to receive(:new).and_return(service)
|
||||
allow(service).to receive(:call)
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
alice.deletion_request.destroy
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed out' do
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it 'does not unfollow' do
|
||||
expect(UnfollowService).not_to receive(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
subject
|
||||
end
|
||||
|
||||
it 'redirects to account path' do
|
||||
expect(service).to have_received(:call).with(user.account, alice)
|
||||
expect(response).to redirect_to(account_path(alice))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -99,100 +99,6 @@ RSpec.describe AccountsController, type: :controller do
|
||||
end
|
||||
|
||||
it_behaves_like 'common response characteristics'
|
||||
|
||||
it 'renders public status' do
|
||||
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status))
|
||||
end
|
||||
|
||||
it 'renders self-reply' do
|
||||
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply))
|
||||
end
|
||||
|
||||
it 'renders status with media' do
|
||||
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
|
||||
end
|
||||
|
||||
it 'renders reblog' do
|
||||
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
|
||||
end
|
||||
|
||||
it 'renders pinned status' do
|
||||
expect(response.body).to include(I18n.t('stream_entries.pinned'))
|
||||
end
|
||||
|
||||
it 'does not render private status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
|
||||
end
|
||||
|
||||
it 'does not render direct status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
|
||||
end
|
||||
|
||||
it 'does not render reply to someone else' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed-in' do
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'when user follows account' do
|
||||
before do
|
||||
user.account.follow!(account)
|
||||
get :show, params: { username: account.username, format: format }
|
||||
end
|
||||
|
||||
it 'does not render private status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is blocked' do
|
||||
before do
|
||||
account.block!(user.account)
|
||||
get :show, params: { username: account.username, format: format }
|
||||
end
|
||||
|
||||
it 'renders unavailable message' do
|
||||
expect(response.body).to include(I18n.t('accounts.unavailable'))
|
||||
end
|
||||
|
||||
it 'does not render public status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
|
||||
end
|
||||
|
||||
it 'does not render self-reply' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
|
||||
end
|
||||
|
||||
it 'does not render status with media' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media))
|
||||
end
|
||||
|
||||
it 'does not render reblog' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
|
||||
end
|
||||
|
||||
it 'does not render pinned status' do
|
||||
expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
|
||||
end
|
||||
|
||||
it 'does not render private status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
|
||||
end
|
||||
|
||||
it 'does not render direct status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
|
||||
end
|
||||
|
||||
it 'does not render reply to someone else' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with replies' do
|
||||
@ -202,38 +108,6 @@ RSpec.describe AccountsController, type: :controller do
|
||||
end
|
||||
|
||||
it_behaves_like 'common response characteristics'
|
||||
|
||||
it 'renders public status' do
|
||||
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status))
|
||||
end
|
||||
|
||||
it 'renders self-reply' do
|
||||
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply))
|
||||
end
|
||||
|
||||
it 'renders status with media' do
|
||||
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
|
||||
end
|
||||
|
||||
it 'renders reblog' do
|
||||
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
|
||||
end
|
||||
|
||||
it 'does not render pinned status' do
|
||||
expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
|
||||
end
|
||||
|
||||
it 'does not render private status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
|
||||
end
|
||||
|
||||
it 'does not render direct status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
|
||||
end
|
||||
|
||||
it 'renders reply to someone else' do
|
||||
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reply))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with media' do
|
||||
@ -243,38 +117,6 @@ RSpec.describe AccountsController, type: :controller do
|
||||
end
|
||||
|
||||
it_behaves_like 'common response characteristics'
|
||||
|
||||
it 'does not render public status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
|
||||
end
|
||||
|
||||
it 'does not render self-reply' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
|
||||
end
|
||||
|
||||
it 'renders status with media' do
|
||||
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
|
||||
end
|
||||
|
||||
it 'does not render reblog' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
|
||||
end
|
||||
|
||||
it 'does not render pinned status' do
|
||||
expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
|
||||
end
|
||||
|
||||
it 'does not render private status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
|
||||
end
|
||||
|
||||
it 'does not render direct status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
|
||||
end
|
||||
|
||||
it 'does not render reply to someone else' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with tag' do
|
||||
@ -289,42 +131,6 @@ RSpec.describe AccountsController, type: :controller do
|
||||
end
|
||||
|
||||
it_behaves_like 'common response characteristics'
|
||||
|
||||
it 'does not render public status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
|
||||
end
|
||||
|
||||
it 'does not render self-reply' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
|
||||
end
|
||||
|
||||
it 'does not render status with media' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media))
|
||||
end
|
||||
|
||||
it 'does not render reblog' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
|
||||
end
|
||||
|
||||
it 'does not render pinned status' do
|
||||
expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
|
||||
end
|
||||
|
||||
it 'does not render private status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
|
||||
end
|
||||
|
||||
it 'does not render direct status' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
|
||||
end
|
||||
|
||||
it 'does not render reply to someone else' do
|
||||
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
|
||||
end
|
||||
|
||||
it 'renders status with tag' do
|
||||
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_tag))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -39,7 +39,7 @@ describe AuthorizeInteractionsController do
|
||||
end
|
||||
|
||||
it 'sets resource from url' do
|
||||
account = Account.new
|
||||
account = Fabricate(:account)
|
||||
service = double
|
||||
allow(ResolveURLService).to receive(:new).and_return(service)
|
||||
allow(service).to receive(:call).with('http://example.com').and_return(account)
|
||||
@ -51,7 +51,7 @@ describe AuthorizeInteractionsController do
|
||||
end
|
||||
|
||||
it 'sets resource from acct uri' do
|
||||
account = Account.new
|
||||
account = Fabricate(:account)
|
||||
service = double
|
||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||
allow(service).to receive(:call).with('found@hostname').and_return(account)
|
||||
|
@ -34,27 +34,6 @@ describe FollowerAccountsController do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
it 'assigns follows' do
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
assigned = assigns(:follows).to_a
|
||||
expect(assigned.size).to eq 2
|
||||
expect(assigned[0]).to eq follow1
|
||||
expect(assigned[1]).to eq follow0
|
||||
end
|
||||
|
||||
it 'does not assign blocked users' do
|
||||
user = Fabricate(:user)
|
||||
user.account.block!(follower0)
|
||||
sign_in(user)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
assigned = assigns(:follows).to_a
|
||||
expect(assigned.size).to eq 1
|
||||
expect(assigned[0]).to eq follow1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when format is json' do
|
||||
|
@ -34,27 +34,6 @@ describe FollowingAccountsController do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
it 'assigns follows' do
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
assigned = assigns(:follows).to_a
|
||||
expect(assigned.size).to eq 2
|
||||
expect(assigned[0]).to eq follow1
|
||||
expect(assigned[1]).to eq follow0
|
||||
end
|
||||
|
||||
it 'does not assign blocked users' do
|
||||
user = Fabricate(:user)
|
||||
user.account.block!(followee0)
|
||||
sign_in(user)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
assigned = assigns(:follows).to_a
|
||||
expect(assigned.size).to eq 1
|
||||
expect(assigned[0]).to eq follow1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when format is json' do
|
||||
|
@ -1,135 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe RemoteFollowController do
|
||||
render_views
|
||||
|
||||
describe '#new' do
|
||||
it 'returns success when session is empty' do
|
||||
account = Fabricate(:account)
|
||||
get :new, params: { account_username: account.to_param }
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to render_template(:new)
|
||||
expect(assigns(:remote_follow).acct).to be_nil
|
||||
end
|
||||
|
||||
it 'populates the remote follow with session data when session exists' do
|
||||
session[:remote_follow] = 'user@example.com'
|
||||
account = Fabricate(:account)
|
||||
get :new, params: { account_username: account.to_param }
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to render_template(:new)
|
||||
expect(assigns(:remote_follow).acct).to eq 'user@example.com'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create' do
|
||||
before do
|
||||
@account = Fabricate(:account, username: 'test_user')
|
||||
end
|
||||
|
||||
context 'with a valid acct' do
|
||||
context 'when webfinger values are wrong' do
|
||||
it 'renders new when redirect url is nil' do
|
||||
resource_with_nil_link = double(link: nil)
|
||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_nil_link)
|
||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
expect(response.body).to include(I18n.t('remote_follow.missing_resource'))
|
||||
end
|
||||
|
||||
it 'renders new when template is nil' do
|
||||
resource_with_link = double(link: nil)
|
||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
|
||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
expect(response.body).to include(I18n.t('remote_follow.missing_resource'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when webfinger values are good' do
|
||||
before do
|
||||
resource_with_link = double(link: 'http://example.com/follow_me?acct={uri}')
|
||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
|
||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
||||
end
|
||||
|
||||
it 'saves the session' do
|
||||
expect(session[:remote_follow]).to eq 'user@example.com'
|
||||
end
|
||||
|
||||
it 'redirects to the remote location' do
|
||||
expect(response).to redirect_to("http://example.com/follow_me?acct=https%3A%2F%2F#{Rails.configuration.x.local_domain}%2Fusers%2Ftest_user")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an invalid acct' do
|
||||
it 'renders new when acct is missing' do
|
||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: '' } }
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
end
|
||||
|
||||
it 'renders new with error when webfinger fails' do
|
||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_raise(Webfinger::Error)
|
||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
expect(response.body).to include(I18n.t('remote_follow.missing_resource'))
|
||||
end
|
||||
|
||||
it 'renders new when occur HTTP::ConnectionError' do
|
||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@unknown').and_raise(HTTP::ConnectionError)
|
||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@unknown' } }
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
expect(response.body).to include(I18n.t('remote_follow.missing_resource'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a permanently suspended account' do
|
||||
before do
|
||||
@account = Fabricate(:account)
|
||||
@account.suspend!
|
||||
@account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone on GET to #new' do
|
||||
get :new, params: { account_username: @account.to_param }
|
||||
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
|
||||
it 'returns http gone on POST to #create' do
|
||||
post :create, params: { account_username: @account.to_param }
|
||||
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a temporarily suspended account' do
|
||||
before do
|
||||
@account = Fabricate(:account)
|
||||
@account.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden on GET to #new' do
|
||||
get :new, params: { account_username: @account.to_param }
|
||||
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
|
||||
it 'returns http forbidden on POST to #create' do
|
||||
post :create, params: { account_username: @account.to_param }
|
||||
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,39 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe RemoteInteractionController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:status) { Fabricate(:status) }
|
||||
|
||||
describe 'GET #new' do
|
||||
it 'returns 200' do
|
||||
get :new, params: { id: status.id }
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
context '@remote_follow is valid' do
|
||||
it 'returns 302' do
|
||||
allow_any_instance_of(RemoteFollow).to receive(:valid?) { true }
|
||||
allow_any_instance_of(RemoteFollow).to receive(:addressable_template) do
|
||||
Addressable::Template.new('https://hoge.com')
|
||||
end
|
||||
|
||||
post :create, params: { id: status.id, remote_follow: { acct: '@hoge' } }
|
||||
expect(response).to have_http_status(302)
|
||||
end
|
||||
end
|
||||
|
||||
context '@remote_follow is invalid' do
|
||||
it 'returns 200' do
|
||||
allow_any_instance_of(RemoteFollow).to receive(:valid?) { false }
|
||||
post :create, params: { id: status.id, remote_follow: { acct: '@hoge' } }
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -10,16 +10,15 @@ RSpec.describe TagsController, type: :controller do
|
||||
let!(:late) { Fabricate(:status, tags: [tag], text: 'late #test') }
|
||||
|
||||
context 'when tag exists' do
|
||||
it 'redirects to web version' do
|
||||
it 'returns http success' do
|
||||
get :show, params: { id: 'test', max_id: late.id }
|
||||
expect(response).to redirect_to('/web/tags/test')
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when tag does not exist' do
|
||||
it 'returns http missing for non-existent tag' do
|
||||
it 'returns http not found' do
|
||||
get :show, params: { id: 'none' }
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
@ -18,36 +18,16 @@ feature 'Profile' do
|
||||
visit account_path('alice')
|
||||
|
||||
is_expected.to have_title("alice (@alice@#{local_domain})")
|
||||
|
||||
within('.public-account-header h1') do
|
||||
is_expected.to have_content("alice @alice@#{local_domain}")
|
||||
end
|
||||
|
||||
bio_elem = first('.public-account-bio')
|
||||
expect(bio_elem).to have_content(alice_bio)
|
||||
# The bio has hashtags made clickable
|
||||
expect(bio_elem).to have_link('cryptology')
|
||||
expect(bio_elem).to have_link('science')
|
||||
# Nicknames are make clickable
|
||||
expect(bio_elem).to have_link('@alice')
|
||||
expect(bio_elem).to have_link('@bob')
|
||||
# Nicknames not on server are not clickable
|
||||
expect(bio_elem).not_to have_link('@pepe')
|
||||
end
|
||||
|
||||
scenario 'I can change my account' do
|
||||
visit settings_profile_path
|
||||
|
||||
fill_in 'Display name', with: 'Bob'
|
||||
fill_in 'Bio', with: 'Bob is silent'
|
||||
first('.btn[type=submit]').click
|
||||
|
||||
first('button[type=submit]').click
|
||||
|
||||
is_expected.to have_content 'Changes successfully saved!'
|
||||
|
||||
# View my own public profile and see the changes
|
||||
click_link "Bob @bob@#{local_domain}"
|
||||
|
||||
within('.public-account-header h1') do
|
||||
is_expected.to have_content("Bob @bob@#{local_domain}")
|
||||
end
|
||||
expect(first('.public-account-bio')).to have_content('Bob is silent')
|
||||
end
|
||||
end
|
||||
|
@ -3,40 +3,31 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe PermalinkRedirector do
|
||||
let(:remote_account) { Fabricate(:account, username: 'alice', domain: 'example.com', url: 'https://example.com/@alice', id: 2) }
|
||||
|
||||
describe '#redirect_url' do
|
||||
before do
|
||||
account = Fabricate(:account, username: 'alice', id: 1)
|
||||
Fabricate(:status, account: account, id: 123)
|
||||
Fabricate(:status, account: remote_account, id: 123, url: 'https://example.com/status-123')
|
||||
end
|
||||
|
||||
it 'returns path for legacy account links' do
|
||||
redirector = described_class.new('web/accounts/1')
|
||||
expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice'
|
||||
redirector = described_class.new('accounts/2')
|
||||
expect(redirector.redirect_path).to eq 'https://example.com/@alice'
|
||||
end
|
||||
|
||||
it 'returns path for legacy status links' do
|
||||
redirector = described_class.new('web/statuses/123')
|
||||
expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice/123'
|
||||
end
|
||||
|
||||
it 'returns path for legacy tag links' do
|
||||
redirector = described_class.new('web/timelines/tag/hoge')
|
||||
expect(redirector.redirect_path).to be_nil
|
||||
redirector = described_class.new('statuses/123')
|
||||
expect(redirector.redirect_path).to eq 'https://example.com/status-123'
|
||||
end
|
||||
|
||||
it 'returns path for pretty account links' do
|
||||
redirector = described_class.new('web/@alice')
|
||||
expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice'
|
||||
redirector = described_class.new('@alice@example.com')
|
||||
expect(redirector.redirect_path).to eq 'https://example.com/@alice'
|
||||
end
|
||||
|
||||
it 'returns path for pretty status links' do
|
||||
redirector = described_class.new('web/@alice/123')
|
||||
expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice/123'
|
||||
end
|
||||
|
||||
it 'returns path for pretty tag links' do
|
||||
redirector = described_class.new('web/tags/hoge')
|
||||
expect(redirector.redirect_path).to be_nil
|
||||
redirector = described_class.new('@alice/123')
|
||||
expect(redirector.redirect_path).to eq 'https://example.com/status-123'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,17 +3,6 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'The account show page' do
|
||||
it 'Has an h-feed with correct number of h-entry objects in it' do
|
||||
alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
|
||||
_status = Fabricate(:status, account: alice, text: 'Hello World')
|
||||
_status2 = Fabricate(:status, account: alice, text: 'Hello World Again')
|
||||
_status3 = Fabricate(:status, account: alice, text: 'Are You Still There World?')
|
||||
|
||||
get '/@alice'
|
||||
|
||||
expect(h_feed_entries.size).to eq(3)
|
||||
end
|
||||
|
||||
it 'has valid opengraph tags' do
|
||||
alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
|
||||
_status = Fabricate(:status, account: alice, text: 'Hello World')
|
||||
@ -33,8 +22,4 @@ describe 'The account show page' do
|
||||
def head_section
|
||||
Nokogiri::Slop(response.body).html.head
|
||||
end
|
||||
|
||||
def h_feed_entries
|
||||
Nokogiri::HTML(response.body).search('.h-feed .h-entry')
|
||||
end
|
||||
end
|
||||
|
@ -1,31 +1,83 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'Routes under accounts/' do
|
||||
describe 'the route for accounts who are followers of an account' do
|
||||
it 'routes to the followers action with the right username' do
|
||||
expect(get('/users/name/followers')).
|
||||
to route_to('follower_accounts#index', account_username: 'name')
|
||||
context 'with local username' do
|
||||
let(:username) { 'alice' }
|
||||
|
||||
it 'routes /@:username' do
|
||||
expect(get("/@#{username}")).to route_to('accounts#show', username: username)
|
||||
end
|
||||
|
||||
it 'routes /@:username.json' do
|
||||
expect(get("/@#{username}.json")).to route_to('accounts#show', username: username, format: 'json')
|
||||
end
|
||||
|
||||
it 'routes /@:username.rss' do
|
||||
expect(get("/@#{username}.rss")).to route_to('accounts#show', username: username, format: 'rss')
|
||||
end
|
||||
|
||||
it 'routes /@:username/:id' do
|
||||
expect(get("/@#{username}/123")).to route_to('statuses#show', account_username: username, id: '123')
|
||||
end
|
||||
|
||||
it 'routes /@:username/:id/embed' do
|
||||
expect(get("/@#{username}/123/embed")).to route_to('statuses#embed', account_username: username, id: '123')
|
||||
end
|
||||
|
||||
it 'routes /@:username/following' do
|
||||
expect(get("/@#{username}/following")).to route_to('following_accounts#index', account_username: username)
|
||||
end
|
||||
|
||||
it 'routes /@:username/followers' do
|
||||
expect(get("/@#{username}/followers")).to route_to('follower_accounts#index', account_username: username)
|
||||
end
|
||||
|
||||
it 'routes /@:username/with_replies' do
|
||||
expect(get("/@#{username}/with_replies")).to route_to('accounts#show', username: username)
|
||||
end
|
||||
|
||||
it 'routes /@:username/media' do
|
||||
expect(get("/@#{username}/media")).to route_to('accounts#show', username: username)
|
||||
end
|
||||
|
||||
it 'routes /@:username/tagged/:tag' do
|
||||
expect(get("/@#{username}/tagged/foo")).to route_to('accounts#show', username: username, tag: 'foo')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'the route for accounts who are followed by an account' do
|
||||
it 'routes to the following action with the right username' do
|
||||
expect(get('/users/name/following')).
|
||||
to route_to('following_accounts#index', account_username: 'name')
|
||||
end
|
||||
end
|
||||
context 'with remote username' do
|
||||
let(:username) { 'alice@example.com' }
|
||||
|
||||
describe 'the route for following an account' do
|
||||
it 'routes to the follow create action with the right username' do
|
||||
expect(post('/users/name/follow')).
|
||||
to route_to('account_follow#create', account_username: 'name')
|
||||
it 'routes /@:username' do
|
||||
expect(get("/@#{username}")).to route_to('home#index', username_with_domain: username)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'the route for unfollowing an account' do
|
||||
it 'routes to the unfollow create action with the right username' do
|
||||
expect(post('/users/name/unfollow')).
|
||||
to route_to('account_unfollow#create', account_username: 'name')
|
||||
it 'routes /@:username/:id' do
|
||||
expect(get("/@#{username}/123")).to route_to('home#index', username_with_domain: username, any: '123')
|
||||
end
|
||||
|
||||
it 'routes /@:username/:id/embed' do
|
||||
expect(get("/@#{username}/123/embed")).to route_to('home#index', username_with_domain: username, any: '123/embed')
|
||||
end
|
||||
|
||||
it 'routes /@:username/following' do
|
||||
expect(get("/@#{username}/following")).to route_to('home#index', username_with_domain: username, any: 'following')
|
||||
end
|
||||
|
||||
it 'routes /@:username/followers' do
|
||||
expect(get("/@#{username}/followers")).to route_to('home#index', username_with_domain: username, any: 'followers')
|
||||
end
|
||||
|
||||
it 'routes /@:username/with_replies' do
|
||||
expect(get("/@#{username}/with_replies")).to route_to('home#index', username_with_domain: username, any: 'with_replies')
|
||||
end
|
||||
|
||||
it 'routes /@:username/media' do
|
||||
expect(get("/@#{username}/media")).to route_to('home#index', username_with_domain: username, any: 'media')
|
||||
end
|
||||
|
||||
it 'routes /@:username/tagged/:tag' do
|
||||
expect(get("/@#{username}/tagged/foo")).to route_to('home#index', username_with_domain: username, any: 'tagged/foo')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user