Compare commits
98 Commits
5c5d81cf26
...
11900689bf
Author | SHA1 | Date |
---|---|---|
Essem | 11900689bf | 1 month ago |
Essem | 9b54b27bf7 | 1 month ago |
Essem | 40ae5aed66 | 1 month ago |
Essem | 13c9fa62fa | 1 month ago |
Essem | ba37843ec2 | 1 month ago |
Essem | b07f5f89d4 | 1 month ago |
Essem | aae6e1b1fd | 1 month ago |
Essem | a12b2ad57a | 1 month ago |
Essem | d54affc107 | 1 month ago |
Essem | 0d68ecf75d | 1 month ago |
Essem | 4311fff076 | 1 month ago |
Essem | a2ab3f541c | 1 month ago |
Essem | 11bebd28a2 | 1 month ago |
Essem | 09b64d761a | 1 month ago |
Essem | 0838432237 | 1 month ago |
Essem | a27b838741 | 1 month ago |
Essem | 4d832522b9 | 1 month ago |
Essem | 03ea7618ad | 1 month ago |
Essem | 22fc82dfee | 1 month ago |
Essem | 938175d5e8 | 1 month ago |
Essem | 28ecb2a4be | 1 month ago |
Essem | 14c0e46ef4 | 1 month ago |
Essem | 227a8d71b3 | 1 month ago |
Claire | c8e5e13c89 | 1 month ago |
Eugen Rochko | 91531e9586 | 1 month ago |
Eugen Rochko | fc533cfad3 | 1 month ago |
Eugen Rochko | b55bbfa2b3 | 1 month ago |
Eugen Rochko | 1ae08ae257 | 1 month ago |
Claire | 0e76b919b5 | 1 month ago |
Claire | a844a6a577 | 1 month ago |
Claire | c3a128f31e | 1 month ago |
Claire | afaad0755f | 1 month ago |
Claire | 1d1c3a808a | 1 month ago |
Claire | f635cde756 | 1 month ago |
Claire | 0f8b33238f | 1 month ago |
Renaud Chaput | 576c085ea0 | 1 month ago |
Claire | 777984faeb | 1 month ago |
Claire | f14b6f3d99 | 1 month ago |
Claire | 903dc53522 | 1 month ago |
Eugen Rochko | 375af259a2 | 1 month ago |
Claire | 67842ffb22 | 1 month ago |
Claire | 0f966209ca | 1 month ago |
Claire | f2b23aa5f3 | 1 month ago |
Eugen Rochko | 0cea7a623b | 2 months ago |
Eugen Rochko | 29f9dc742e | 2 months ago |
Eugen Rochko | dd061291b1 | 2 months ago |
renovate[bot] | 766c1fea20 | 2 months ago |
renovate[bot] | 55e2c827bd | 2 months ago |
renovate[bot] | 45f8364cd1 | 2 months ago |
renovate[bot] | bbf36836b6 | 2 months ago |
github-actions[bot] | 799e3be9bd | 2 months ago |
Eugen Rochko | 8e7e86ee35 | 2 months ago |
Renaud Chaput | 6c381f20b1 | 2 months ago |
Claire | 81a04ac25c | 2 months ago |
Claire | 37ca59815c | 2 months ago |
renovate[bot] | 119c7aa0df | 2 months ago |
Claire | 58376eedda | 2 months ago |
Claire | d71d26a3c9 | 2 months ago |
Claire | de6c9e0fcd | 2 months ago |
Claire | 387c78ddf9 | 2 months ago |
Claire | dfa43707eb | 2 months ago |
Matt Jankowski | 34f293475e | 2 months ago |
github-actions[bot] | 5db5fa879b | 2 months ago |
Matt Jankowski | 8c1d29df7e | 2 months ago |
Renaud Chaput | ec1e770fea | 2 months ago |
Claire | 05eda8d193 | 2 months ago |
Claire | 70a8fcf07d | 2 months ago |
Matt Jankowski | 142c018cfa | 2 months ago |
renovate[bot] | ec6d016da1 | 2 months ago |
renovate[bot] | 7f5e930bd2 | 2 months ago |
renovate[bot] | f5444c8fe4 | 2 months ago |
renovate[bot] | 05abefe989 | 2 months ago |
Claire | 814a48517f | 2 months ago |
Matt Jankowski | a59f5694fe | 2 months ago |
Claire | 75f34b80a8 | 2 months ago |
renovate[bot] | 1df00d4e76 | 2 months ago |
renovate[bot] | 2ec3fcaffe | 2 months ago |
renovate[bot] | a506b09de0 | 2 months ago |
renovate[bot] | 1feb228275 | 2 months ago |
Nick Schonning | d13cdced1e | 2 months ago |
Claire | 885d0faf83 | 2 months ago |
github-actions[bot] | c007dd5dd2 | 2 months ago |
Matt Jankowski | 77897cd24c | 2 months ago |
Matt Jankowski | 718ee72c80 | 2 months ago |
Matt Jankowski | cdd168f5d3 | 2 months ago |
renovate[bot] | 01464074c9 | 2 months ago |
renovate[bot] | 3f363c61bc | 2 months ago |
Claire | 7434c9c276 | 2 months ago |
Matt Jankowski | 39bac24cb7 | 2 months ago |
Matt Jankowski | 62722238c9 | 2 months ago |
Eugen Rochko | be52633ee4 | 2 months ago |
Claire | f4d753aedf | 2 months ago |
Claire | 98a2bb8be2 | 2 months ago |
Claire | 954b470fbc | 2 months ago |
Claire | d4449cc682 | 2 months ago |
renovate[bot] | 99c9db5f67 | 2 months ago |
renovate[bot] | 27a6fa7b0e | 2 months ago |
Claire | 44bf7b8128 | 2 months ago |
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Statuses::ReactionsController < Api::V1::Statuses::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
def create
|
||||||
|
ReactService.new.call(current_account, @status, params[:id])
|
||||||
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
UnreactWorker.perform_async(current_account.id, @status.id, params[:id])
|
||||||
|
|
||||||
|
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reactions_map: { @status.id => false })
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,61 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SeveredRelationshipsController < ApplicationController
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :set_body_classes
|
||||||
|
before_action :set_cache_headers
|
||||||
|
|
||||||
|
before_action :set_event, only: [:following, :followers]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@events = AccountRelationshipSeveranceEvent.where(account: current_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def following
|
||||||
|
respond_to do |format|
|
||||||
|
format.csv { send_data following_data, filename: "following-#{@event.target_name}-#{@event.created_at.to_date.iso8601}.csv" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def followers
|
||||||
|
respond_to do |format|
|
||||||
|
format.csv { send_data followers_data, filename: "followers-#{@event.target_name}-#{@event.created_at.to_date.iso8601}.csv" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_event
|
||||||
|
@event = AccountRelationshipSeveranceEvent.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def following_data
|
||||||
|
CSV.generate(headers: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], write_headers: true) do |csv|
|
||||||
|
@event.severed_relationships.active.about_local_account(current_account).includes(:remote_account).reorder(id: :desc).each do |follow|
|
||||||
|
csv << [acct(follow.target_account), follow.show_reblogs, follow.notify, follow.languages&.join(', ')]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def followers_data
|
||||||
|
CSV.generate(headers: ['Account address'], write_headers: true) do |csv|
|
||||||
|
@event.severed_relationships.passive.about_local_account(current_account).includes(:remote_account).reorder(id: :desc).each do |follow|
|
||||||
|
csv << [acct(follow.account)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def acct(account)
|
||||||
|
account.local? ? account.local_username_and_domain : account.acct
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_body_classes
|
||||||
|
@body_classes = 'admin'
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_cache_headers
|
||||||
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,175 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
|
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||||
|
import spring from 'react-motion/lib/spring';
|
||||||
|
|
||||||
|
import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
|
||||||
|
import { autoPlayGif, reduceMotion } from '../initial_state';
|
||||||
|
import { assetHost } from '../utils/config';
|
||||||
|
|
||||||
|
import { AnimatedNumber } from './animated_number';
|
||||||
|
|
||||||
|
export default class StatusReactions extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
statusId: PropTypes.string.isRequired,
|
||||||
|
reactions: ImmutablePropTypes.list.isRequired,
|
||||||
|
numVisible: PropTypes.number,
|
||||||
|
addReaction: PropTypes.func.isRequired,
|
||||||
|
canReact: PropTypes.bool.isRequired,
|
||||||
|
removeReaction: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
willEnter() {
|
||||||
|
return { scale: reduceMotion ? 1 : 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
willLeave() {
|
||||||
|
return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { reactions, numVisible } = this.props;
|
||||||
|
let visibleReactions = reactions
|
||||||
|
.filter(x => x.get('count') > 0)
|
||||||
|
.sort((a, b) => b.get('count') - a.get('count'));
|
||||||
|
|
||||||
|
if (numVisible >= 0) {
|
||||||
|
visibleReactions = visibleReactions.filter((_, i) => i < numVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = visibleReactions.map(reaction => ({
|
||||||
|
key: reaction.get('name'),
|
||||||
|
data: reaction,
|
||||||
|
style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
|
||||||
|
})).toArray();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
|
||||||
|
{items => (
|
||||||
|
<div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
||||||
|
{items.map(({ key, data, style }) => (
|
||||||
|
<Reaction
|
||||||
|
key={key}
|
||||||
|
statusId={this.props.statusId}
|
||||||
|
reaction={data}
|
||||||
|
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
|
||||||
|
addReaction={this.props.addReaction}
|
||||||
|
removeReaction={this.props.removeReaction}
|
||||||
|
canReact={this.props.canReact}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TransitionMotion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Reaction extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
statusId: PropTypes.string,
|
||||||
|
reaction: ImmutablePropTypes.map.isRequired,
|
||||||
|
addReaction: PropTypes.func.isRequired,
|
||||||
|
removeReaction: PropTypes.func.isRequired,
|
||||||
|
canReact: PropTypes.bool.isRequired,
|
||||||
|
style: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
hovered: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
const { reaction, statusId, addReaction, removeReaction } = this.props;
|
||||||
|
|
||||||
|
if (reaction.get('me')) {
|
||||||
|
removeReaction(statusId, reaction.get('name'));
|
||||||
|
} else {
|
||||||
|
addReaction(statusId, reaction.get('name'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMouseEnter = () => this.setState({ hovered: true });
|
||||||
|
|
||||||
|
handleMouseLeave = () => this.setState({ hovered: false });
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { reaction } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
onMouseEnter={this.handleMouseEnter}
|
||||||
|
onMouseLeave={this.handleMouseLeave}
|
||||||
|
disabled={!this.props.canReact}
|
||||||
|
style={this.props.style}
|
||||||
|
>
|
||||||
|
<span className='reactions-bar__item__emoji'>
|
||||||
|
<Emoji
|
||||||
|
hovered={this.state.hovered}
|
||||||
|
emoji={reaction.get('name')}
|
||||||
|
url={reaction.get('url')}
|
||||||
|
staticUrl={reaction.get('static_url')}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className='reactions-bar__item__count'>
|
||||||
|
<AnimatedNumber value={reaction.get('count')} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Emoji extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
emoji: PropTypes.string.isRequired,
|
||||||
|
hovered: PropTypes.bool.isRequired,
|
||||||
|
url: PropTypes.string,
|
||||||
|
staticUrl: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { emoji, hovered, url, staticUrl } = this.props;
|
||||||
|
|
||||||
|
if (unicodeMapping[emoji]) {
|
||||||
|
const { filename, shortCode } = unicodeMapping[this.props.emoji];
|
||||||
|
const title = shortCode ? `:${shortCode}:` : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
draggable='false'
|
||||||
|
className='emojione'
|
||||||
|
alt={emoji}
|
||||||
|
title={title}
|
||||||
|
src={`${assetHost}/emoji/${filename}.svg`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const filename = (autoPlayGif || hovered) ? url : staticUrl;
|
||||||
|
const shortCode = `:${emoji}:`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
draggable='false'
|
||||||
|
className='emojione custom-emoji'
|
||||||
|
alt={shortCode}
|
||||||
|
title={shortCode}
|
||||||
|
src={filename}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,77 +1,81 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
|
||||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||||
|
import { undoUploadCompose, initMediaEditModal } from 'flavours/glitch/actions/compose';
|
||||||
import { Blurhash } from 'flavours/glitch/components/blurhash';
|
import { Blurhash } from 'flavours/glitch/components/blurhash';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import Motion from 'flavours/glitch/features/ui/util/optional_motion';
|
||||||
|
|
||||||
import Motion from '../../ui/util/optional_motion';
|
export const Upload = ({ id, onDragStart, onDragEnter, onDragEnd }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
export default class Upload extends ImmutablePureComponent {
|
const media = useSelector(state => state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id));
|
||||||
|
const sensitive = useSelector(state => state.getIn(['compose', 'sensitive']));
|
||||||
static propTypes = {
|
|
||||||
media: ImmutablePropTypes.map.isRequired,
|
const handleUndoClick = useCallback(() => {
|
||||||
sensitive: PropTypes.bool,
|
dispatch(undoUploadCompose(id));
|
||||||
onUndo: PropTypes.func.isRequired,
|
}, [dispatch, id]);
|
||||||
onOpenFocalPoint: PropTypes.func.isRequired,
|
|
||||||
};
|
const handleFocalPointClick = useCallback(() => {
|
||||||
|
dispatch(initMediaEditModal(id));
|
||||||
handleUndoClick = e => {
|
}, [dispatch, id]);
|
||||||
e.stopPropagation();
|
|
||||||
this.props.onUndo(this.props.media.get('id'));
|
const handleDragStart = useCallback(() => {
|
||||||
};
|
onDragStart(id);
|
||||||
|
}, [onDragStart, id]);
|
||||||
handleFocalPointClick = e => {
|
|
||||||
e.stopPropagation();
|
const handleDragEnter = useCallback(() => {
|
||||||
this.props.onOpenFocalPoint(this.props.media.get('id'));
|
onDragEnter(id);
|
||||||
};
|
}, [onDragEnter, id]);
|
||||||
|
|
||||||
render () {
|
if (!media) {
|
||||||
const { media, sensitive } = this.props;
|
return null;
|
||||||
|
|
||||||
if (!media) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusX = media.getIn(['meta', 'focus', 'x']);
|
|
||||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
|
||||||
const x = ((focusX / 2) + .5) * 100;
|
|
||||||
const y = ((focusY / -2) + .5) * 100;
|
|
||||||
const missingDescription = (media.get('description') || '').length === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='compose-form__upload'>
|
|
||||||
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
|
||||||
{({ scale }) => (
|
|
||||||
<div className='compose-form__upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: !sensitive ? `url(${media.get('preview_url')})` : null, backgroundPosition: `${x}% ${y}%` }}>
|
|
||||||
{sensitive && <Blurhash
|
|
||||||
hash={media.get('blurhash')}
|
|
||||||
className='compose-form__upload__preview'
|
|
||||||
/>}
|
|
||||||
|
|
||||||
<div className='compose-form__upload__actions'>
|
|
||||||
<button type='button' className='icon-button compose-form__upload__delete' onClick={this.handleUndoClick}><Icon icon={CloseIcon} /></button>
|
|
||||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='compose-form__upload__warning'>
|
|
||||||
<button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={this.handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Motion>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||||
|
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||||
|
const x = ((focusX / 2) + .5) * 100;
|
||||||
|
const y = ((focusY / -2) + .5) * 100;
|
||||||
|
const missingDescription = (media.get('description') || '').length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='compose-form__upload' draggable onDragStart={handleDragStart} onDragEnter={handleDragEnter} onDragEnd={onDragEnd}>
|
||||||
|
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
||||||
|
{({ scale }) => (
|
||||||
|
<div className='compose-form__upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: !sensitive ? `url(${media.get('preview_url')})` : null, backgroundPosition: `${x}% ${y}%` }}>
|
||||||
|
{sensitive && <Blurhash
|
||||||
|
hash={media.get('blurhash')}
|
||||||
|
className='compose-form__upload__preview'
|
||||||
|
/>}
|
||||||
|
|
||||||
|
<div className='compose-form__upload__actions'>
|
||||||
|
<button type='button' className='icon-button compose-form__upload__delete' onClick={handleUndoClick}><Icon icon={CloseIcon} /></button>
|
||||||
|
<button type='button' className='icon-button' onClick={handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='compose-form__upload__warning'>
|
||||||
|
<button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Motion>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Upload.propTypes = {
|
||||||
|
id: PropTypes.string,
|
||||||
|
onDragEnter: PropTypes.func,
|
||||||
|
onDragStart: PropTypes.func,
|
||||||
|
onDragEnd: PropTypes.func,
|
||||||
|
};
|
||||||
|
@ -1,35 +1,56 @@
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import { useRef, useCallback } from 'react';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import UploadContainer from '../containers/upload_container';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import UploadProgressContainer from '../containers/upload_progress_container';
|
|
||||||
|
|
||||||
import { SensitiveButton } from './sensitive_button';
|
import { changeMediaOrder } from 'flavours/glitch/actions/compose';
|
||||||
|
|
||||||
export default class UploadForm extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
mediaIds: ImmutablePropTypes.list.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { mediaIds } = this.props;
|
|
||||||
|
|
||||||
return (
|
import { SensitiveButton } from './sensitive_button';
|
||||||
<>
|
import { Upload } from './upload';
|
||||||
<UploadProgressContainer />
|
import { UploadProgress } from './upload_progress';
|
||||||
|
|
||||||
{mediaIds.size > 0 && (
|
export const UploadForm = () => {
|
||||||
<div className='compose-form__uploads'>
|
const dispatch = useDispatch();
|
||||||
{mediaIds.map(id => (
|
const mediaIds = useSelector(state => state.getIn(['compose', 'media_attachments']).map(item => item.get('id')));
|
||||||
<UploadContainer id={id} key={id} />
|
const active = useSelector(state => state.getIn(['compose', 'is_uploading']));
|
||||||
))}
|
const progress = useSelector(state => state.getIn(['compose', 'progress']));
|
||||||
</div>
|
const isProcessing = useSelector(state => state.getIn(['compose', 'is_processing']));
|
||||||
)}
|
|
||||||
|
const dragItem = useRef();
|
||||||
{!mediaIds.isEmpty() && <SensitiveButton />}
|
const dragOverItem = useRef();
|
||||||
</>
|
|
||||||
);
|
const handleDragStart = useCallback(id => {
|
||||||
}
|
dragItem.current = id;
|
||||||
|
}, [dragItem]);
|
||||||
}
|
|
||||||
|
const handleDragEnter = useCallback(id => {
|
||||||
|
dragOverItem.current = id;
|
||||||
|
}, [dragOverItem]);
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(() => {
|
||||||
|
dispatch(changeMediaOrder(dragItem.current, dragOverItem.current));
|
||||||
|
dragItem.current = null;
|
||||||
|
dragOverItem.current = null;
|
||||||
|
}, [dispatch, dragItem, dragOverItem]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UploadProgress active={active} progress={progress} isProcessing={isProcessing} />
|
||||||
|
|
||||||
|
{mediaIds.size > 0 && (
|
||||||
|
<div className='compose-form__uploads'>
|
||||||
|
{mediaIds.map(id => (
|
||||||
|
<Upload
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!mediaIds.isEmpty() && <SensitiveButton />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { undoUploadCompose, initMediaEditModal, submitCompose } from '../../../actions/compose';
|
|
||||||
import Upload from '../components/upload';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { id }) => ({
|
|
||||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
|
||||||
sensitive: state.getIn(['compose', 'sensitive']),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
|
|
||||||
onUndo: id => {
|
|
||||||
dispatch(undoUploadCompose(id));
|
|
||||||
},
|
|
||||||
|
|
||||||
onOpenFocalPoint: id => {
|
|
||||||
dispatch(initMediaEditModal(id));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSubmit (router) {
|
|
||||||
dispatch(submitCompose(router));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Upload);
|
|
@ -1,9 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import UploadForm from '../components/upload_form';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(UploadForm);
|
|
@ -1,11 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import UploadProgress from '../components/upload_progress';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
active: state.getIn(['compose', 'is_uploading']),
|
|
||||||
progress: state.getIn(['compose', 'progress']),
|
|
||||||
isProcessing: state.getIn(['compose', 'is_processing']),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(UploadProgress);
|
|
@ -0,0 +1,45 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import HeartBrokenIcon from '@/material-icons/400-24px/heart_broken-fill.svg?react';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import { domain } from 'flavours/glitch/initial_state';
|
||||||
|
|
||||||
|
// This needs to be kept in sync with app/models/relationships_severance_event.rb
|
||||||
|
const messages = defineMessages({
|
||||||
|
account_suspension: { id: 'notification.relationships_severance_event.account_suspension', defaultMessage: 'An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.' },
|
||||||
|
domain_block: { id: 'notification.relationships_severance_event.domain_block', defaultMessage: 'An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.' },
|
||||||
|
user_domain_block: { id: 'notification.relationships_severance_event.user_domain_block', defaultMessage: 'You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RelationshipsSeveranceEvent = ({ type, target, followingCount, followersCount, hidden }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href='/severed_relationships' target='_blank' rel='noopener noreferrer' className='notification__relationships-severance-event'>
|
||||||
|
<Icon id='heart_broken' icon={HeartBrokenIcon} />
|
||||||
|
|
||||||
|
<div className='notification__relationships-severance-event__content'>
|
||||||
|
<p>{intl.formatMessage(messages[type], { from: <strong>{domain}</strong>, target: <strong>{target}</strong>, followingCount, followersCount })}</p>
|
||||||
|
<span className='link-button'><FormattedMessage id='notification.relationships_severance_event.learn_more' defaultMessage='Learn more' /></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RelationshipsSeveranceEvent.propTypes = {
|
||||||
|
type: PropTypes.oneOf([
|
||||||
|
'account_suspension',
|
||||||
|
'domain_block',
|
||||||
|
'user_domain_block',
|
||||||
|
]).isRequired,
|
||||||
|
target: PropTypes.string.isRequired,
|
||||||
|
followersCount: PropTypes.number.isRequired,
|
||||||
|
followingCount: PropTypes.number.isRequired,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
|
};
|
@ -0,0 +1,16 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
|
export class IdentityConsumer extends PureComponent {
|
||||||
|
static contextTypes = {
|
||||||
|
identity: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.props.children(this.context.identity);
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -1,77 +1,81 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
|
||||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||||
|
import { undoUploadCompose, initMediaEditModal } from 'mastodon/actions/compose';
|
||||||
import { Blurhash } from 'mastodon/components/blurhash';
|
import { Blurhash } from 'mastodon/components/blurhash';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import Motion from 'mastodon/features/ui/util/optional_motion';
|
||||||
|
|
||||||
import Motion from '../../ui/util/optional_motion';
|
export const Upload = ({ id, onDragStart, onDragEnter, onDragEnd }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
export default class Upload extends ImmutablePureComponent {
|
const media = useSelector(state => state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id));
|
||||||
|
const sensitive = useSelector(state => state.getIn(['compose', 'spoiler']));
|
||||||
static propTypes = {
|
|
||||||
media: ImmutablePropTypes.map.isRequired,
|
const handleUndoClick = useCallback(() => {
|
||||||
sensitive: PropTypes.bool,
|
dispatch(undoUploadCompose(id));
|
||||||
onUndo: PropTypes.func.isRequired,
|
}, [dispatch, id]);
|
||||||
onOpenFocalPoint: PropTypes.func.isRequired,
|
|
||||||
};
|
const handleFocalPointClick = useCallback(() => {
|
||||||
|
dispatch(initMediaEditModal(id));
|
||||||
handleUndoClick = e => {
|
}, [dispatch, id]);
|
||||||
e.stopPropagation();
|
|
||||||
this.props.onUndo(this.props.media.get('id'));
|
const handleDragStart = useCallback(() => {
|
||||||
};
|
onDragStart(id);
|
||||||
|
}, [onDragStart, id]);
|
||||||
handleFocalPointClick = e => {
|
|
||||||
e.stopPropagation();
|
const handleDragEnter = useCallback(() => {
|
||||||
this.props.onOpenFocalPoint(this.props.media.get('id'));
|
onDragEnter(id);
|
||||||
};
|
}, [onDragEnter, id]);
|
||||||
|
|
||||||
render () {
|
if (!media) {
|
||||||
const { media, sensitive } = this.props;
|
return null;
|
||||||
|
|
||||||
if (!media) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusX = media.getIn(['meta', 'focus', 'x']);
|
|
||||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
|
||||||
const x = ((focusX / 2) + .5) * 100;
|
|
||||||
const y = ((focusY / -2) + .5) * 100;
|
|
||||||
const missingDescription = (media.get('description') || '').length === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='compose-form__upload'>
|
|
||||||
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
|
||||||
{({ scale }) => (
|
|
||||||
<div className='compose-form__upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: !sensitive ? `url(${media.get('preview_url')})` : null, backgroundPosition: `${x}% ${y}%` }}>
|
|
||||||
{sensitive && <Blurhash
|
|
||||||
hash={media.get('blurhash')}
|
|
||||||
className='compose-form__upload__preview'
|
|
||||||
/>}
|
|
||||||
|
|
||||||
<div className='compose-form__upload__actions'>
|
|
||||||
<button type='button' className='icon-button compose-form__upload__delete' onClick={this.handleUndoClick}><Icon icon={CloseIcon} /></button>
|
|
||||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='compose-form__upload__warning'>
|
|
||||||
<button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={this.handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Motion>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||||
|
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||||
|
const x = ((focusX / 2) + .5) * 100;
|
||||||
|
const y = ((focusY / -2) + .5) * 100;
|
||||||
|
const missingDescription = (media.get('description') || '').length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='compose-form__upload' draggable onDragStart={handleDragStart} onDragEnter={handleDragEnter} onDragEnd={onDragEnd}>
|
||||||
|
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
||||||
|
{({ scale }) => (
|
||||||
|
<div className='compose-form__upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: !sensitive ? `url(${media.get('preview_url')})` : null, backgroundPosition: `${x}% ${y}%` }}>
|
||||||
|
{sensitive && <Blurhash
|
||||||
|
hash={media.get('blurhash')}
|
||||||
|
className='compose-form__upload__preview'
|
||||||
|
/>}
|
||||||
|
|
||||||
|
<div className='compose-form__upload__actions'>
|
||||||
|
<button type='button' className='icon-button compose-form__upload__delete' onClick={handleUndoClick}><Icon icon={CloseIcon} /></button>
|
||||||
|
<button type='button' className='icon-button' onClick={handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='compose-form__upload__warning'>
|
||||||
|
<button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Motion>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Upload.propTypes = {
|
||||||
|
id: PropTypes.string,
|
||||||
|
onDragEnter: PropTypes.func,
|
||||||
|
onDragStart: PropTypes.func,
|
||||||
|
onDragEnd: PropTypes.func,
|
||||||
|
};
|
||||||
|
@ -1,31 +1,53 @@
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import { useRef, useCallback } from 'react';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import UploadContainer from '../containers/upload_container';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import UploadProgressContainer from '../containers/upload_progress_container';
|
|
||||||
|
|
||||||
export default class UploadForm extends ImmutablePureComponent {
|
import { changeMediaOrder } from 'mastodon/actions/compose';
|
||||||
|
|
||||||
static propTypes = {
|
import { Upload } from './upload';
|
||||||
mediaIds: ImmutablePropTypes.list.isRequired,
|
import { UploadProgress } from './upload_progress';
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
export const UploadForm = () => {
|
||||||
const { mediaIds } = this.props;
|
const dispatch = useDispatch();
|
||||||
|
const mediaIds = useSelector(state => state.getIn(['compose', 'media_attachments']).map(item => item.get('id')));
|
||||||
|
const active = useSelector(state => state.getIn(['compose', 'is_uploading']));
|
||||||
|
const progress = useSelector(state => state.getIn(['compose', 'progress']));
|
||||||
|
const isProcessing = useSelector(state => state.getIn(['compose', 'is_processing']));
|
||||||
|
|
||||||
return (
|
const dragItem = useRef();
|
||||||
<>
|
const dragOverItem = useRef();
|
||||||
<UploadProgressContainer />
|
|
||||||
|
|
||||||
{mediaIds.size > 0 && (
|
const handleDragStart = useCallback(id => {
|
||||||
<div className='compose-form__uploads'>
|
dragItem.current = id;
|
||||||
{mediaIds.map(id => (
|
}, [dragItem]);
|
||||||
<UploadContainer id={id} key={id} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
const handleDragEnter = useCallback(id => {
|
||||||
|
dragOverItem.current = id;
|
||||||
|
}, [dragOverItem]);
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(() => {
|
||||||
|
dispatch(changeMediaOrder(dragItem.current, dragOverItem.current));
|
||||||
|
dragItem.current = null;
|
||||||
|
dragOverItem.current = null;
|
||||||
|
}, [dispatch, dragItem, dragOverItem]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UploadProgress active={active} progress={progress} isProcessing={isProcessing} />
|
||||||
|
|
||||||
|
{mediaIds.size > 0 && (
|
||||||
|
<div className='compose-form__uploads'>
|
||||||
|
{mediaIds.map(id => (
|
||||||
|
<Upload
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { undoUploadCompose, initMediaEditModal, submitCompose } from '../../../actions/compose';
|
|
||||||
import Upload from '../components/upload';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { id }) => ({
|
|
||||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
|
||||||
sensitive: state.getIn(['compose', 'spoiler']),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
|
|
||||||
onUndo: id => {
|
|
||||||
dispatch(undoUploadCompose(id));
|
|
||||||
},
|
|
||||||
|
|
||||||
onOpenFocalPoint: id => {
|
|
||||||
dispatch(initMediaEditModal(id));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSubmit (router) {
|
|
||||||
dispatch(submitCompose(router));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Upload);
|
|
@ -1,9 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import UploadForm from '../components/upload_form';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(UploadForm);
|
|
@ -1,11 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import UploadProgress from '../components/upload_progress';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
active: state.getIn(['compose', 'is_uploading']),
|
|
||||||
progress: state.getIn(['compose', 'progress']),
|
|
||||||
isProcessing: state.getIn(['compose', 'is_processing']),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(UploadProgress);
|
|
@ -0,0 +1,45 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import HeartBrokenIcon from '@/material-icons/400-24px/heart_broken-fill.svg?react';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
// This needs to be kept in sync with app/models/relationships_severance_event.rb
|
||||||
|
const messages = defineMessages({
|
||||||
|
account_suspension: { id: 'notification.relationships_severance_event.account_suspension', defaultMessage: 'An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.' },
|
||||||
|
domain_block: { id: 'notification.relationships_severance_event.domain_block', defaultMessage: 'An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.' },
|
||||||
|
user_domain_block: { id: 'notification.relationships_severance_event.user_domain_block', defaultMessage: 'You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RelationshipsSeveranceEvent = ({ type, target, followingCount, followersCount, hidden }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href='/severed_relationships' target='_blank' rel='noopener noreferrer' className='notification__relationships-severance-event'>
|
||||||
|
<Icon id='heart_broken' icon={HeartBrokenIcon} />
|
||||||
|
|
||||||
|
<div className='notification__relationships-severance-event__content'>
|
||||||
|
<p>{intl.formatMessage(messages[type], { from: <strong>{domain}</strong>, target: <strong>{target}</strong>, followingCount, followersCount })}</p>
|
||||||
|
<span className='link-button'><FormattedMessage id='notification.relationships_severance_event.learn_more' defaultMessage='Learn more' /></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RelationshipsSeveranceEvent.propTypes = {
|
||||||
|
type: PropTypes.oneOf([
|
||||||
|
'account_suspension',
|
||||||
|
'domain_block',
|
||||||
|
'user_domain_block',
|
||||||
|
]).isRequired,
|
||||||
|
target: PropTypes.string.isRequired,
|
||||||
|
followersCount: PropTypes.number.isRequired,
|
||||||
|
followingCount: PropTypes.number.isRequired,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue