diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index 9361358078..499be2010a 100644 --- a/.github/workflows/lint-haml.yml +++ b/.github/workflows/lint-haml.yml @@ -43,4 +43,4 @@ jobs: - name: Run haml-lint run: | echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json" - bin/haml-lint --parallel --reporter github + bin/haml-lint --reporter github diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9c9751775e..1f88835bd0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.72.2. +# using RuboCop version 1.73.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new diff --git a/Gemfile.lock b/Gemfile.lock index 3e97c94d35..e89e762265 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -719,7 +719,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 8) rspec-support (3.13.2) - rubocop (1.72.2) + rubocop (1.73.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -730,7 +730,7 @@ GEM rubocop-ast (>= 1.38.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.38.0) + rubocop-ast (1.38.1) parser (>= 3.3.1.0) rubocop-capybara (2.21.0) rubocop (~> 1.41) @@ -738,7 +738,7 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.72.1, < 2.0) rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rails (2.30.2) + rubocop-rails (2.30.3) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) @@ -854,7 +854,7 @@ GEM unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) - uri (1.0.2) + uri (1.0.3) useragent (0.16.11) validate_email (0.1.6) activemodel (>= 3.0) diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index 5ea26d55bd..c427e055ea 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -3,8 +3,8 @@ class Api::V1::MediaController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:media' } before_action :require_user! - before_action :set_media_attachment, except: [:create] - before_action :check_processing, except: [:create] + before_action :set_media_attachment, except: [:create, :destroy] + before_action :check_processing, except: [:create, :destroy] def show render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment @@ -25,6 +25,15 @@ class Api::V1::MediaController < Api::BaseController render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment end + def destroy + @media_attachment = current_account.media_attachments.find(params[:id]) + + return render json: in_usage_error, status: 422 unless @media_attachment.status_id.nil? + + @media_attachment.destroy + render_empty + end + private def status_code_for_media_attachment @@ -54,4 +63,8 @@ class Api::V1::MediaController < Api::BaseController def processing_error { error: 'Error processing thumbnail for uploaded media' } end + + def in_usage_error + { error: 'Media attachment is currently used by a status' } + end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 2593ef7da5..819c6f424a 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -113,7 +113,7 @@ class Api::V1::StatusesController < Api::BaseController @status.account.statuses_count = @status.account.statuses_count - 1 json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true - RemovalWorker.perform_async(@status.id, { 'redraft' => true }) + RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) }) render json: json end diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index 513b9e24a9..e54e6c3f14 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -142,6 +142,13 @@ export function fetchAccountFail(id, error) { }; } +/** + * @param {string} id + * @param {Object} options + * @param {boolean} [options.reblogs] + * @param {boolean} [options.notify] + * @returns {function(): void} + */ export function followAccount(id, options = { reblogs: true }) { return (dispatch, getState) => { const alreadyFollowing = getState().getIn(['relationships', id, 'following']); diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 2b6b589b37..02c8e70df0 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -139,7 +139,7 @@ export function deleteStatus(id, withRedraft = false) { dispatch(deleteStatusRequest(id)); - api().delete(`/api/v1/statuses/${id}`).then(response => { + api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => { dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); dispatch(importFetchedAccount(response.data.account)); diff --git a/app/javascript/flavours/glitch/components/follow_button.tsx b/app/javascript/flavours/glitch/components/follow_button.tsx index 5365da3838..a7d6573bfa 100644 --- a/app/javascript/flavours/glitch/components/follow_button.tsx +++ b/app/javascript/flavours/glitch/components/follow_button.tsx @@ -57,7 +57,7 @@ export const FollowButton: React.FC<{ ); } - if (!relationship) return; + if (!relationship || !accountId) return; if (accountId === me) { return; diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.jsx b/app/javascript/flavours/glitch/features/account/components/action_bar.jsx deleted file mode 100644 index 3fcf0440c0..0000000000 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import { PureComponent } from 'react'; - -import { FormattedMessage, FormattedNumber } from 'react-intl'; - -import { NavLink } from 'react-router-dom'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; - -import InfoIcon from '@/material-icons/400-24px/info.svg?react'; -import { Icon } from 'flavours/glitch/components/icon'; - - -class ActionBar extends PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - }; - - isStatusesPageActive = (match, location) => { - if (!match) { - return false; - } - return !location.pathname.match(/\/(followers|following)\/?$/); - }; - - render () { - const { account } = this.props; - - if (account.get('suspended')) { - return ( -
-
- - -
-
- ); - } - - let extraInfo = ''; - - if (account.get('acct') !== account.get('username')) { - extraInfo = ( -
- -
- - {' '} - - - -
-
- ); - } - - return ( -
- {extraInfo} - -
-
- - - - - - - - - - - - - { account.get('followers_count') < 0 ? '-' : } - -
-
-
- ); - } - -} - -export default ActionBar; diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.tsx b/app/javascript/flavours/glitch/features/account/components/action_bar.tsx new file mode 100644 index 0000000000..3f47da1339 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.tsx @@ -0,0 +1,106 @@ +import { FormattedMessage, FormattedNumber } from 'react-intl'; + +import { NavLink } from 'react-router-dom'; +import type { NavLinkProps } from 'react-router-dom'; + +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import { Icon } from 'flavours/glitch/components/icon'; +import type { Account } from 'flavours/glitch/models/account'; + +const isStatusesPageActive: NavLinkProps['isActive'] = (match, location) => { + if (!match) { + return false; + } + return !/\/(followers|following)\/?$/.exec(location.pathname); +}; + +export const ActionBar: React.FC<{ account: Account }> = ({ account }) => { + if (account.suspended) { + return ( +
+
+ + +
+
+ ); + } + + let extraInfo = null; + + if (account.get('acct') !== account.get('username')) { + extraInfo = ( +
+ +
+ {' '} + + + +
+
+ ); + } + + return ( +
+ {extraInfo} + +
+
+ + + + + + + + + + + + + + + + + + {account.get('followers_count') < 0 ? ( + '-' + ) : ( + + )} + + +
+
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/account/components/header.jsx b/app/javascript/flavours/glitch/features/account/components/header.jsx deleted file mode 100644 index 6de90b2a43..0000000000 --- a/app/javascript/flavours/glitch/features/account/components/header.jsx +++ /dev/null @@ -1,415 +0,0 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; -import { Helmet } from 'react-helmet'; -import { withRouter } from 'react-router-dom'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import CheckIcon from '@/material-icons/400-24px/check.svg?react'; -import LockIcon from '@/material-icons/400-24px/lock.svg?react'; -import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; -import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react'; -import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react'; -import ShareIcon from '@/material-icons/400-24px/share.svg?react'; -import { Avatar } from 'flavours/glitch/components/avatar'; -import { Badge, AutomatedBadge, GroupBadge } from 'flavours/glitch/components/badge'; -import { Button } from 'flavours/glitch/components/button'; -import { CopyIconButton } from 'flavours/glitch/components/copy_icon_button'; -import { Icon } from 'flavours/glitch/components/icon'; -import { IconButton } from 'flavours/glitch/components/icon_button'; -import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; -import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; -import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; -import { autoPlayGif, me, domain as localDomain } from 'flavours/glitch/initial_state'; -import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; -import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; -import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; - -import AccountNoteContainer from '../containers/account_note_container'; -import FollowRequestNoteContainer from '../containers/follow_request_note_container'; - -import { DomainPill } from './domain_pill'; - -const messages = defineMessages({ - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, - linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' }, - account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' }, - mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, - direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - report: { id: 'account.report', defaultMessage: 'Report @{name}' }, - share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' }, - copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' }, - media: { id: 'account.media', defaultMessage: 'Media' }, - blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, - unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, - hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, - showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, - enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' }, - disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' }, - pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, - follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, - favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' }, - lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, - followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, - blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, - domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' }, - mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, - endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, - unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, - add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, - admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, - admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, - languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, - openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, -}); - -const titleFromAccount = account => { - const displayName = account.get('display_name'); - const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${localDomain}` : account.get('acct'); - const prefix = displayName.trim().length === 0 ? account.get('username') : displayName; - - return `${prefix} (@${acct})`; -}; - -const dateFormatOptions = { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', -}; - -class Header extends ImmutablePureComponent { - - static propTypes = { - identity: identityContextPropShape, - account: ImmutablePropTypes.record, - identity_props: ImmutablePropTypes.list, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onDirect: PropTypes.func.isRequired, - onReblogToggle: PropTypes.func.isRequired, - onNotifyToggle: PropTypes.func.isRequired, - onReport: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - onBlockDomain: PropTypes.func.isRequired, - onUnblockDomain: PropTypes.func.isRequired, - onEndorseToggle: PropTypes.func.isRequired, - onAddToList: PropTypes.func.isRequired, - onChangeLanguages: PropTypes.func.isRequired, - onInteractionModal: PropTypes.func.isRequired, - onOpenAvatar: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - domain: PropTypes.string.isRequired, - hidden: PropTypes.bool, - ...WithRouterPropTypes, - }; - - openEditProfile = () => { - window.open(profileLink, '_blank'); - }; - - handleMouseEnter = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-original'); - } - }; - - handleMouseLeave = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-static'); - } - }; - - handleAvatarClick = e => { - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.onOpenAvatar(); - } - }; - - handleShare = () => { - const { account } = this.props; - - navigator.share({ - url: account.get('url'), - }).catch((e) => { - if (e.name !== 'AbortError') console.error(e); - }); - }; - - render () { - const { account, hidden, intl } = this.props; - const { signedIn, permissions } = this.props.identity; - - if (!account) { - return null; - } - - const suspended = account.get('suspended'); - const isRemote = account.get('acct') !== account.get('username'); - const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null; - - let actionBtn, bellBtn, lockedIcon, shareBtn; - - let info = []; - let menu = []; - - if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { - info.push(); - } else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) { - info.push(); - } - - if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) { - info.push(); - } else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) { - info.push(); - } - - if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) { - bellBtn = ; - } - - if ('share' in navigator) { - shareBtn = ; - } else { - shareBtn = ; - } - - if (me !== account.get('id')) { - if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded - actionBtn = ; - } else if (account.getIn(['relationship', 'requested'])) { - actionBtn = + ); + } else if (!relationship?.blocking) { + actionBtn = ( +