mirror of
https://github.com/mastodon/mastodon.git
synced 2025-10-06 17:12:44 +00:00
Allow editing status quote policy (#35762)
This commit is contained in:
parent
a2cddb9eac
commit
651e51a82e
|
@ -1,8 +1,10 @@
|
||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { apiGetContext } from 'mastodon/api/statuses';
|
import { apiGetContext, apiSetQuotePolicy } from 'mastodon/api/statuses';
|
||||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||||
|
|
||||||
|
import type { ApiQuotePolicy } from '../api_types/quotes';
|
||||||
|
|
||||||
import { importFetchedStatuses } from './importer';
|
import { importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
export const fetchContext = createDataLoadingThunk(
|
export const fetchContext = createDataLoadingThunk(
|
||||||
|
@ -23,3 +25,10 @@ export const fetchContext = createDataLoadingThunk(
|
||||||
export const completeContextRefresh = createAction<{ statusId: string }>(
|
export const completeContextRefresh = createAction<{ statusId: string }>(
|
||||||
'status/context/complete',
|
'status/context/complete',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const setStatusQuotePolicy = createDataLoadingThunk(
|
||||||
|
'status/setQuotePolicy',
|
||||||
|
({ statusId, policy }: { statusId: string; policy: ApiQuotePolicy }) => {
|
||||||
|
return apiSetQuotePolicy(statusId, policy);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import api, { getAsyncRefreshHeader } from 'mastodon/api';
|
import api, { apiRequestPut, getAsyncRefreshHeader } from 'mastodon/api';
|
||||||
import type { ApiContextJSON } from 'mastodon/api_types/statuses';
|
import type {
|
||||||
|
ApiContextJSON,
|
||||||
|
ApiStatusJSON,
|
||||||
|
} from 'mastodon/api_types/statuses';
|
||||||
|
|
||||||
|
import type { ApiQuotePolicy } from '../api_types/quotes';
|
||||||
|
|
||||||
export const apiGetContext = async (statusId: string) => {
|
export const apiGetContext = async (statusId: string) => {
|
||||||
const response = await api().request<ApiContextJSON>({
|
const response = await api().request<ApiContextJSON>({
|
||||||
|
@ -12,3 +17,15 @@ export const apiGetContext = async (statusId: string) => {
|
||||||
refresh: getAsyncRefreshHeader(response),
|
refresh: getAsyncRefreshHeader(response),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const apiSetQuotePolicy = async (
|
||||||
|
statusId: string,
|
||||||
|
policy: ApiQuotePolicy,
|
||||||
|
) => {
|
||||||
|
return apiRequestPut<ApiStatusJSON>(
|
||||||
|
`v1/statuses/${statusId}/interaction_policy`,
|
||||||
|
{
|
||||||
|
quote_approval_policy: policy,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { ApiStatusJSON } from './statuses';
|
import type { ApiStatusJSON } from './statuses';
|
||||||
|
|
||||||
export type ApiQuoteState = 'accepted' | 'pending' | 'revoked' | 'unauthorized';
|
export type ApiQuoteState = 'accepted' | 'pending' | 'revoked' | 'unauthorized';
|
||||||
export type ApiQuotePolicy = 'public' | 'followers' | 'nobody';
|
export type ApiQuotePolicy = 'public' | 'followers' | 'nobody' | 'unknown';
|
||||||
|
|
||||||
interface ApiQuoteEmptyJSON {
|
interface ApiQuoteEmptyJSON {
|
||||||
state: Exclude<ApiQuoteState, 'accepted'>;
|
state: Exclude<ApiQuoteState, 'accepted'>;
|
||||||
|
@ -21,3 +21,13 @@ interface ApiQuoteAcceptedJSON {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiQuoteJSON = ApiQuoteAcceptedJSON | ApiQuoteEmptyJSON;
|
export type ApiQuoteJSON = ApiQuoteAcceptedJSON | ApiQuoteEmptyJSON;
|
||||||
|
|
||||||
|
export interface ApiQuotePolicyJSON {
|
||||||
|
automatic: ApiQuotePolicy[];
|
||||||
|
manual: ApiQuotePolicy[];
|
||||||
|
current_user: ApiQuotePolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isQuotePolicy(policy: string): policy is ApiQuotePolicy {
|
||||||
|
return ['public', 'followers', 'nobody'].includes(policy);
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { ApiAccountJSON } from './accounts';
|
||||||
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
||||||
import type { ApiMediaAttachmentJSON } from './media_attachments';
|
import type { ApiMediaAttachmentJSON } from './media_attachments';
|
||||||
import type { ApiPollJSON } from './polls';
|
import type { ApiPollJSON } from './polls';
|
||||||
import type { ApiQuoteJSON } from './quotes';
|
import type { ApiQuoteJSON, ApiQuotePolicyJSON } from './quotes';
|
||||||
|
|
||||||
// See app/modals/status.rb
|
// See app/modals/status.rb
|
||||||
export type StatusVisibility =
|
export type StatusVisibility =
|
||||||
|
@ -120,9 +120,16 @@ export interface ApiStatusJSON {
|
||||||
card?: ApiPreviewCardJSON;
|
card?: ApiPreviewCardJSON;
|
||||||
poll?: ApiPollJSON;
|
poll?: ApiPollJSON;
|
||||||
quote?: ApiQuoteJSON;
|
quote?: ApiQuoteJSON;
|
||||||
|
quote_approval?: ApiQuotePolicyJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiContextJSON {
|
export interface ApiContextJSON {
|
||||||
ancestors: ApiStatusJSON[];
|
ancestors: ApiStatusJSON[];
|
||||||
descendants: ApiStatusJSON[];
|
descendants: ApiStatusJSON[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiStatusSourceJSON {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
spoiler_text: string;
|
||||||
|
}
|
||||||
|
|
114
app/javascript/mastodon/components/dropdown/index.tsx
Normal file
114
app/javascript/mastodon/components/dropdown/index.tsx
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import { useCallback, useId, useMemo, useRef, useState } from 'react';
|
||||||
|
import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import type { MessageDescriptor } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
|
import type { SelectItem } from '../dropdown_selector';
|
||||||
|
import { DropdownSelector } from '../dropdown_selector';
|
||||||
|
|
||||||
|
interface DropdownProps {
|
||||||
|
title: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
items: SelectItem[];
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
current: string;
|
||||||
|
emptyText?: MessageDescriptor;
|
||||||
|
classPrefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dropdown: FC<
|
||||||
|
DropdownProps & Omit<ComponentPropsWithoutRef<'button'>, keyof DropdownProps>
|
||||||
|
> = ({
|
||||||
|
title,
|
||||||
|
disabled,
|
||||||
|
items,
|
||||||
|
current,
|
||||||
|
onChange,
|
||||||
|
classPrefix,
|
||||||
|
className,
|
||||||
|
...buttonProps
|
||||||
|
}) => {
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const accessibilityId = useId();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const handleToggle = useCallback(() => {
|
||||||
|
if (!disabled) {
|
||||||
|
setOpen((prevOpen) => !prevOpen);
|
||||||
|
}
|
||||||
|
}, [disabled]);
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, []);
|
||||||
|
const currentText = useMemo(
|
||||||
|
() => items.find((i) => i.value === current)?.text,
|
||||||
|
[current, items],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
{...buttonProps}
|
||||||
|
title={title}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls={accessibilityId}
|
||||||
|
onClick={handleToggle}
|
||||||
|
disabled={disabled}
|
||||||
|
className={classNames(
|
||||||
|
`${classPrefix}__button`,
|
||||||
|
{
|
||||||
|
active: open,
|
||||||
|
disabled,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={buttonRef}
|
||||||
|
>
|
||||||
|
{currentText ?? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='dropdown.empty'
|
||||||
|
defaultMessage='Select an option'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Overlay
|
||||||
|
show={open}
|
||||||
|
offset={[0, 4]}
|
||||||
|
placement='bottom-start'
|
||||||
|
onHide={handleClose}
|
||||||
|
flip
|
||||||
|
target={buttonRef.current}
|
||||||
|
popperConfig={{
|
||||||
|
strategy: 'fixed',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ props, placement }) => (
|
||||||
|
<div {...props} className={`${classPrefix}__overlay`}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'dropdown-animation',
|
||||||
|
`${classPrefix}__dropdown`,
|
||||||
|
placement,
|
||||||
|
)}
|
||||||
|
id={accessibilityId}
|
||||||
|
>
|
||||||
|
<DropdownSelector
|
||||||
|
items={items}
|
||||||
|
value={current}
|
||||||
|
onClose={handleClose}
|
||||||
|
onChange={onChange}
|
||||||
|
classNamePrefix={classPrefix}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Overlay>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -13,8 +13,8 @@ const listenerOptions = supportsPassiveEvents
|
||||||
? { passive: true, capture: true }
|
? { passive: true, capture: true }
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
export interface SelectItem {
|
export interface SelectItem<Value extends string = string> {
|
||||||
value: string;
|
value: Value;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
iconComponent?: IconProp;
|
iconComponent?: IconProp;
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -24,7 +24,7 @@ export interface SelectItem {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
classNamePrefix: string;
|
classNamePrefix?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
items: SelectItem[];
|
items: SelectItem[];
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||||
import { me } from '../initial_state';
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
import { IconButton } from './icon_button';
|
import { IconButton } from './icon_button';
|
||||||
|
import { isFeatureEnabled } from '../utils/environment';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
|
@ -68,6 +69,7 @@ const messages = defineMessages({
|
||||||
filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
|
filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
|
||||||
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
||||||
revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}’s post' },
|
revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}’s post' },
|
||||||
|
quotePolicyChange: { id: 'status.quote_policy_change', defaultMessage: 'Change who can quote' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, { status }) => {
|
const mapStateToProps = (state, { status }) => {
|
||||||
|
@ -89,6 +91,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
onReblog: PropTypes.func,
|
onReblog: PropTypes.func,
|
||||||
onDelete: PropTypes.func,
|
onDelete: PropTypes.func,
|
||||||
onRevokeQuote: PropTypes.func,
|
onRevokeQuote: PropTypes.func,
|
||||||
|
onQuotePolicyChange: PropTypes.func,
|
||||||
onDirect: PropTypes.func,
|
onDirect: PropTypes.func,
|
||||||
onMention: PropTypes.func,
|
onMention: PropTypes.func,
|
||||||
onMute: PropTypes.func,
|
onMute: PropTypes.func,
|
||||||
|
@ -200,7 +203,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleRevokeQuoteClick = () => {
|
handleRevokeQuoteClick = () => {
|
||||||
this.props.onRevokeQuote(this.props.status);
|
this.props.onRevokeQuote(this.props.status);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
handleQuotePolicyChange = () => {
|
||||||
|
this.props.onQuotePolicyChange(this.props.status);
|
||||||
|
};
|
||||||
|
|
||||||
handleBlockClick = () => {
|
handleBlockClick = () => {
|
||||||
const { status, relationship, onBlock, onUnblock } = this.props;
|
const { status, relationship, onBlock, onUnblock } = this.props;
|
||||||
|
@ -291,6 +298,9 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
if (writtenByMe || withDismiss) {
|
if (writtenByMe || withDismiss) {
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
|
if (writtenByMe && isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange });
|
||||||
|
}
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,10 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
|
||||||
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
|
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onQuotePolicyChange(status) {
|
||||||
|
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId: status.get('id') } }));
|
||||||
|
},
|
||||||
|
|
||||||
onEdit (status) {
|
onEdit (status) {
|
||||||
dispatch((_, getState) => {
|
dispatch((_, getState) => {
|
||||||
let state = getState();
|
let state = getState();
|
||||||
|
|
|
@ -14,7 +14,7 @@ import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
|
||||||
import { DropdownSelector } from 'mastodon/components/dropdown_selector';
|
import { DropdownSelector } from 'mastodon/components/dropdown_selector';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
|
public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
|
||||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' },
|
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' },
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/
|
||||||
import { IconButton } from '../../../components/icon_button';
|
import { IconButton } from '../../../components/icon_button';
|
||||||
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||||
import { me } from '../../../initial_state';
|
import { me } from '../../../initial_state';
|
||||||
|
import { isFeatureEnabled } from '@/mastodon/utils/environment';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
|
@ -62,6 +63,7 @@ const messages = defineMessages({
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
||||||
revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}’s post' },
|
revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}’s post' },
|
||||||
|
quotePolicyChange: { id: 'status.quote_policy_change', defaultMessage: 'Change who can quote' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, { status }) => {
|
const mapStateToProps = (state, { status }) => {
|
||||||
|
@ -84,6 +86,7 @@ class ActionBar extends PureComponent {
|
||||||
onBookmark: PropTypes.func.isRequired,
|
onBookmark: PropTypes.func.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
onRevokeQuote: PropTypes.func,
|
onRevokeQuote: PropTypes.func,
|
||||||
|
onQuotePolicyChange: PropTypes.func,
|
||||||
onEdit: PropTypes.func.isRequired,
|
onEdit: PropTypes.func.isRequired,
|
||||||
onDirect: PropTypes.func.isRequired,
|
onDirect: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
|
@ -122,7 +125,11 @@ class ActionBar extends PureComponent {
|
||||||
|
|
||||||
handleRevokeQuoteClick = () => {
|
handleRevokeQuoteClick = () => {
|
||||||
this.props.onRevokeQuote(this.props.status);
|
this.props.onRevokeQuote(this.props.status);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
handleQuotePolicyChange = () => {
|
||||||
|
this.props.onQuotePolicyChange(this.props.status);
|
||||||
|
};
|
||||||
|
|
||||||
handleRedraftClick = () => {
|
handleRedraftClick = () => {
|
||||||
this.props.onDelete(this.props.status, true);
|
this.props.onDelete(this.props.status, true);
|
||||||
|
@ -240,6 +247,9 @@ class ActionBar extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
|
if (isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange });
|
||||||
|
}
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
|
||||||
|
|
|
@ -265,6 +265,11 @@ class Status extends ImmutablePureComponent {
|
||||||
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
|
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleQuotePolicyChange = (status) => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId: status.get('id') } }));
|
||||||
|
};
|
||||||
|
|
||||||
handleEditClick = (status) => {
|
handleEditClick = (status) => {
|
||||||
const { dispatch, askReplyConfirmation } = this.props;
|
const { dispatch, askReplyConfirmation } = this.props;
|
||||||
|
|
||||||
|
@ -642,6 +647,7 @@ class Status extends ImmutablePureComponent {
|
||||||
onBookmark={this.handleBookmarkClick}
|
onBookmark={this.handleBookmarkClick}
|
||||||
onDelete={this.handleDeleteClick}
|
onDelete={this.handleDeleteClick}
|
||||||
onRevokeQuote={this.handleRevokeQuoteClick}
|
onRevokeQuote={this.handleRevokeQuoteClick}
|
||||||
|
onQuotePolicyChange={this.handleQuotePolicyChange}
|
||||||
onEdit={this.handleEditClick}
|
onEdit={this.handleEditClick}
|
||||||
onDirect={this.handleDirectClick}
|
onDirect={this.handleDirectClick}
|
||||||
onMention={this.handleMentionClick}
|
onMention={this.handleMentionClick}
|
||||||
|
|
|
@ -43,6 +43,7 @@ import { ImageModal } from './image_modal';
|
||||||
import MediaModal from './media_modal';
|
import MediaModal from './media_modal';
|
||||||
import { ModalPlaceholder } from './modal_placeholder';
|
import { ModalPlaceholder } from './modal_placeholder';
|
||||||
import VideoModal from './video_modal';
|
import VideoModal from './video_modal';
|
||||||
|
import { VisibilityModal } from './visibility_modal';
|
||||||
|
|
||||||
export const MODAL_COMPONENTS = {
|
export const MODAL_COMPONENTS = {
|
||||||
'MEDIA': () => Promise.resolve({ default: MediaModal }),
|
'MEDIA': () => Promise.resolve({ default: MediaModal }),
|
||||||
|
@ -76,6 +77,7 @@ export const MODAL_COMPONENTS = {
|
||||||
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
|
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
|
||||||
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
|
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
|
||||||
'ANNUAL_REPORT': AnnualReportModal,
|
'ANNUAL_REPORT': AnnualReportModal,
|
||||||
|
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalRoot extends PureComponent {
|
export default class ModalRoot extends PureComponent {
|
||||||
|
|
|
@ -0,0 +1,293 @@
|
||||||
|
import { forwardRef, useCallback, useId, useMemo } from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { changeComposeVisibility } from '@/mastodon/actions/compose';
|
||||||
|
import { setStatusQuotePolicy } from '@/mastodon/actions/statuses_typed';
|
||||||
|
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
||||||
|
import { isQuotePolicy } from '@/mastodon/api_types/quotes';
|
||||||
|
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||||
|
import { Dropdown } from '@/mastodon/components/dropdown';
|
||||||
|
import type { SelectItem } from '@/mastodon/components/dropdown_selector';
|
||||||
|
import { IconButton } from '@/mastodon/components/icon_button';
|
||||||
|
import { messages as privacyMessages } from '@/mastodon/features/compose/components/privacy_dropdown';
|
||||||
|
import {
|
||||||
|
createAppSelector,
|
||||||
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
|
} from '@/mastodon/store';
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
|
||||||
|
import type { BaseConfirmationModalProps } from './confirmation_modals/confirmation_modal';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
|
buttonTitle: {
|
||||||
|
id: 'visibility_modal.button_title',
|
||||||
|
defaultMessage: 'Set visibility',
|
||||||
|
},
|
||||||
|
quotePublic: {
|
||||||
|
id: 'visibility_modal.quote_public',
|
||||||
|
defaultMessage: 'Anyone',
|
||||||
|
},
|
||||||
|
quoteFollowers: {
|
||||||
|
id: 'visibility_modal.quote_followers',
|
||||||
|
defaultMessage: 'Followers only',
|
||||||
|
},
|
||||||
|
quoteNobody: {
|
||||||
|
id: 'visibility_modal.quote_nobody',
|
||||||
|
defaultMessage: 'No one',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface VisibilityModalProps extends BaseConfirmationModalProps {
|
||||||
|
statusId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectStatusPolicy = createAppSelector(
|
||||||
|
[(state) => state.statuses, (_state, statusId: string) => statusId],
|
||||||
|
(statuses, statusId) => {
|
||||||
|
const status = statuses.get(statusId);
|
||||||
|
if (!status) {
|
||||||
|
return 'public';
|
||||||
|
}
|
||||||
|
const policy =
|
||||||
|
(status.getIn(['quote_approval', 'automatic', 0]) as string) || 'nobody';
|
||||||
|
const visibility = status.get('visibility') as StatusVisibility;
|
||||||
|
|
||||||
|
// If the status is private or direct, it cannot be quoted by anyone.
|
||||||
|
if (visibility === 'private' || visibility === 'direct') {
|
||||||
|
return 'nobody';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the status has a specific quote policy, return it.
|
||||||
|
if (isQuotePolicy(policy)) {
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, return the default based on visibility.
|
||||||
|
if (visibility === 'unlisted') {
|
||||||
|
return 'followers';
|
||||||
|
}
|
||||||
|
return 'public';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
({ onClose, statusId }, ref) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const currentVisibility = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
(state.statuses.getIn([statusId, 'visibility'], 'public') as
|
||||||
|
| StatusVisibility
|
||||||
|
| undefined) ?? 'public',
|
||||||
|
);
|
||||||
|
const currentQuotePolicy = useAppSelector((state) =>
|
||||||
|
selectStatusPolicy(state, statusId),
|
||||||
|
);
|
||||||
|
const disableQuotePolicy =
|
||||||
|
currentVisibility === 'private' || currentVisibility === 'direct';
|
||||||
|
const isSaving = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
state.statuses.getIn([statusId, 'isSavingQuotePolicy']) === true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
value: 'public',
|
||||||
|
text: intl.formatMessage(privacyMessages.public_short),
|
||||||
|
meta: intl.formatMessage(privacyMessages.public_long),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'unlisted',
|
||||||
|
text: intl.formatMessage(privacyMessages.unlisted_short),
|
||||||
|
meta: intl.formatMessage(privacyMessages.unlisted_long),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'private',
|
||||||
|
text: intl.formatMessage(privacyMessages.private_short),
|
||||||
|
meta: intl.formatMessage(privacyMessages.private_long),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'direct',
|
||||||
|
text: intl.formatMessage(privacyMessages.direct_short),
|
||||||
|
meta: intl.formatMessage(privacyMessages.direct_long),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[intl],
|
||||||
|
);
|
||||||
|
const quoteItems = useMemo<SelectItem<ApiQuotePolicy>[]>(
|
||||||
|
() => [
|
||||||
|
{ value: 'public', text: intl.formatMessage(messages.quotePublic) },
|
||||||
|
{
|
||||||
|
value: 'followers',
|
||||||
|
text: intl.formatMessage(messages.quoteFollowers),
|
||||||
|
},
|
||||||
|
{ value: 'nobody', text: intl.formatMessage(messages.quoteNobody) },
|
||||||
|
],
|
||||||
|
[intl],
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handleVisibilityChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
// Published statuses cannot change visibility.
|
||||||
|
if (statusId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(changeComposeVisibility(value));
|
||||||
|
},
|
||||||
|
[dispatch, statusId],
|
||||||
|
);
|
||||||
|
const handleQuotePolicyChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
if (isQuotePolicy(value) && !disableQuotePolicy) {
|
||||||
|
void dispatch(setStatusQuotePolicy({ policy: value, statusId }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disableQuotePolicy, dispatch, statusId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const privacyDropdownId = useId();
|
||||||
|
const quoteDropdownId = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal dialog-modal visibility-modal'>
|
||||||
|
<div className='dialog-modal__header'>
|
||||||
|
<IconButton
|
||||||
|
className='dialog-modal__header__close'
|
||||||
|
title={intl.formatMessage(messages.close)}
|
||||||
|
icon='times'
|
||||||
|
iconComponent={CloseIcon}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='visibility_modal.header'
|
||||||
|
defaultMessage='Visibility and interaction'
|
||||||
|
>
|
||||||
|
{(chunks) => (
|
||||||
|
<span className='dialog-modal__header__title'>{chunks}</span>
|
||||||
|
)}
|
||||||
|
</FormattedMessage>
|
||||||
|
</div>
|
||||||
|
<div className='dialog-modal__content'>
|
||||||
|
<div className='dialog-modal__content__description'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='visibility_modal.instructions'
|
||||||
|
defaultMessage='Control who can interact with this post. Global settings can be found under <link>Preferences > Other</link>.'
|
||||||
|
values={{
|
||||||
|
link: (chunks) => (
|
||||||
|
<a href='/settings/preferences/other'>{chunks}</a>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
tagName='p'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='dialog-modal__content__form'>
|
||||||
|
<label
|
||||||
|
htmlFor={privacyDropdownId}
|
||||||
|
className={classNames('visibility-dropdown__label', {
|
||||||
|
disabled: isSaving || !!statusId,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='visibility_modal.privacy_label'
|
||||||
|
defaultMessage='Privacy'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
items={visibilityItems}
|
||||||
|
classPrefix='visibility-dropdown'
|
||||||
|
current={currentVisibility}
|
||||||
|
onChange={handleVisibilityChange}
|
||||||
|
title={intl.formatMessage(privacyMessages.change_privacy)}
|
||||||
|
disabled={isSaving || !!statusId}
|
||||||
|
id={privacyDropdownId}
|
||||||
|
/>
|
||||||
|
{!!statusId && (
|
||||||
|
<p className='visibility-dropdown__helper'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='visibility_modal.helper.privacy_editing'
|
||||||
|
defaultMessage="Visibility can't be changed after a post is published."
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
htmlFor={quoteDropdownId}
|
||||||
|
className={classNames('visibility-dropdown__label', {
|
||||||
|
disabled: disableQuotePolicy || isSaving,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='visibility_modal.quote_label'
|
||||||
|
defaultMessage='Change who can quote'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
items={quoteItems}
|
||||||
|
onChange={handleQuotePolicyChange}
|
||||||
|
classPrefix='visibility-dropdown'
|
||||||
|
current={currentQuotePolicy}
|
||||||
|
title={intl.formatMessage(messages.buttonTitle)}
|
||||||
|
disabled={disableQuotePolicy || isSaving}
|
||||||
|
id={quoteDropdownId}
|
||||||
|
/>
|
||||||
|
<QuotePolicyHelper
|
||||||
|
policy={currentQuotePolicy}
|
||||||
|
visibility={currentVisibility}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
VisibilityModal.displayName = 'VisibilityModal';
|
||||||
|
|
||||||
|
const QuotePolicyHelper: FC<{
|
||||||
|
policy: ApiQuotePolicy;
|
||||||
|
visibility: StatusVisibility;
|
||||||
|
}> = ({ policy, visibility }) => {
|
||||||
|
if (visibility === 'unlisted' && policy !== 'nobody') {
|
||||||
|
return (
|
||||||
|
<p className='visibility-dropdown__helper'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='visibility_modal.helper.unlisted_quoting'
|
||||||
|
defaultMessage='When people quote you, their post will also be hidden from trending timelines.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibility === 'private') {
|
||||||
|
return (
|
||||||
|
<p className='visibility-dropdown__helper'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='visibility_modal.helper.private_quoting'
|
||||||
|
defaultMessage="Follower-only posts can't be quoted."
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibility === 'direct') {
|
||||||
|
return (
|
||||||
|
<p className='visibility-dropdown__helper'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='visibility_modal.helper.direct_quoting'
|
||||||
|
defaultMessage="Private mentions can't be quoted."
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
|
@ -292,6 +292,7 @@
|
||||||
"domain_pill.your_handle": "Your handle:",
|
"domain_pill.your_handle": "Your handle:",
|
||||||
"domain_pill.your_server": "Your digital home, where all of your posts live. Don’t like this one? Transfer servers at any time and bring your followers, too.",
|
"domain_pill.your_server": "Your digital home, where all of your posts live. Don’t like this one? Transfer servers at any time and bring your followers, too.",
|
||||||
"domain_pill.your_username": "Your unique identifier on this server. It’s possible to find users with the same username on different servers.",
|
"domain_pill.your_username": "Your unique identifier on this server. It’s possible to find users with the same username on different servers.",
|
||||||
|
"dropdown.empty": "Select an option",
|
||||||
"embed.instructions": "Embed this post on your website by copying the code below.",
|
"embed.instructions": "Embed this post on your website by copying the code below.",
|
||||||
"embed.preview": "Here is what it will look like:",
|
"embed.preview": "Here is what it will look like:",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
|
@ -884,6 +885,7 @@
|
||||||
"status.quote_error.pending_approval": "Post pending",
|
"status.quote_error.pending_approval": "Post pending",
|
||||||
"status.quote_error.pending_approval_popout.body": "Quotes shared across the Fediverse may take time to display, as different servers have different protocols.",
|
"status.quote_error.pending_approval_popout.body": "Quotes shared across the Fediverse may take time to display, as different servers have different protocols.",
|
||||||
"status.quote_error.pending_approval_popout.title": "Pending quote? Remain calm",
|
"status.quote_error.pending_approval_popout.title": "Pending quote? Remain calm",
|
||||||
|
"status.quote_policy_change": "Change who can quote",
|
||||||
"status.quote_post_author": "Quoted a post by @{name}",
|
"status.quote_post_author": "Quoted a post by @{name}",
|
||||||
"status.read_more": "Read more",
|
"status.read_more": "Read more",
|
||||||
"status.reblog": "Boost",
|
"status.reblog": "Boost",
|
||||||
|
@ -959,5 +961,17 @@
|
||||||
"video.skip_forward": "Skip forward",
|
"video.skip_forward": "Skip forward",
|
||||||
"video.unmute": "Unmute",
|
"video.unmute": "Unmute",
|
||||||
"video.volume_down": "Volume down",
|
"video.volume_down": "Volume down",
|
||||||
"video.volume_up": "Volume up"
|
"video.volume_up": "Volume up",
|
||||||
|
"visibility_modal.button_title": "Set visibility",
|
||||||
|
"visibility_modal.header": "Visibility and interaction",
|
||||||
|
"visibility_modal.helper.direct_quoting": "Private mentions can't be quoted.",
|
||||||
|
"visibility_modal.helper.privacy_editing": "Published posts cannot change their visibility.",
|
||||||
|
"visibility_modal.helper.private_quoting": "Follower-only posts can't be quoted.",
|
||||||
|
"visibility_modal.helper.unlisted_quoting": "When people quote you, their post will also be hidden from trending timelines.",
|
||||||
|
"visibility_modal.instructions": "Control who can interact with this post. Global settings can be found under <link>Preferences > Other</link>.",
|
||||||
|
"visibility_modal.privacy_label": "Privacy",
|
||||||
|
"visibility_modal.quote_followers": "Followers only",
|
||||||
|
"visibility_modal.quote_label": "Change who can quote",
|
||||||
|
"visibility_modal.quote_nobody": "No one",
|
||||||
|
"visibility_modal.quote_public": "Anyone"
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
STATUS_FETCH_REQUEST,
|
STATUS_FETCH_REQUEST,
|
||||||
STATUS_FETCH_FAIL,
|
STATUS_FETCH_FAIL,
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
|
import { setStatusQuotePolicy } from '../actions/statuses_typed';
|
||||||
|
|
||||||
const importStatus = (state, status) => state.set(status.id, fromJS(status));
|
const importStatus = (state, status) => state.set(status.id, fromJS(status));
|
||||||
|
|
||||||
|
@ -70,6 +71,22 @@ const initialState = ImmutableMap();
|
||||||
|
|
||||||
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
|
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
|
||||||
export default function statuses(state = initialState, action) {
|
export default function statuses(state = initialState, action) {
|
||||||
|
if (setStatusQuotePolicy.pending.match(action)) {
|
||||||
|
const status = state.get(action.meta.arg.statusId);
|
||||||
|
if (status) {
|
||||||
|
return state.setIn([action.meta.arg.statusId, 'isSavingQuotePolicy'], true);
|
||||||
|
}
|
||||||
|
} else if (setStatusQuotePolicy.fulfilled.match(action)) {
|
||||||
|
const status = state.get(action.payload.id);
|
||||||
|
if (status) {
|
||||||
|
return state
|
||||||
|
.setIn([action.payload.id, 'quote_approval'], action.payload.quote_approval)
|
||||||
|
.deleteIn([action.payload.id, 'isSavingQuotePolicy']);
|
||||||
|
}
|
||||||
|
} else if (setStatusQuotePolicy.rejected.match(action)) {
|
||||||
|
return state.deleteIn([action.meta.arg.statusId, 'isSavingQuotePolicy']);
|
||||||
|
}
|
||||||
|
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case STATUS_FETCH_REQUEST:
|
case STATUS_FETCH_REQUEST:
|
||||||
return state.setIn([action.id, 'isLoading'], true);
|
return state.setIn([action.id, 'isLoading'], true);
|
||||||
|
|
|
@ -40,7 +40,10 @@ interface AppThunkConfig {
|
||||||
fulfilledMeta: AppMeta;
|
fulfilledMeta: AppMeta;
|
||||||
rejectedMeta: AppMeta;
|
rejectedMeta: AppMeta;
|
||||||
}
|
}
|
||||||
type AppThunkApi = Pick<GetThunkAPI<AppThunkConfig>, 'getState' | 'dispatch'>;
|
export type AppThunkApi = Pick<
|
||||||
|
GetThunkAPI<AppThunkConfig>,
|
||||||
|
'getState' | 'dispatch'
|
||||||
|
>;
|
||||||
|
|
||||||
interface AppThunkOptions<Arg> {
|
interface AppThunkOptions<Arg> {
|
||||||
useLoadingBar?: boolean;
|
useLoadingBar?: boolean;
|
||||||
|
|
|
@ -12,7 +12,11 @@ export function isProduction() {
|
||||||
else return import.meta.env.PROD;
|
else return import.meta.env.PROD;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Features = 'modern_emojis';
|
export type Features =
|
||||||
|
| 'modern_emojis'
|
||||||
|
| 'outgoing_quotes'
|
||||||
|
| 'fasp'
|
||||||
|
| 'http_message_signatures';
|
||||||
|
|
||||||
export function isFeatureEnabled(feature: Features) {
|
export function isFeatureEnabled(feature: Features) {
|
||||||
return initialState?.features.includes(feature) ?? false;
|
return initialState?.features.includes(feature) ?? false;
|
||||||
|
|
|
@ -5402,7 +5402,8 @@ a.status-card {
|
||||||
}
|
}
|
||||||
|
|
||||||
.privacy-dropdown__dropdown,
|
.privacy-dropdown__dropdown,
|
||||||
.language-dropdown__dropdown {
|
.language-dropdown__dropdown,
|
||||||
|
.visibility-dropdown__dropdown {
|
||||||
box-shadow: var(--dropdown-shadow);
|
box-shadow: var(--dropdown-shadow);
|
||||||
background: var(--dropdown-background-color);
|
background: var(--dropdown-background-color);
|
||||||
backdrop-filter: $backdrop-blur-filter;
|
backdrop-filter: $backdrop-blur-filter;
|
||||||
|
@ -5431,7 +5432,8 @@ a.status-card {
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.privacy-dropdown__option {
|
.privacy-dropdown__option,
|
||||||
|
.visibility-dropdown__option {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
letter-spacing: 0.25px;
|
letter-spacing: 0.25px;
|
||||||
|
@ -5577,6 +5579,39 @@ a.status-card {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visibility-dropdown {
|
||||||
|
&__overlay[data-popper-placement] {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label.disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
color: $primary-text-color;
|
||||||
|
background: var(--dropdown-background-color);
|
||||||
|
border: 1px solid var(--dropdown-border-color);
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__helper {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: $dark-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -5869,6 +5904,17 @@ a.status-card {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-root label {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.video-modal .video-player {
|
.video-modal .video-player {
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
|
@ -6375,6 +6421,15 @@ a.status-card {
|
||||||
letter-spacing: 0.25px;
|
letter-spacing: 0.25px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
margin: 24px 24px 0;
|
||||||
|
color: $darker-text-color;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__form {
|
&__form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user