@ -0,0 +1,71 @@ |
||||
--- |
||||
################################# |
||||
################################# |
||||
## Super Linter GitHub Actions ## |
||||
################################# |
||||
################################# |
||||
name: Lint Code Base |
||||
|
||||
# |
||||
# Documentation: |
||||
# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions |
||||
# |
||||
|
||||
############################# |
||||
# Start the job on all push # |
||||
############################# |
||||
on: |
||||
push: |
||||
branches-ignore: [main] |
||||
# Remove the line above to run when pushing to master |
||||
pull_request: |
||||
branches: [main] |
||||
|
||||
############### |
||||
# Set the Job # |
||||
############### |
||||
permissions: |
||||
checks: write |
||||
contents: read |
||||
pull-requests: write |
||||
statuses: write |
||||
|
||||
jobs: |
||||
build: |
||||
# Name the Job |
||||
name: Lint Code Base |
||||
# Set the agent to run on |
||||
runs-on: ubuntu-latest |
||||
|
||||
################## |
||||
# Load all steps # |
||||
################## |
||||
steps: |
||||
########################## |
||||
# Checkout the code base # |
||||
########################## |
||||
- name: Checkout Code |
||||
uses: actions/checkout@v3 |
||||
with: |
||||
# Full git history is needed to get a proper list of changed files within `super-linter` |
||||
fetch-depth: 0 |
||||
|
||||
- name: Intall dependencies |
||||
run: yarn install --frozen-lockfile |
||||
|
||||
################################ |
||||
# Run Linter against code base # |
||||
################################ |
||||
- name: Lint Code Base |
||||
uses: github/super-linter@v4 |
||||
env: |
||||
CSS_FILE_NAME: stylelint.config.js |
||||
DEFAULT_BRANCH: main |
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
||||
JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.js |
||||
LINTER_RULES_PATH: . |
||||
RUBY_CONFIG_FILE: .rubocop.yml |
||||
VALIDATE_ALL_CODEBASE: false |
||||
VALIDATE_CSS: true |
||||
VALIDATE_JAVASCRIPT_ES: true |
||||
VALIDATE_RUBY: true |
@ -1,37 +0,0 @@ |
||||
# Linter Documentation: |
||||
# https://github.com/sasstools/sass-lint/tree/v1.13.1/docs/options |
||||
|
||||
files: |
||||
include: app/javascript/styles/**/*.scss |
||||
ignore: |
||||
- app/javascript/styles/mastodon/reset.scss |
||||
|
||||
rules: |
||||
# Disallows |
||||
no-color-literals: 0 |
||||
no-css-comments: 0 |
||||
no-duplicate-properties: 0 |
||||
no-ids: 0 |
||||
no-important: 0 |
||||
no-mergeable-selectors: 0 |
||||
no-misspelled-properties: 0 |
||||
no-qualifying-elements: 0 |
||||
no-transition-all: 0 |
||||
no-vendor-prefixes: 0 |
||||
|
||||
# Nesting |
||||
force-element-nesting: 0 |
||||
force-attribute-nesting: 0 |
||||
force-pseudo-nesting: 0 |
||||
|
||||
# Name Formats |
||||
class-name-format: 0 |
||||
leading-zero: 0 |
||||
|
||||
# Style Guide |
||||
attribute-quotes: 0 |
||||
hex-length: 0 |
||||
indentation: 0 |
||||
nesting-depth: 0 |
||||
property-sort-order: 0 |
||||
quotes: 0 |
@ -0,0 +1,109 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class Api::V1::Admin::DomainBlocksController < Api::BaseController |
||||
include Authorization |
||||
include AccountableConcern |
||||
|
||||
LIMIT = 100 |
||||
|
||||
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_blocks' }, only: [:index, :show] |
||||
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_blocks' }, except: [:index, :show] |
||||
before_action :require_staff! |
||||
before_action :set_domain_blocks, only: :index |
||||
before_action :set_domain_block, only: [:show, :update, :destroy] |
||||
|
||||
after_action :insert_pagination_headers, only: :index |
||||
|
||||
PAGINATION_PARAMS = %i(limit).freeze |
||||
|
||||
def create |
||||
authorize :domain_block, :create? |
||||
|
||||
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil |
||||
return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if existing_domain_block.present? |
||||
|
||||
@domain_block = DomainBlock.create!(resource_params) |
||||
DomainBlockWorker.perform_async(@domain_block.id) |
||||
log_action :create, @domain_block |
||||
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer |
||||
end |
||||
|
||||
def index |
||||
authorize :domain_block, :index? |
||||
render json: @domain_blocks, each_serializer: REST::Admin::DomainBlockSerializer |
||||
end |
||||
|
||||
def show |
||||
authorize @domain_block, :show? |
||||
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer |
||||
end |
||||
|
||||
def update |
||||
authorize @domain_block, :update? |
||||
|
||||
@domain_block.update(domain_block_params) |
||||
severity_changed = @domain_block.severity_changed? |
||||
@domain_block.save! |
||||
DomainBlockWorker.perform_async(@domain_block.id, severity_changed) |
||||
log_action :update, @domain_block |
||||
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer |
||||
end |
||||
|
||||
def destroy |
||||
authorize @domain_block, :destroy? |
||||
UnblockDomainService.new.call(@domain_block) |
||||
log_action :destroy, @domain_block |
||||
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer |
||||
end |
||||
|
||||
private |
||||
|
||||
def set_domain_blocks |
||||
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) |
||||
end |
||||
|
||||
def set_domain_block |
||||
@domain_block = DomainBlock.find(params[:id]) |
||||
end |
||||
|
||||
def filtered_domain_blocks |
||||
# TODO: no filtering yet |
||||
DomainBlock.all |
||||
end |
||||
|
||||
def domain_block_params |
||||
params.permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) |
||||
end |
||||
|
||||
def insert_pagination_headers |
||||
set_pagination_headers(next_path, prev_path) |
||||
end |
||||
|
||||
def next_path |
||||
api_v1_admin_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue? |
||||
end |
||||
|
||||
def prev_path |
||||
api_v1_admin_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @domain_blocks.empty? |
||||
end |
||||
|
||||
def pagination_max_id |
||||
@domain_blocks.last.id |
||||
end |
||||
|
||||
def pagination_since_id |
||||
@domain_blocks.first.id |
||||
end |
||||
|
||||
def records_continue? |
||||
@domain_blocks.size == limit_param(LIMIT) |
||||
end |
||||
|
||||
def pagination_params(core_params) |
||||
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) |
||||
end |
||||
|
||||
def resource_params |
||||
params.permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) |
||||
end |
||||
end |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 27 KiB |
@ -1,197 +0,0 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import ReactSwipeableViews from 'react-swipeable-views'; |
||||
import classNames from 'classnames'; |
||||
import { connect } from 'react-redux'; |
||||
import { FormattedMessage } from 'react-intl'; |
||||
import { closeOnboarding } from '../../actions/onboarding'; |
||||
import screenHello from '../../../images/screen_hello.svg'; |
||||
import screenFederation from '../../../images/screen_federation.svg'; |
||||
import screenInteractions from '../../../images/screen_interactions.svg'; |
||||
import logoTransparent from '../../../images/logo_transparent.svg'; |
||||
import { disableSwiping } from 'mastodon/initial_state'; |
||||
|
||||
const FrameWelcome = ({ domain, onNext }) => ( |
||||
<div className='introduction__frame'> |
||||
<div className='introduction__illustration' style={{ background: `url(${logoTransparent}) no-repeat center center / auto 80%` }}> |
||||
<img src={screenHello} alt='' /> |
||||
</div> |
||||
|
||||
<div className='introduction__text introduction__text--centered'> |
||||
<h3><FormattedMessage id='introduction.welcome.headline' defaultMessage='First steps' /></h3> |
||||
<p><FormattedMessage id='introduction.welcome.text' defaultMessage="Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name." values={{ domain: <code>{domain}</code> }} /></p> |
||||
</div> |
||||
|
||||
<div className='introduction__action'> |
||||
<button className='button' onClick={onNext}><FormattedMessage id='introduction.welcome.action' defaultMessage="Let's go!" /></button> |
||||
</div> |
||||
</div> |
||||
); |
||||
|
||||
FrameWelcome.propTypes = { |
||||
domain: PropTypes.string.isRequired, |
||||
onNext: PropTypes.func.isRequired, |
||||
}; |
||||
|
||||
const FrameFederation = ({ onNext }) => ( |
||||
<div className='introduction__frame'> |
||||
<div className='introduction__illustration'> |
||||
<img src={screenFederation} alt='' /> |
||||
</div> |
||||
|
||||
<div className='introduction__text introduction__text--columnized'> |
||||
<div> |
||||
<h3><FormattedMessage id='introduction.federation.home.headline' defaultMessage='Home' /></h3> |
||||
<p><FormattedMessage id='introduction.federation.home.text' defaultMessage='Posts from people you follow will appear in your home feed. You can follow anyone on any server!' /></p> |
||||
</div> |
||||
|
||||
<div> |
||||
<h3><FormattedMessage id='introduction.federation.local.headline' defaultMessage='Local' /></h3> |
||||
<p><FormattedMessage id='introduction.federation.local.text' defaultMessage='Public posts from people on the same server as you will appear in the local timeline.' /></p> |
||||
</div> |
||||
|
||||
<div> |
||||
<h3><FormattedMessage id='introduction.federation.federated.headline' defaultMessage='Federated' /></h3> |
||||
<p><FormattedMessage id='introduction.federation.federated.text' defaultMessage='Public posts from other servers of the fediverse will appear in the federated timeline.' /></p> |
||||
</div> |
||||
</div> |
||||
|
||||
<div className='introduction__action'> |
||||
<button className='button' onClick={onNext}><FormattedMessage id='introduction.federation.action' defaultMessage='Next' /></button> |
||||
</div> |
||||
</div> |
||||
); |
||||
|
||||
FrameFederation.propTypes = { |
||||
onNext: PropTypes.func.isRequired, |
||||
}; |
||||
|
||||
const FrameInteractions = ({ onNext }) => ( |
||||
<div className='introduction__frame'> |
||||
<div className='introduction__illustration'> |
||||
<img src={screenInteractions} alt='' /> |
||||
</div> |
||||
|
||||
<div className='introduction__text introduction__text--columnized'> |
||||
<div> |
||||
<h3><FormattedMessage id='introduction.interactions.reply.headline' defaultMessage='Reply' /></h3> |
||||
<p><FormattedMessage id='introduction.interactions.reply.text' defaultMessage="You can reply to other people's and your own toots, which will chain them together in a conversation." /></p> |
||||
</div> |
||||
|
||||
<div> |
||||
<h3><FormattedMessage id='introduction.interactions.reblog.headline' defaultMessage='Boost' /></h3> |
||||
<p><FormattedMessage id='introduction.interactions.reblog.text' defaultMessage="You can share other people's toots with your followers by boosting them." /></p> |
||||
</div> |
||||
|
||||
<div> |
||||
<h3><FormattedMessage id='introduction.interactions.favourite.headline' defaultMessage='Favourite' /></h3> |
||||
<p><FormattedMessage id='introduction.interactions.favourite.text' defaultMessage='You can save a toot for later, and let the author know that you liked it, by favouriting it.' /></p> |
||||
</div> |
||||
</div> |
||||
|
||||
<div className='introduction__action'> |
||||
<button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish toot-orial!' /></button> |
||||
</div> |
||||
</div> |
||||
); |
||||
|
||||
FrameInteractions.propTypes = { |
||||
onNext: PropTypes.func.isRequired, |
||||
}; |
||||
|
||||
export default @connect(state => ({ domain: state.getIn(['meta', 'domain']) })) |
||||
class Introduction extends React.PureComponent { |
||||
|
||||
static propTypes = { |
||||
domain: PropTypes.string.isRequired, |
||||
dispatch: PropTypes.func.isRequired, |
||||
}; |
||||
|
||||
state = { |
||||
currentIndex: 0, |
||||
}; |
||||
|
||||
componentWillMount () { |
||||
this.pages = [ |
||||
<FrameWelcome domain={this.props.domain} onNext={this.handleNext} />, |
||||
<FrameFederation onNext={this.handleNext} />, |
||||
<FrameInteractions onNext={this.handleFinish} />, |
||||
]; |
||||
} |
||||
|
||||
componentDidMount() { |
||||
window.addEventListener('keyup', this.handleKeyUp); |
||||
} |
||||
|
||||
componentWillUnmount() { |
||||
window.addEventListener('keyup', this.handleKeyUp); |
||||
} |
||||
|
||||
handleDot = (e) => { |
||||
const i = Number(e.currentTarget.getAttribute('data-index')); |
||||
e.preventDefault(); |
||||
this.setState({ currentIndex: i }); |
||||
} |
||||
|
||||
handlePrev = () => { |
||||
this.setState(({ currentIndex }) => ({ |
||||
currentIndex: Math.max(0, currentIndex - 1), |
||||
})); |
||||
} |
||||
|
||||
handleNext = () => { |
||||
const { pages } = this; |
||||
|
||||
this.setState(({ currentIndex }) => ({ |
||||
currentIndex: Math.min(currentIndex + 1, pages.length - 1), |
||||
})); |
||||
} |
||||
|
||||
handleSwipe = (index) => { |
||||
this.setState({ currentIndex: index }); |
||||
} |
||||
|
||||
handleFinish = () => { |
||||
this.props.dispatch(closeOnboarding()); |
||||
} |
||||
|
||||
handleKeyUp = ({ key }) => { |
||||
switch (key) { |
||||
case 'ArrowLeft': |
||||
this.handlePrev(); |
||||
break; |
||||
case 'ArrowRight': |
||||
this.handleNext(); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
const { currentIndex } = this.state; |
||||
const { pages } = this; |
||||
|
||||
return ( |
||||
<div className='introduction'> |
||||
<ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} disabled={disableSwiping} className='introduction__pager'> |
||||
{pages.map((page, i) => ( |
||||
<div key={i} className={classNames('introduction__frame-wrapper', { 'active': i === currentIndex })}>{page}</div> |
||||
))} |
||||
</ReactSwipeableViews> |
||||
|
||||
<div className='introduction__dots'> |
||||
{pages.map((_, i) => ( |
||||
<div |
||||
key={`dot-${i}`} |
||||
role='button' |
||||
tabIndex='0' |
||||
data-index={i} |
||||
onClick={this.handleDot} |
||||
className={classNames('introduction__dot', { active: i === currentIndex })} |
||||
/> |
||||
))} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
@ -1,154 +0,0 @@ |
||||
.introduction { |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
align-items: center; |
||||
height: 100vh; |
||||
background: $ui-base-color; |
||||
|
||||
@media screen and (max-width: 920px) { |
||||
display: block !important; |
||||
} |
||||
|
||||
&__pager { |
||||
background: darken($ui-base-color, 8%); |
||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); |
||||
overflow: hidden; |
||||
} |
||||
|
||||
&__pager, |
||||
&__frame { |
||||
border-radius: 10px; |
||||
width: 50vw; |
||||
min-width: 920px; |
||||
|
||||
@media screen and (max-width: 920px) { |
||||
min-width: 0; |
||||
width: 100%; |
||||
border-radius: 0; |
||||
box-shadow: none; |
||||
} |
||||
} |
||||
|
||||
&__frame-wrapper { |
||||
opacity: 0; |
||||
transition: opacity 500ms linear; |
||||
|
||||
&.active { |
||||
opacity: 1; |
||||
transition: opacity 50ms linear; |
||||
} |
||||
} |
||||
|
||||
&__frame { |
||||
overflow: hidden; |
||||
} |
||||
|
||||
&__illustration { |
||||
height: 50vh; |
||||
|
||||
@media screen and (max-width: 630px) { |
||||
height: auto; |
||||
} |
||||
|
||||
img { |
||||
object-fit: cover; |
||||
display: block; |
||||
margin: 0; |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
} |
||||
|
||||
&__text { |
||||
border-top: 2px solid $ui-highlight-color; |
||||
|
||||
&--columnized { |
||||
display: flex; |
||||
|
||||
& > div { |
||||
flex: 1 1 33.33%; |
||||
text-align: center; |
||||
padding: 25px; |
||||
padding-bottom: 30px; |
||||
} |
||||
|
||||
@media screen and (max-width: 630px) { |
||||
display: block; |
||||
padding: 15px 0; |
||||
padding-bottom: 20px; |
||||
|
||||
& > div { |
||||
padding: 10px 25px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
h3 { |
||||
font-size: 24px; |
||||
line-height: 1.5; |
||||
font-weight: 700; |
||||
margin-bottom: 10px; |
||||
} |
||||
|
||||
p { |
||||
font-size: 16px; |
||||
line-height: 24px; |
||||
font-weight: 400; |
||||
color: $darker-text-color; |
||||
|
||||
code { |
||||
display: inline-block; |
||||
background: darken($ui-base-color, 8%); |
||||
font-size: 15px; |
||||
border: 1px solid lighten($ui-base-color, 8%); |
||||
border-radius: 2px; |
||||
padding: 1px 3px; |
||||
} |
||||
} |
||||
|
||||
&--centered { |
||||
padding: 25px; |
||||
padding-bottom: 30px; |
||||
text-align: center; |
||||
} |
||||
} |
||||
|
||||
&__dots { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
padding: 25px; |
||||
|
||||
@media screen and (max-width: 630px) { |
||||
display: none; |
||||
} |
||||
} |
||||
|
||||
&__dot { |
||||
width: 14px; |
||||
height: 14px; |
||||
border-radius: 14px; |
||||
border: 1px solid $ui-highlight-color; |
||||
background: transparent; |
||||
margin: 0 3px; |
||||
cursor: pointer; |
||||
|
||||
&:hover { |
||||
background: lighten($ui-base-color, 8%); |
||||
} |
||||
|
||||
&.active { |
||||
cursor: default; |
||||
background: $ui-highlight-color; |
||||
} |
||||
} |
||||
|
||||
&__action { |
||||
padding: 25px; |
||||
padding-top: 0; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class REST::Admin::DomainBlockSerializer < ActiveModel::Serializer |
||||
attributes :id, :domain, :created_at, :severity, |
||||
:reject_media, :reject_reports, |
||||
:private_comment, :public_comment, :obfuscate |
||||
|
||||
def id |
||||
object.id.to_s |
||||
end |
||||
end |
@ -0,0 +1,15 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class REST::Admin::ExistingDomainBlockErrorSerializer < ActiveModel::Serializer |
||||
attributes :error |
||||
|
||||
has_one :existing_domain_block, serializer: REST::Admin::DomainBlockSerializer |
||||
|
||||
def error |
||||
I18n.t('admin.domain_blocks.existing_domain_block', name: existing_domain_block.domain) |
||||
end |
||||
|
||||
def existing_domain_block |
||||
object |
||||
end |
||||
end |