Compare commits
98 Commits
5c5d81cf26
...
11900689bf
Author | SHA1 | Date |
---|---|---|
Essem | 11900689bf | 4 weeks ago |
Essem | 9b54b27bf7 | 4 weeks ago |
Essem | 40ae5aed66 | 4 weeks ago |
Essem | 13c9fa62fa | 4 weeks ago |
Essem | ba37843ec2 | 4 weeks ago |
Essem | b07f5f89d4 | 4 weeks ago |
Essem | aae6e1b1fd | 4 weeks ago |
Essem | a12b2ad57a | 4 weeks ago |
Essem | d54affc107 | 4 weeks ago |
Essem | 0d68ecf75d | 4 weeks ago |
Essem | 4311fff076 | 4 weeks ago |
Essem | a2ab3f541c | 4 weeks ago |
Essem | 11bebd28a2 | 4 weeks ago |
Essem | 09b64d761a | 4 weeks ago |
Essem | 0838432237 | 4 weeks ago |
Essem | a27b838741 | 4 weeks ago |
Essem | 4d832522b9 | 4 weeks ago |
Essem | 03ea7618ad | 4 weeks ago |
Essem | 22fc82dfee | 4 weeks ago |
Essem | 938175d5e8 | 4 weeks ago |
Essem | 28ecb2a4be | 4 weeks ago |
Essem | 14c0e46ef4 | 4 weeks ago |
Essem | 227a8d71b3 | 4 weeks ago |
Claire | c8e5e13c89 | 4 weeks 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 | 1 month ago |
Eugen Rochko | 29f9dc742e | 1 month ago |
Eugen Rochko | dd061291b1 | 1 month ago |
renovate[bot] | 766c1fea20 | 1 month ago |
renovate[bot] | 55e2c827bd | 1 month ago |
renovate[bot] | 45f8364cd1 | 1 month ago |
renovate[bot] | bbf36836b6 | 1 month ago |
github-actions[bot] | 799e3be9bd | 1 month ago |
Eugen Rochko | 8e7e86ee35 | 1 month ago |
Renaud Chaput | 6c381f20b1 | 1 month 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 { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
|
||||
import EditIcon from '@/material-icons/400-24px/edit.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 { Icon } from 'flavours/glitch/components/icon';
|
||||
import Motion from 'flavours/glitch/features/ui/util/optional_motion';
|
||||
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
|
||||
export default class Upload extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
sensitive: PropTypes.bool,
|
||||
onUndo: PropTypes.func.isRequired,
|
||||
onOpenFocalPoint: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleUndoClick = e => {
|
||||
e.stopPropagation();
|
||||
this.props.onUndo(this.props.media.get('id'));
|
||||
};
|
||||
|
||||
handleFocalPointClick = e => {
|
||||
e.stopPropagation();
|
||||
this.props.onOpenFocalPoint(this.props.media.get('id'));
|
||||
};
|
||||
|
||||
render () {
|
||||
const { media, sensitive } = this.props;
|
||||
|
||||
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>
|
||||
);
|
||||
export const Upload = ({ id, onDragStart, onDragEnter, onDragEnd }) => {
|
||||
const dispatch = useDispatch();
|
||||
const media = useSelector(state => state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id));
|
||||
const sensitive = useSelector(state => state.getIn(['compose', 'sensitive']));
|
||||
|
||||
const handleUndoClick = useCallback(() => {
|
||||
dispatch(undoUploadCompose(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleFocalPointClick = useCallback(() => {
|
||||
dispatch(initMediaEditModal(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
onDragStart(id);
|
||||
}, [onDragStart, id]);
|
||||
|
||||
const handleDragEnter = useCallback(() => {
|
||||
onDragEnter(id);
|
||||
}, [onDragEnter, id]);
|
||||
|
||||
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' 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 ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { useRef, useCallback } from 'react';
|
||||
|
||||
import UploadContainer from '../containers/upload_container';
|
||||
import UploadProgressContainer from '../containers/upload_progress_container';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import { SensitiveButton } from './sensitive_button';
|
||||
|
||||
export default class UploadForm extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
mediaIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { mediaIds } = this.props;
|
||||
import { changeMediaOrder } from 'flavours/glitch/actions/compose';
|
||||
|
||||
return (
|
||||
<>
|
||||
<UploadProgressContainer />
|
||||
|
||||
{mediaIds.size > 0 && (
|
||||
<div className='compose-form__uploads'>
|
||||
{mediaIds.map(id => (
|
||||
<UploadContainer id={id} key={id} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!mediaIds.isEmpty() && <SensitiveButton />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
import { SensitiveButton } from './sensitive_button';
|
||||
import { Upload } from './upload';
|
||||
import { UploadProgress } from './upload_progress';
|
||||
|
||||
export const UploadForm = () => {
|
||||
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']));
|
||||
|
||||
const dragItem = useRef();
|
||||
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 { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
|
||||
import EditIcon from '@/material-icons/400-24px/edit.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 { Icon } from 'mastodon/components/icon';
|
||||
import Motion from 'mastodon/features/ui/util/optional_motion';
|
||||
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
|
||||
export default class Upload extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
sensitive: PropTypes.bool,
|
||||
onUndo: PropTypes.func.isRequired,
|
||||
onOpenFocalPoint: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleUndoClick = e => {
|
||||
e.stopPropagation();
|
||||
this.props.onUndo(this.props.media.get('id'));
|
||||
};
|
||||
|
||||
handleFocalPointClick = e => {
|
||||
e.stopPropagation();
|
||||
this.props.onOpenFocalPoint(this.props.media.get('id'));
|
||||
};
|
||||
|
||||
render () {
|
||||
const { media, sensitive } = this.props;
|
||||
|
||||
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>
|
||||
);
|
||||
export const Upload = ({ id, onDragStart, onDragEnter, onDragEnd }) => {
|
||||
const dispatch = useDispatch();
|
||||
const media = useSelector(state => state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id));
|
||||
const sensitive = useSelector(state => state.getIn(['compose', 'spoiler']));
|
||||
|
||||
const handleUndoClick = useCallback(() => {
|
||||
dispatch(undoUploadCompose(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleFocalPointClick = useCallback(() => {
|
||||
dispatch(initMediaEditModal(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
onDragStart(id);
|
||||
}, [onDragStart, id]);
|
||||
|
||||
const handleDragEnter = useCallback(() => {
|
||||
onDragEnter(id);
|
||||
}, [onDragEnter, id]);
|
||||
|
||||
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' 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 ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { useRef, useCallback } from 'react';
|
||||
|
||||
import UploadContainer from '../containers/upload_container';
|
||||
import UploadProgressContainer from '../containers/upload_progress_container';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
export default class UploadForm extends ImmutablePureComponent {
|
||||
import { changeMediaOrder } from 'mastodon/actions/compose';
|
||||
|
||||
static propTypes = {
|
||||
mediaIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
import { Upload } from './upload';
|
||||
import { UploadProgress } from './upload_progress';
|
||||
|
||||
render () {
|
||||
const { mediaIds } = this.props;
|
||||
export const UploadForm = () => {
|
||||
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 (
|
||||
<>
|
||||
<UploadProgressContainer />
|
||||
const dragItem = useRef();
|
||||
const dragOverItem = useRef();
|
||||
|
||||
{mediaIds.size > 0 && (
|
||||
<div className='compose-form__uploads'>
|
||||
{mediaIds.map(id => (
|
||||
<UploadContainer id={id} key={id} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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