commit
cb19be67d1
@ -1,72 +1,19 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AboutController < ApplicationController
|
class AboutController < ApplicationController
|
||||||
include RegistrationSpamConcern
|
include WebAppControllerConcern
|
||||||
|
|
||||||
before_action :set_pack
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
layout 'public'
|
|
||||||
|
|
||||||
before_action :require_open_federation!, only: [:show, :more]
|
|
||||||
before_action :set_body_classes, only: :show
|
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
before_action :set_expires_in, only: [:more]
|
|
||||||
before_action :set_registration_form_time, only: :show
|
|
||||||
|
|
||||||
skip_before_action :require_functional!, only: [:more]
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def more
|
|
||||||
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
|
|
||||||
|
|
||||||
toc_generator = TOCGenerator.new(@instance_presenter.extended_description)
|
|
||||||
|
|
||||||
@rules = Rule.ordered
|
def show
|
||||||
@contents = toc_generator.html
|
expires_in 0, public: true unless user_signed_in?
|
||||||
@table_of_contents = toc_generator.toc
|
|
||||||
@blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
helper_method :display_blocks?
|
|
||||||
helper_method :display_blocks_rationale?
|
|
||||||
helper_method :public_fetch_mode?
|
|
||||||
helper_method :new_user
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def require_open_federation!
|
|
||||||
not_found if whitelist_mode?
|
|
||||||
end
|
|
||||||
|
|
||||||
def display_blocks?
|
|
||||||
Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
|
|
||||||
end
|
|
||||||
|
|
||||||
def display_blocks_rationale?
|
|
||||||
Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
|
|
||||||
end
|
|
||||||
|
|
||||||
def new_user
|
|
||||||
User.new.tap do |user|
|
|
||||||
user.build_account
|
|
||||||
user.build_invite_request
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'public'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_instance_presenter
|
def set_instance_presenter
|
||||||
@instance_presenter = InstancePresenter.new
|
@instance_presenter = InstancePresenter.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@hide_navbar = true
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_expires_in
|
|
||||||
expires_in 0, public: true
|
|
||||||
end
|
|
||||||
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
|
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::AboutController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_about_path
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::AppearanceController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_appearance_path
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::BrandingController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_branding_path
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::ContentRetentionController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_content_retention_path
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::DiscoveryController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_discovery_path
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::RegistrationsController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_registrations_path
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Instances::DomainBlocksController < Api::BaseController
|
||||||
|
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
|
||||||
|
|
||||||
|
before_action :require_enabled_api!
|
||||||
|
before_action :set_domain_blocks
|
||||||
|
|
||||||
|
def index
|
||||||
|
expires_in 3.minutes, public: true
|
||||||
|
render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_enabled_api!
|
||||||
|
head 404 unless Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_domain_blocks
|
||||||
|
@domain_blocks = DomainBlock.with_user_facing_limitations.by_severity
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController
|
||||||
|
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
|
||||||
|
|
||||||
|
before_action :set_extended_description
|
||||||
|
|
||||||
|
def show
|
||||||
|
expires_in 3.minutes, public: true
|
||||||
|
render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_extended_description
|
||||||
|
@extended_description = ExtendedDescription.current
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController
|
||||||
|
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
|
||||||
|
|
||||||
|
before_action :set_privacy_policy
|
||||||
|
|
||||||
|
def show
|
||||||
|
expires_in 1.day, public: true
|
||||||
|
render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_privacy_policy
|
||||||
|
@privacy_policy = PrivacyPolicy.current
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,32 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WebAppControllerConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
before_action :set_pack
|
||||||
|
before_action :redirect_unauthenticated_to_permalinks!
|
||||||
|
before_action :set_app_body_class
|
||||||
|
before_action :set_referrer_policy_header
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def set_pack
|
||||||
|
use_pack 'home'
|
||||||
|
end
|
||||||
|
end
|
@ -1,37 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class DirectoriesController < ApplicationController
|
|
||||||
layout 'public'
|
|
||||||
|
|
||||||
before_action :authenticate_user!, if: :whitelist_mode?
|
|
||||||
before_action :require_enabled!
|
|
||||||
before_action :set_instance_presenter
|
|
||||||
before_action :set_accounts
|
|
||||||
before_action :set_pack
|
|
||||||
|
|
||||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
|
||||||
|
|
||||||
def index
|
|
||||||
render :index
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'share'
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_enabled!
|
|
||||||
return not_found unless Setting.profile_directory
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_accounts
|
|
||||||
@accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query|
|
|
||||||
query.merge!(Account.not_excluded_by_account(current_account)) if current_account
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,28 +1,19 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PrivacyController < ApplicationController
|
class PrivacyController < ApplicationController
|
||||||
layout 'public'
|
include WebAppControllerConcern
|
||||||
|
|
||||||
before_action :set_pack
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
before_action :set_expires_in
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
expires_in 0, public: true if current_account.nil?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'public'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_instance_presenter
|
def set_instance_presenter
|
||||||
@instance_presenter = InstancePresenter.new
|
@instance_presenter = InstancePresenter.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_expires_in
|
|
||||||
expires_in 0, public: true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class PublicTimelinesController < ApplicationController
|
|
||||||
before_action :set_pack
|
|
||||||
layout 'public'
|
|
||||||
|
|
||||||
before_action :authenticate_user!, if: :whitelist_mode?
|
|
||||||
before_action :require_enabled!
|
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_instance_presenter
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def require_enabled!
|
|
||||||
not_found unless Setting.timeline_preview
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'with-modals'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'about'
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,46 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class RemoteFollowController < ApplicationController
|
|
||||||
include AccountOwnedConcern
|
|
||||||
|
|
||||||
layout 'modal'
|
|
||||||
|
|
||||||
before_action :set_pack
|
|
||||||
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_pack
|
|
||||||
use_pack 'modal'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'modal-layout'
|
|
||||||
@hide_header = true
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,60 +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
|
|
||||||
before_action :set_pack
|
|
||||||
|
|
||||||
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_pack
|
|
||||||
use_pack 'modal'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_interaction_type
|
|
||||||
@interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply'
|
|
||||||
end
|
|
||||||
end
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,34 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export const FEATURED_TAGS_FETCH_REQUEST = 'FEATURED_TAGS_FETCH_REQUEST';
|
||||||
|
export const FEATURED_TAGS_FETCH_SUCCESS = 'FEATURED_TAGS_FETCH_SUCCESS';
|
||||||
|
export const FEATURED_TAGS_FETCH_FAIL = 'FEATURED_TAGS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const fetchFeaturedTags = (id) => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['user_lists', 'featured_tags', id, 'items'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchFeaturedTagsRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/accounts/${id}/featured_tags`)
|
||||||
|
.then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data)))
|
||||||
|
.catch(err => dispatch(fetchFeaturedTagsFail(id, err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchFeaturedTagsRequest = (id) => ({
|
||||||
|
type: FEATURED_TAGS_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchFeaturedTagsSuccess = (id, tags) => ({
|
||||||
|
type: FEATURED_TAGS_FETCH_SUCCESS,
|
||||||
|
id,
|
||||||
|
tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchFeaturedTagsFail = (id, error) => ({
|
||||||
|
type: FEATURED_TAGS_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import IconButton from './icon_button';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import { bannerSettings } from 'mastodon/settings';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class DismissableBanner extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.node,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
visible: !bannerSettings.get(this.props.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDismiss = () => {
|
||||||
|
const { id } = this.props;
|
||||||
|
this.setState({ visible: false }, () => bannerSettings.set(id, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { visible } = this.state;
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { children, intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='dismissable-banner'>
|
||||||
|
<div className='dismissable-banner__message'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='dismissable-banner__action'>
|
||||||
|
<IconButton icon='times' title={intl.formatMessage(messages.dismiss)} onClick={this.handleDismiss} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Blurhash from './blurhash';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export default class Image extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
src: PropTypes.string,
|
||||||
|
srcSet: PropTypes.string,
|
||||||
|
blurhash: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLoad = () => this.setState({ loaded: true });
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { src, srcSet, blurhash, className } = this.props;
|
||||||
|
const { loaded } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('image', { loaded }, className)} role='presentation'>
|
||||||
|
{blurhash && <Blurhash hash={blurhash} className='image__preview' />}
|
||||||
|
<img src={src} srcSet={srcSet} alt='' onLoad={this.handleLoad} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||||
|
import { showTrends } from 'mastodon/initial_state';
|
||||||
|
import Trends from 'mastodon/features/getting_started/containers/trends_container';
|
||||||
|
import AccountNavigation from 'mastodon/features/account/navigation';
|
||||||
|
|
||||||
|
const DefaultNavigation = () => (
|
||||||
|
<>
|
||||||
|
{showTrends && (
|
||||||
|
<>
|
||||||
|
<div className='flex-spacer' />
|
||||||
|
<Trends />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default @withRouter
|
||||||
|
class NavigationPortal extends React.PureComponent {
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route path='/@:acct/(tagged/:tagged?)?' component={AccountNavigation} />
|
||||||
|
<Route component={DefaultNavigation} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
const NotSignedInIndicator = () => (
|
||||||
|
<div className='scrollable scrollable--flex'>
|
||||||
|
<div className='empty-column-indicator'>
|
||||||
|
<FormattedMessage id='not_signed_in_indicator.not_signed_in' defaultMessage='You need to sign in to access this resource.' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NotSignedInIndicator;
|
@ -0,0 +1,222 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Column from 'mastodon/components/column';
|
||||||
|
import LinkFooter from 'mastodon/features/ui/components/link_footer';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
|
||||||
|
import Account from 'mastodon/containers/account_container';
|
||||||
|
import Skeleton from 'mastodon/components/skeleton';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Image from 'mastodon/components/image';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'column.about', defaultMessage: 'About' },
|
||||||
|
rules: { id: 'about.rules', defaultMessage: 'Server rules' },
|
||||||
|
blocks: { id: 'about.blocks', defaultMessage: 'Moderated servers' },
|
||||||
|
silenced: { id: 'about.domain_blocks.silenced.title', defaultMessage: 'Limited' },
|
||||||
|
silencedExplanation: { id: 'about.domain_blocks.silenced.explanation', defaultMessage: 'You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.' },
|
||||||
|
suspended: { id: 'about.domain_blocks.suspended.title', defaultMessage: 'Suspended' },
|
||||||
|
suspendedExplanation: { id: 'about.domain_blocks.suspended.explanation', defaultMessage: 'No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const severityMessages = {
|
||||||
|
silence: {
|
||||||
|
title: messages.silenced,
|
||||||
|
explanation: messages.silencedExplanation,
|
||||||
|
},
|
||||||
|
|
||||||
|
suspend: {
|
||||||
|
title: messages.suspended,
|
||||||
|
explanation: messages.suspendedExplanation,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
server: state.getIn(['server', 'server']),
|
||||||
|
extendedDescription: state.getIn(['server', 'extendedDescription']),
|
||||||
|
domainBlocks: state.getIn(['server', 'domainBlocks']),
|
||||||
|
});
|
||||||
|
|
||||||
|
class Section extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
open: PropTypes.bool,
|
||||||
|
onOpen: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
collapsed: !this.props.open,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
const { onOpen } = this.props;
|
||||||
|
const { collapsed } = this.state;
|
||||||
|
|
||||||
|
this.setState({ collapsed: !collapsed }, () => onOpen && onOpen());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { title, children } = this.props;
|
||||||
|
const { collapsed } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('about__section', { active: !collapsed })}>
|
||||||
|
<div className='about__section__title' role='button' tabIndex='0' onClick={this.handleClick}>
|
||||||
|
<Icon id={collapsed ? 'chevron-right' : 'chevron-down'} fixedWidth /> {title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!collapsed && (
|
||||||
|
<div className='about__section__body'>{children}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class About extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
server: ImmutablePropTypes.map,
|
||||||
|
extendedDescription: ImmutablePropTypes.map,
|
||||||
|
domainBlocks: ImmutablePropTypes.contains({
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
isAvailable: PropTypes.bool,
|
||||||
|
items: ImmutablePropTypes.list,
|
||||||
|
}),
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchServer());
|
||||||
|
dispatch(fetchExtendedDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDomainBlocksOpen = () => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchDomainBlocks());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props;
|
||||||
|
const isLoading = server.get('isLoading');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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' />
|
||||||
|
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
|
||||||
|
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='about__meta'>
|
||||||
|
<div className='about__meta__column'>
|
||||||
|
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
|
||||||
|
|
||||||
|
<Account id={server.getIn(['contact', 'account', 'id'])} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className='about__meta__divider' />
|
||||||
|
|
||||||
|
<div className='about__meta__column'>
|
||||||
|
<h4><FormattedMessage id='about.contact' defaultMessage='Contact:' /></h4>
|
||||||
|
|
||||||
|
{isLoading ? <Skeleton width='10ch' /> : <a className='about__mail' href={`mailto:${server.getIn(['contact', 'email'])}`}>{server.getIn(['contact', 'email'])}</a>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Section open title={intl.formatMessage(messages.title)}>
|
||||||
|
{extendedDescription.get('isLoading') ? (
|
||||||
|
<>
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<br />
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<br />
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<br />
|
||||||
|
<Skeleton width='70%' />
|
||||||
|
</>
|
||||||
|
) : (extendedDescription.get('content')?.length > 0 ? (
|
||||||
|
<div
|
||||||
|
className='prose'
|
||||||
|
dangerouslySetInnerHTML={{ __html: extendedDescription.get('content') }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title={intl.formatMessage(messages.rules)}>
|
||||||
|
{!isLoading && (server.get('rules').isEmpty() ? (
|
||||||
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
|
) : (
|
||||||
|
<ol className='rules-list'>
|
||||||
|
{server.get('rules').map(rule => (
|
||||||
|
<li key={rule.get('id')}>
|
||||||
|
<span className='rules-list__text'>{rule.get('text')}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}>
|
||||||
|
{domainBlocks.get('isLoading') ? (
|
||||||
|
<>
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<br />
|
||||||
|
<Skeleton width='70%' />
|
||||||
|
</>
|
||||||
|
) : (domainBlocks.get('isAvailable') ? (
|
||||||
|
<>
|
||||||
|
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
|
||||||
|
|
||||||
|
<table className='about__domain-blocks'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><FormattedMessage id='about.domain_blocks.domain' defaultMessage='Domain' /></th>
|
||||||
|
<th><FormattedMessage id='about.domain_blocks.severity' defaultMessage='Severity' /></th>
|
||||||
|
<th><FormattedMessage id='about.domain_blocks.comment' defaultMessage='Reason' /></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{domainBlocks.get('items').map(block => (
|
||||||
|
<tr key={block.get('domain')}>
|
||||||
|
<td><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></td>
|
||||||
|
<td><span title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span></td>
|
||||||
|
<td>{block.get('comment')}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<LinkFooter />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)}</title>
|
||||||
|
<meta name='robots' content='all' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import Hashtag from 'mastodon/components/hashtag';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' },
|
||||||
|
empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class FeaturedTags extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map,
|
||||||
|
featuredTags: ImmutablePropTypes.list,
|
||||||
|
tagged: PropTypes.string,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, featuredTags, intl } = this.props;
|
||||||
|
|
||||||
|
if (!account || account.get('suspended') || featuredTags.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='getting-started__trends'>
|
||||||
|
<h4><FormattedMessage id='account.featured_tags.title' defaultMessage="{name}'s featured hashtags" values={{ name: <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> }} /></h4>
|
||||||
|
|
||||||
|
{featuredTags.take(3).map(featuredTag => (
|
||||||
|
<Hashtag
|
||||||
|
key={featuredTag.get('name')}
|
||||||
|
name={featuredTag.get('name')}
|
||||||
|
href={featuredTag.get('url')}
|
||||||
|
to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
|
||||||
|
uses={featuredTag.get('statuses_count') * 1}
|
||||||
|
withGraph={false}
|
||||||
|
description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import FeaturedTags from '../components/featured_tags';
|
||||||
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
const mapStateToProps = () => {
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
return (state, { accountId }) => ({
|
||||||
|
account: getAccount(state, accountId),
|
||||||
|
featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items'], ImmutableList()),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(FeaturedTags);
|
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import FeaturedTags from 'mastodon/features/account/containers/featured_tags_container';
|
||||||
|
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { match: { params: { acct } } }) => {
|
||||||
|
const accountId = state.getIn(['accounts_map', normalizeForLookup(acct)]);
|
||||||
|
|
||||||
|
if (!accountId) {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accountId,
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class AccountNavigation extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
match: PropTypes.shape({
|
||||||
|
params: PropTypes.shape({
|
||||||
|
acct: PropTypes.string,
|
||||||
|
tagged: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
accountId: PropTypes.string,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { accountId, isLoading, match: { params: { tagged } } } = this.props;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='flex-spacer' />
|
||||||
|
<FeaturedTags accountId={accountId} tagged={tagged} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
import { fetchServer } from 'mastodon/actions/server';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
closed_registrations_message: state.getIn(['server', 'server', 'registrations', 'closed_registrations_message']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class ClosedRegistrationsModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchServer());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let closedRegistrationsMessage;
|
||||||
|
|
||||||
|
if (this.props.closed_registrations_message) {
|
||||||
|
closedRegistrationsMessage = (
|
||||||
|
<p
|
||||||
|
className='prose'
|
||||||
|
dangerouslySetInnerHTML={{ __html: this.props.closed_registrations_message }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
closedRegistrationsMessage = (
|
||||||
|
<p className='prose'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='closed_registrations_modal.description'
|
||||||
|
defaultMessage='Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.'
|
||||||
|
values={{ domain: <strong>{domain}</strong> }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal interaction-modal'>
|
||||||
|
<div className='interaction-modal__lead'>
|
||||||
|
<h3><FormattedMessage id='closed_registrations_modal.title' defaultMessage='Signing up on Mastodon' /></h3>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='closed_registrations_modal.preamble'
|
||||||
|
defaultMessage='Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='interaction-modal__choices'>
|
||||||
|
<div className='interaction-modal__choices__choice'>
|
||||||
|
<h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3>
|
||||||
|
{closedRegistrationsMessage}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='interaction-modal__choices__choice'>
|
||||||
|
<h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3>
|
||||||
|
<p className='prose'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='closed_registrations.other_server_instructions'
|
||||||
|
defaultMessage='Since Mastodon is decentralized, you can create an account on another server and still interact with this one.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<a href='https://joinmastodon.org/servers' className='button button--block'><FormattedMessage id='closed_registrations_modal.find_another_server' defaultMessage='Find another server' /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue