state.notificationGroups.groups,
- ],
- (showFilterBar, allowedType, excludedTypes, notifications) => {
- if (!showFilterBar || allowedType === 'all') {
- // used if user changed the notification settings after loading the notifications from the server
- // otherwise a list of notifications will come pre-filtered from the backend
- // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
- return notifications.filter(
- (item) => item.type === 'gap' || !excludedTypes.includes(item.type),
- );
- }
- return notifications.filter(
- (item) => item.type === 'gap' || allowedType === item.type,
- );
- },
-);
-
export const Notifications: React.FC<{
columnId?: string;
multiColumn?: boolean;
}> = ({ columnId, multiColumn }) => {
const intl = useIntl();
- const notifications = useAppSelector(getNotifications);
+ const notifications = useAppSelector(selectNotificationGroups);
const dispatch = useAppDispatch();
const isLoading = useAppSelector((s) => s.notificationGroups.isLoading);
const hasMore = notifications.at(-1)?.type === 'gap';
const lastReadId = useAppSelector((s) =>
selectSettingsNotificationsShowUnread(s)
- ? s.notificationGroups.lastReadId
+ ? s.notificationGroups.readMarkerId
: '0',
);
@@ -102,11 +79,17 @@ export const Notifications: React.FC<{
selectUnreadNotificationGroupsCount,
);
- const isUnread = unreadNotificationsCount > 0;
+ const anyPendingNotification = useAppSelector(selectAnyPendingNotification);
+
+ const needsReload = useAppSelector(
+ (state) => state.notificationGroups.mergedNotifications === 'needs-reload',
+ );
+
+ const isUnread = unreadNotificationsCount > 0 || needsReload;
const canMarkAsRead =
useAppSelector(selectSettingsNotificationsShowUnread) &&
- unreadNotificationsCount > 0;
+ anyPendingNotification;
const needsNotificationPermission = useAppSelector(
selectNeedsNotificationPermission,
@@ -139,11 +122,11 @@ export const Notifications: React.FC<{
// Keep track of mounted components for unread notification handling
useEffect(() => {
- dispatch(mountNotifications());
+ void dispatch(mountNotifications());
return () => {
dispatch(unmountNotifications());
- dispatch(updateScrollPosition({ top: false }));
+ void dispatch(updateScrollPosition({ top: false }));
};
}, [dispatch]);
@@ -168,11 +151,11 @@ export const Notifications: React.FC<{
}, [dispatch]);
const handleScrollToTop = useDebouncedCallback(() => {
- dispatch(updateScrollPosition({ top: true }));
+ void dispatch(updateScrollPosition({ top: true }));
}, 100);
const handleScroll = useDebouncedCallback(() => {
- dispatch(updateScrollPosition({ top: false }));
+ void dispatch(updateScrollPosition({ top: false }));
}, 100);
useEffect(() => {
@@ -306,16 +289,21 @@ export const Notifications: React.FC<{
);
- const extraButton = canMarkAsRead ? (
-
- ) : null;
+ const extraButton = (
+ <>
+
+ {canMarkAsRead && (
+
+ )}
+ >
+ );
return (
{
- const { dispatch, askReplyConfirmation, status, intl } = this.props;
+ const { dispatch, askReplyConfirmation, status, onClose } = this.props;
const { signedIn } = this.props.identity;
if (signedIn) {
if (askReplyConfirmation) {
- dispatch(openModal({
- modalType: 'CONFIRM',
- modalProps: {
- message: intl.formatMessage(messages.replyMessage),
- confirm: intl.formatMessage(messages.replyConfirm),
- onConfirm: this._performReply,
- },
- }));
+ onClose(true);
+ dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
} else {
this._performReply();
}
diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
index 1c9e3ccce1..34bde2fc6e 100644
--- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
+++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
@@ -1,4 +1,4 @@
-import { defineMessages, injectIntl } from 'react-intl';
+import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
@@ -27,15 +27,6 @@ import { deleteModal } from '../../../initial_state';
import { makeGetStatus } from '../../../selectors';
import DetailedStatus from '../components/detailed_status';
-const messages = defineMessages({
- deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
- deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
- redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
- redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
- replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
- replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
-});
-
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
@@ -48,20 +39,13 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
-const mapDispatchToProps = (dispatch, { intl }) => ({
+const mapDispatchToProps = (dispatch) => ({
onReply (status) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
- dispatch(openModal({
- modalType: 'CONFIRM',
- modalProps: {
- message: intl.formatMessage(messages.replyMessage),
- confirm: intl.formatMessage(messages.replyConfirm),
- onConfirm: () => dispatch(replyCompose(status)),
- },
- }));
+ dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
} else {
dispatch(replyCompose(status));
}
@@ -98,14 +82,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), withRedraft));
} else {
- dispatch(openModal({
- modalType: 'CONFIRM',
- modalProps: {
- message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
- confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
- onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)),
- },
- }));
+ dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
}
},
diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx
index 218f953617..90920513a0 100644
--- a/app/javascript/flavours/glitch/features/status/index.jsx
+++ b/app/javascript/flavours/glitch/features/status/index.jsx
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
@@ -19,6 +19,7 @@ import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
+import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
@@ -41,7 +42,6 @@ import {
addReaction,
removeReaction,
} from '../../actions/interactions';
-import { changeLocalSetting } from '../../actions/local_settings';
import { openModal } from '../../actions/modal';
import { initMuteModal } from '../../actions/mutes';
import { initReport } from '../../actions/reports';
@@ -69,16 +69,10 @@ import DetailedStatus from './components/detailed_status';
const messages = defineMessages({
- deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
- deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
- redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
- redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
- replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
- replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
tootHeading: { id: 'account.posts_with_replies', defaultMessage: 'Posts and replies' },
});
@@ -309,20 +303,12 @@ class Status extends ImmutablePureComponent {
};
handleReplyClick = (status) => {
- const { askReplyConfirmation, dispatch, intl } = this.props;
+ const { askReplyConfirmation, dispatch } = this.props;
const { signedIn } = this.props.identity;
if (signedIn) {
if (askReplyConfirmation) {
- dispatch(openModal({
- modalType: 'CONFIRM',
- modalProps: {
- message: intl.formatMessage(messages.replyMessage),
- confirm: intl.formatMessage(messages.replyConfirm),
- onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)),
- onConfirm: () => dispatch(replyCompose(status)),
- },
- }));
+ dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
} else {
dispatch(replyCompose(status));
}
@@ -365,24 +351,23 @@ class Status extends ImmutablePureComponent {
};
handleDeleteClick = (status, withRedraft = false) => {
- const { dispatch, intl } = this.props;
+ const { dispatch } = this.props;
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), withRedraft));
} else {
- dispatch(openModal({
- modalType: 'CONFIRM',
- modalProps: {
- message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
- confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
- onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)),
- },
- }));
+ dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
}
};
handleEditClick = (status) => {
- this.props.dispatch(editStatus(status.get('id')));
+ const { dispatch, askReplyConfirmation } = this.props;
+
+ if (askReplyConfirmation) {
+ dispatch(openModal({ modalType: 'CONFIRM_EDIT_STATUS', modalProps: { statusId: status.get('id') } }));
+ } else {
+ dispatch(editStatus(status.get('id')));
+ }
};
handleDirectClick = (account) => {
@@ -653,7 +638,7 @@ class Status extends ImmutablePureComponent {
};
render () {
- let ancestors, descendants;
+ let ancestors, descendants, remoteHint;
const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen } = this.state;
@@ -684,6 +669,17 @@ class Status extends ImmutablePureComponent {
const isLocal = status.getIn(['account', 'acct'], '').indexOf('@') === -1;
const isIndexable = !status.getIn(['account', 'noindex']);
+ if (!isLocal) {
+ remoteHint = (
+ }
+ label={{status.getIn(['account', 'acct']).split('@')[1]} }} />}
+ />
+ );
+ }
+
const handlers = {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
@@ -758,6 +754,7 @@ class Status extends ImmutablePureComponent {
{descendants}
+ {remoteHint}
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.jsx
deleted file mode 100644
index 51a501595b..0000000000
--- a/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.jsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { injectIntl, FormattedMessage } from 'react-intl';
-
-import { Button } from '../../../components/button';
-
-class ConfirmationModal extends PureComponent {
-
- static propTypes = {
- message: PropTypes.node.isRequired,
- confirm: PropTypes.string.isRequired,
- onClose: PropTypes.func.isRequired,
- onConfirm: PropTypes.func.isRequired,
- secondary: PropTypes.string,
- onSecondary: PropTypes.func,
- closeWhenConfirm: PropTypes.bool,
- onDoNotAsk: PropTypes.func,
- intl: PropTypes.object.isRequired,
- };
-
- static defaultProps = {
- closeWhenConfirm: true,
- };
-
- handleClick = () => {
- if (this.props.closeWhenConfirm) {
- this.props.onClose();
- }
- this.props.onConfirm();
- if (this.props.onDoNotAsk && this.doNotAskCheckbox.checked) {
- this.props.onDoNotAsk();
- }
- };
-
- handleSecondary = () => {
- this.props.onClose();
- this.props.onSecondary();
- };
-
- handleCancel = () => {
- this.props.onClose();
- };
-
- setDoNotAskRef = (c) => {
- this.doNotAskCheckbox = c;
- };
-
- render () {
- const { message, confirm, secondary, onDoNotAsk } = this.props;
-
- return (
-
-
- {message}
-
-
-
- { onDoNotAsk && (
-
-
-
-
- )}
-
-
- {secondary !== undefined && (
-
- )}
-
-
-
-
- );
- }
-
-}
-
-export default injectIntl(ConfirmationModal);
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/clear_notifications.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/clear_notifications.tsx
new file mode 100644
index 0000000000..6560446289
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/clear_notifications.tsx
@@ -0,0 +1,46 @@
+import { useCallback } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { clearNotifications } from 'flavours/glitch/actions/notification_groups';
+import { useAppDispatch } from 'flavours/glitch/store';
+
+import type { BaseConfirmationModalProps } from './confirmation_modal';
+import { ConfirmationModal } from './confirmation_modal';
+
+const messages = defineMessages({
+ clearTitle: {
+ id: 'notifications.clear_title',
+ defaultMessage: 'Clear notifications?',
+ },
+ clearMessage: {
+ id: 'notifications.clear_confirmation',
+ defaultMessage:
+ 'Are you sure you want to permanently clear all your notifications?',
+ },
+ clearConfirm: {
+ id: 'notifications.clear',
+ defaultMessage: 'Clear notifications',
+ },
+});
+
+export const ConfirmClearNotificationsModal: React.FC<
+ BaseConfirmationModalProps
+> = ({ onClose }) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+
+ const onConfirm = useCallback(() => {
+ void dispatch(clearNotifications());
+ }, [dispatch]);
+
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/confirmation_modal.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/confirmation_modal.tsx
new file mode 100644
index 0000000000..429850ef32
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/confirmation_modal.tsx
@@ -0,0 +1,87 @@
+import { useCallback } from 'react';
+
+import { FormattedMessage, defineMessages } from 'react-intl';
+
+import { Button } from 'flavours/glitch/components/button';
+
+export interface BaseConfirmationModalProps {
+ onClose: () => void;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars -- keep the message around while we find a place to show it
+const messages = defineMessages({
+ doNotAskAgain: {
+ id: 'confirmation_modal.do_not_ask_again',
+ defaultMessage: 'Do not ask for confirmation again',
+ },
+});
+
+export const ConfirmationModal: React.FC<
+ {
+ title: React.ReactNode;
+ message: React.ReactNode;
+ confirm: React.ReactNode;
+ secondary?: React.ReactNode;
+ onSecondary?: () => void;
+ onConfirm: () => void;
+ closeWhenConfirm?: boolean;
+ } & BaseConfirmationModalProps
+> = ({
+ title,
+ message,
+ confirm,
+ onClose,
+ onConfirm,
+ secondary,
+ onSecondary,
+ closeWhenConfirm = true,
+}) => {
+ const handleClick = useCallback(() => {
+ if (closeWhenConfirm) {
+ onClose();
+ }
+
+ onConfirm();
+ }, [onClose, onConfirm, closeWhenConfirm]);
+
+ const handleSecondary = useCallback(() => {
+ onClose();
+ onSecondary?.();
+ }, [onClose, onSecondary]);
+
+ const handleCancel = useCallback(() => {
+ onClose();
+ }, [onClose]);
+
+ return (
+
+
+
+
+
+ {secondary && (
+ <>
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/delete_list.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/delete_list.tsx
new file mode 100644
index 0000000000..948b6c83da
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/delete_list.tsx
@@ -0,0 +1,58 @@
+import { useCallback } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { useHistory } from 'react-router';
+
+import { removeColumn } from 'flavours/glitch/actions/columns';
+import { deleteList } from 'flavours/glitch/actions/lists';
+import { useAppDispatch } from 'flavours/glitch/store';
+
+import type { BaseConfirmationModalProps } from './confirmation_modal';
+import { ConfirmationModal } from './confirmation_modal';
+
+const messages = defineMessages({
+ deleteListTitle: {
+ id: 'confirmations.delete_list.title',
+ defaultMessage: 'Delete list?',
+ },
+ deleteListMessage: {
+ id: 'confirmations.delete_list.message',
+ defaultMessage: 'Are you sure you want to permanently delete this list?',
+ },
+ deleteListConfirm: {
+ id: 'confirmations.delete_list.confirm',
+ defaultMessage: 'Delete',
+ },
+});
+
+export const ConfirmDeleteListModal: React.FC<
+ {
+ listId: string;
+ columnId: string;
+ } & BaseConfirmationModalProps
+> = ({ listId, columnId, onClose }) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+ const history = useHistory();
+
+ const onConfirm = useCallback(() => {
+ dispatch(deleteList(listId));
+
+ if (columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ history.push('/lists');
+ }
+ }, [dispatch, history, columnId, listId]);
+
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/delete_status.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/delete_status.tsx
new file mode 100644
index 0000000000..e86cec10c0
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/delete_status.tsx
@@ -0,0 +1,67 @@
+import { useCallback } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { deleteStatus } from 'flavours/glitch/actions/statuses';
+import { useAppDispatch } from 'flavours/glitch/store';
+
+import type { BaseConfirmationModalProps } from './confirmation_modal';
+import { ConfirmationModal } from './confirmation_modal';
+
+const messages = defineMessages({
+ deleteAndRedraftTitle: {
+ id: 'confirmations.redraft.title',
+ defaultMessage: 'Delete & redraft post?',
+ },
+ deleteAndRedraftMessage: {
+ id: 'confirmations.redraft.message',
+ defaultMessage:
+ 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.',
+ },
+ deleteAndRedraftConfirm: {
+ id: 'confirmations.redraft.confirm',
+ defaultMessage: 'Delete & redraft',
+ },
+ deleteTitle: {
+ id: 'confirmations.delete.title',
+ defaultMessage: 'Delete post?',
+ },
+ deleteMessage: {
+ id: 'confirmations.delete.message',
+ defaultMessage: 'Are you sure you want to delete this status?',
+ },
+ deleteConfirm: {
+ id: 'confirmations.delete.confirm',
+ defaultMessage: 'Delete',
+ },
+});
+
+export const ConfirmDeleteStatusModal: React.FC<
+ {
+ statusId: string;
+ withRedraft: boolean;
+ } & BaseConfirmationModalProps
+> = ({ statusId, withRedraft, onClose }) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+
+ const onConfirm = useCallback(() => {
+ dispatch(deleteStatus(statusId, withRedraft));
+ }, [dispatch, statusId, withRedraft]);
+
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/edit_status.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/edit_status.tsx
new file mode 100644
index 0000000000..4a7e56c2a1
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/edit_status.tsx
@@ -0,0 +1,45 @@
+import { useCallback } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { editStatus } from 'flavours/glitch/actions/statuses';
+import { useAppDispatch } from 'flavours/glitch/store';
+
+import type { BaseConfirmationModalProps } from './confirmation_modal';
+import { ConfirmationModal } from './confirmation_modal';
+
+const messages = defineMessages({
+ editTitle: {
+ id: 'confirmations.edit.title',
+ defaultMessage: 'Overwrite post?',
+ },
+ editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
+ editMessage: {
+ id: 'confirmations.edit.message',
+ defaultMessage:
+ 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?',
+ },
+});
+
+export const ConfirmEditStatusModal: React.FC<
+ {
+ statusId: string;
+ } & BaseConfirmationModalProps
+> = ({ statusId, onClose }) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+
+ const onConfirm = useCallback(() => {
+ dispatch(editStatus(statusId));
+ }, [dispatch, statusId]);
+
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/index.ts b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/index.ts
new file mode 100644
index 0000000000..912c99a393
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/index.ts
@@ -0,0 +1,8 @@
+export { ConfirmationModal } from './confirmation_modal';
+export { ConfirmDeleteStatusModal } from './delete_status';
+export { ConfirmDeleteListModal } from './delete_list';
+export { ConfirmReplyModal } from './reply';
+export { ConfirmEditStatusModal } from './edit_status';
+export { ConfirmUnfollowModal } from './unfollow';
+export { ConfirmClearNotificationsModal } from './clear_notifications';
+export { ConfirmLogOutModal } from './log_out';
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/log_out.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/log_out.tsx
new file mode 100644
index 0000000000..9c90e0f8b9
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/log_out.tsx
@@ -0,0 +1,40 @@
+import { useCallback } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { logOut } from 'flavours/glitch/utils/log_out';
+
+import type { BaseConfirmationModalProps } from './confirmation_modal';
+import { ConfirmationModal } from './confirmation_modal';
+
+const messages = defineMessages({
+ logoutTitle: { id: 'confirmations.logout.title', defaultMessage: 'Log out?' },
+ logoutMessage: {
+ id: 'confirmations.logout.message',
+ defaultMessage: 'Are you sure you want to log out?',
+ },
+ logoutConfirm: {
+ id: 'confirmations.logout.confirm',
+ defaultMessage: 'Log out',
+ },
+});
+
+export const ConfirmLogOutModal: React.FC = ({
+ onClose,
+}) => {
+ const intl = useIntl();
+
+ const onConfirm = useCallback(() => {
+ void logOut();
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/reply.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/reply.tsx
new file mode 100644
index 0000000000..415a453954
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/reply.tsx
@@ -0,0 +1,46 @@
+import { useCallback } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { replyCompose } from 'flavours/glitch/actions/compose';
+import type { Status } from 'flavours/glitch/models/status';
+import { useAppDispatch } from 'flavours/glitch/store';
+
+import type { BaseConfirmationModalProps } from './confirmation_modal';
+import { ConfirmationModal } from './confirmation_modal';
+
+const messages = defineMessages({
+ replyTitle: {
+ id: 'confirmations.reply.title',
+ defaultMessage: 'Overwrite post?',
+ },
+ replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
+ replyMessage: {
+ id: 'confirmations.reply.message',
+ defaultMessage:
+ 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?',
+ },
+});
+
+export const ConfirmReplyModal: React.FC<
+ {
+ status: Status;
+ } & BaseConfirmationModalProps
+> = ({ status, onClose }) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+
+ const onConfirm = useCallback(() => {
+ dispatch(replyCompose(status));
+ }, [dispatch, status]);
+
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/unfollow.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/unfollow.tsx
new file mode 100644
index 0000000000..fe6f5e362d
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/unfollow.tsx
@@ -0,0 +1,50 @@
+import { useCallback } from 'react';
+
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+
+import { unfollowAccount } from 'flavours/glitch/actions/accounts';
+import type { Account } from 'flavours/glitch/models/account';
+import { useAppDispatch } from 'flavours/glitch/store';
+
+import type { BaseConfirmationModalProps } from './confirmation_modal';
+import { ConfirmationModal } from './confirmation_modal';
+
+const messages = defineMessages({
+ unfollowTitle: {
+ id: 'confirmations.unfollow.title',
+ defaultMessage: 'Unfollow user?',
+ },
+ unfollowConfirm: {
+ id: 'confirmations.unfollow.confirm',
+ defaultMessage: 'Unfollow',
+ },
+});
+
+export const ConfirmUnfollowModal: React.FC<
+ {
+ account: Account;
+ } & BaseConfirmationModalProps
+> = ({ account, onClose }) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+
+ const onConfirm = useCallback(() => {
+ dispatch(unfollowAccount(account.id));
+ }, [dispatch, account.id]);
+
+ return (
+ @{account.acct} }}
+ />
+ }
+ confirm={intl.formatMessage(messages.unfollowConfirm)}
+ onConfirm={onConfirm}
+ onClose={onClose}
+ />
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/ui/components/disabled_account_banner.jsx b/app/javascript/flavours/glitch/features/ui/components/disabled_account_banner.jsx
index 396127829b..7a19983a9c 100644
--- a/app/javascript/flavours/glitch/features/ui/components/disabled_account_banner.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/disabled_account_banner.jsx
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
-import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+import { FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
@@ -9,29 +9,15 @@ import { connect } from 'react-redux';
import { openModal } from 'flavours/glitch/actions/modal';
import { disabledAccountId, movedToAccountId, domain } from 'flavours/glitch/initial_state';
-import { logOut } from 'flavours/glitch/utils/log_out';
-
-const messages = defineMessages({
- logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
- logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
-});
const mapStateToProps = (state) => ({
disabledAcct: state.getIn(['accounts', disabledAccountId, 'acct']),
movedToAcct: movedToAccountId ? state.getIn(['accounts', movedToAccountId, 'acct']) : undefined,
});
-const mapDispatchToProps = (dispatch, { intl }) => ({
+const mapDispatchToProps = (dispatch) => ({
onLogout () {
- dispatch(openModal({
- modalType: 'CONFIRM',
- modalProps: {
- message: intl.formatMessage(messages.logoutMessage),
- confirm: intl.formatMessage(messages.logoutConfirm),
- closeWhenConfirm: false,
- onConfirm: () => logOut(),
- },
- }));
+ dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
},
});
diff --git a/app/javascript/flavours/glitch/features/ui/components/ignore_notifications_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/ignore_notifications_modal.jsx
new file mode 100644
index 0000000000..3affc76aa2
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/ignore_notifications_modal.jsx
@@ -0,0 +1,108 @@
+import PropTypes from 'prop-types';
+import { useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { useDispatch } from 'react-redux';
+
+import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
+import PersonAlertIcon from '@/material-icons/400-24px/person_alert.svg?react';
+import ShieldQuestionIcon from '@/material-icons/400-24px/shield_question.svg?react';
+import { closeModal } from 'flavours/glitch/actions/modal';
+import { updateNotificationsPolicy } from 'flavours/glitch/actions/notification_policies';
+import { Button } from 'flavours/glitch/components/button';
+import { Icon } from 'flavours/glitch/components/icon';
+
+export const IgnoreNotificationsModal = ({ filterType }) => {
+ const dispatch = useDispatch();
+
+ const handleClick = useCallback(() => {
+ dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
+ void dispatch(updateNotificationsPolicy({ [filterType]: 'drop' }));
+ }, [dispatch, filterType]);
+
+ const handleSecondaryClick = useCallback(() => {
+ dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
+ void dispatch(updateNotificationsPolicy({ [filterType]: 'filter' }));
+ }, [dispatch, filterType]);
+
+ const handleCancel = useCallback(() => {
+ dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
+ }, [dispatch]);
+
+ let title = null;
+
+ switch(filterType) {
+ case 'for_not_following':
+ title = ;
+ break;
+ case 'for_not_followers':
+ title = ;
+ break;
+ case 'for_new_accounts':
+ title = ;
+ break;
+ case 'for_private_mentions':
+ title = ;
+ break;
+ case 'for_limited_accounts':
+ title = ;
+ break;
+ }
+
+ return (
+
+
+
+
{title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+IgnoreNotificationsModal.propTypes = {
+ filterType: PropTypes.string.isRequired,
+};
+
+export default IgnoreNotificationsModal;
diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx b/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx
index 7c0ece465f..fb07f9e549 100644
--- a/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
-import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+import { FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
@@ -11,24 +11,11 @@ import { openModal } from 'flavours/glitch/actions/modal';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'flavours/glitch/initial_state';
import { PERMISSION_INVITE_USERS } from 'flavours/glitch/permissions';
-import { logOut } from 'flavours/glitch/utils/log_out';
-const messages = defineMessages({
- logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
- logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
-});
-
-const mapDispatchToProps = (dispatch, { intl }) => ({
+const mapDispatchToProps = (dispatch) => ({
onLogout () {
- dispatch(openModal({
- modalType: 'CONFIRM',
- modalProps: {
- message: intl.formatMessage(messages.logoutMessage),
- confirm: intl.formatMessage(messages.logoutConfirm),
- closeWhenConfirm: false,
- onConfirm: () => logOut(),
- },
- }));
+ dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
+
},
});
diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx
index 063316ef1b..64c6b52c31 100644
--- a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx
@@ -19,6 +19,7 @@ import {
InteractionModal,
SubscribedLanguagesModal,
ClosedRegistrationsModal,
+ IgnoreNotificationsModal,
} from 'flavours/glitch/features/ui/util/async-components';
import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar';
@@ -28,7 +29,16 @@ import ActionsModal from './actions_modal';
import AudioModal from './audio_modal';
import { BoostModal } from './boost_modal';
import BundleModalError from './bundle_modal_error';
-import ConfirmationModal from './confirmation_modal';
+import {
+ ConfirmationModal,
+ ConfirmDeleteStatusModal,
+ ConfirmDeleteListModal,
+ ConfirmReplyModal,
+ ConfirmEditStatusModal,
+ ConfirmUnfollowModal,
+ ConfirmClearNotificationsModal,
+ ConfirmLogOutModal,
+} from './confirmation_modals';
import DeprecatedSettingsModal from './deprecated_settings_modal';
import DoodleModal from './doodle_modal';
import FavouriteModal from './favourite_modal';
@@ -47,6 +57,13 @@ export const MODAL_COMPONENTS = {
'FAVOURITE': () => Promise.resolve({ default: FavouriteModal }),
'DOODLE': () => Promise.resolve({ default: DoodleModal }),
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
+ 'CONFIRM_DELETE_STATUS': () => Promise.resolve({ default: ConfirmDeleteStatusModal }),
+ 'CONFIRM_DELETE_LIST': () => Promise.resolve({ default: ConfirmDeleteListModal }),
+ 'CONFIRM_REPLY': () => Promise.resolve({ default: ConfirmReplyModal }),
+ 'CONFIRM_EDIT_STATUS': () => Promise.resolve({ default: ConfirmEditStatusModal }),
+ 'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }),
+ 'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
+ 'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'MUTE': MuteModal,
'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal,
@@ -64,6 +81,7 @@ export const MODAL_COMPONENTS = {
'SUBSCRIBED_LANGUAGES': SubscribedLanguagesModal,
'INTERACTION': InteractionModal,
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
+ 'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
};
export default class ModalRoot extends PureComponent {
diff --git a/app/javascript/flavours/glitch/features/ui/util/async-components.js b/app/javascript/flavours/glitch/features/ui/util/async-components.js
index e334e1a3b6..c7f2e6cff9 100644
--- a/app/javascript/flavours/glitch/features/ui/util/async-components.js
+++ b/app/javascript/flavours/glitch/features/ui/util/async-components.js
@@ -146,6 +146,10 @@ export function SettingsModal () {
return import(/* webpackChunkName: "flavours/glitch/async/settings_modal" */'../../local_settings');
}
+export function IgnoreNotificationsModal () {
+ return import(/* webpackChunkName: "flavours/glitch/async/ignore_notifications_modal" */'../components/ignore_notifications_modal');
+}
+
export function MediaGallery () {
return import(/* webpackChunkName: "flavours/glitch/async/media_gallery" */'../../../components/media_gallery');
}
diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json
index 1b7f5392d6..efa0b271fb 100644
--- a/app/javascript/flavours/glitch/locales/en.json
+++ b/app/javascript/flavours/glitch/locales/en.json
@@ -34,10 +34,6 @@
"confirmations.missing_media_description.confirm": "Send anyway",
"confirmations.missing_media_description.edit": "Edit media",
"confirmations.missing_media_description.message": "At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.",
- "confirmations.unfilter.author": "Author",
- "confirmations.unfilter.confirm": "Show",
- "confirmations.unfilter.edit_filter": "Edit filter",
- "confirmations.unfilter.filters": "Matching {count, plural, one {filter} other {filters}}",
"direct.group_by_conversations": "Group by conversation",
"endorsed_accounts_editor.endorsed_accounts": "Featured accounts",
"favourite_modal.combo": "You can press {combo} to skip this next time",
@@ -153,14 +149,18 @@
"settings.wide_view": "Wide view (Desktop mode only)",
"settings.wide_view_hint": "Stretches columns to better fill the available space.",
"status.collapse": "Collapse",
+ "status.filtered": "Filtered",
"status.has_audio": "Features attached audio files",
"status.has_pictures": "Features attached pictures",
"status.has_preview_card": "Features an attached preview card",
"status.has_video": "Features attached videos",
+ "status.hide": "Hide post",
"status.in_reply_to": "This toot is a reply",
"status.is_poll": "This toot is a poll",
"status.local_only": "Only visible from your instance",
"status.react": "React",
- "status.uncollapse": "Uncollapse",
- "suggestions.dismiss": "Dismiss suggestion"
+ "status.show_filter_reason": "Show anyway",
+ "status.show_less": "Show less",
+ "status.show_more": "Show more",
+ "status.uncollapse": "Uncollapse"
}
diff --git a/app/javascript/flavours/glitch/models/notification_group.ts b/app/javascript/flavours/glitch/models/notification_group.ts
index 58659f56da..13681f88df 100644
--- a/app/javascript/flavours/glitch/models/notification_group.ts
+++ b/app/javascript/flavours/glitch/models/notification_group.ts
@@ -14,14 +14,14 @@ import type { ApiReportJSON } from 'flavours/glitch/api_types/reports';
export const NOTIFICATIONS_GROUP_MAX_AVATARS = 8;
interface BaseNotificationGroup
- extends Omit {
+ extends Omit {
sampleAccountIds: string[];
}
interface BaseNotificationWithStatus
extends BaseNotificationGroup {
type: Type;
- statusId: string;
+ statusId: string | undefined;
}
interface BaseNotification
@@ -117,8 +117,7 @@ function createAccountRelationshipSeveranceEventFromJSON(
export function createNotificationGroupFromJSON(
groupJson: ApiNotificationGroupJSON,
): NotificationGroup {
- const { sample_accounts, ...group } = groupJson;
- const sampleAccountIds = sample_accounts.map((account) => account.id);
+ const { sample_account_ids: sampleAccountIds, ...group } = groupJson;
switch (group.type) {
case 'favourite':
@@ -128,9 +127,9 @@ export function createNotificationGroupFromJSON(
case 'mention':
case 'poll':
case 'update': {
- const { status, ...groupWithoutStatus } = group;
+ const { status_id: statusId, ...groupWithoutStatus } = group;
return {
- statusId: status.id,
+ statusId: statusId ?? undefined,
sampleAccountIds,
...groupWithoutStatus,
};
@@ -188,7 +187,7 @@ export function createNotificationGroupFromNotificationJSON(
case 'mention':
case 'poll':
case 'update':
- return { ...group, statusId: notification.status.id };
+ return { ...group, statusId: notification.status?.id };
case 'admin.report':
return { ...group, report: createReportFromJSON(notification.report) };
case 'severed_relationships':
diff --git a/app/javascript/flavours/glitch/reducers/notification_groups.ts b/app/javascript/flavours/glitch/reducers/notification_groups.ts
index b0b284b696..0fabf21b53 100644
--- a/app/javascript/flavours/glitch/reducers/notification_groups.ts
+++ b/app/javascript/flavours/glitch/reducers/notification_groups.ts
@@ -19,12 +19,17 @@ import {
markNotificationsAsRead,
mountNotifications,
unmountNotifications,
+ refreshStaleNotificationGroups,
+ pollRecentNotifications,
} from 'flavours/glitch/actions/notification_groups';
import {
disconnectTimeline,
timelineDelete,
} from 'flavours/glitch/actions/timelines_typed';
-import type { ApiNotificationJSON } from 'flavours/glitch/api_types/notifications';
+import type {
+ ApiNotificationJSON,
+ ApiNotificationGroupJSON,
+} from 'flavours/glitch/api_types/notifications';
import { compareId } from 'flavours/glitch/compare_id';
import { usePendingItems } from 'flavours/glitch/initial_state';
import {
@@ -48,8 +53,10 @@ interface NotificationGroupsState {
scrolledToTop: boolean;
isLoading: boolean;
lastReadId: string;
+ readMarkerId: string;
mounted: number;
isTabVisible: boolean;
+ mergedNotifications: 'ok' | 'pending' | 'needs-reload';
}
const initialState: NotificationGroupsState = {
@@ -57,8 +64,11 @@ const initialState: NotificationGroupsState = {
pendingGroups: [], // holds pending groups in slow mode
scrolledToTop: false,
isLoading: false,
+ // this is used to track whether we need to refresh notifications after accepting requests
+ mergedNotifications: 'ok',
// The following properties are used to track unread notifications
- lastReadId: '0', // used for unread notifications
+ lastReadId: '0', // used internally for unread notifications
+ readMarkerId: '0', // user-facing and updated when focus changes
mounted: 0, // number of mounted notification list components, usually 0 or 1
isTabVisible: true,
};
@@ -284,6 +294,112 @@ function updateLastReadId(
}
}
+function commitLastReadId(state: NotificationGroupsState) {
+ if (shouldMarkNewNotificationsAsRead(state)) {
+ state.readMarkerId = state.lastReadId;
+ }
+}
+
+function fillNotificationsGap(
+ groups: NotificationGroupsState['groups'],
+ gap: NotificationGap,
+ notifications: ApiNotificationGroupJSON[],
+): NotificationGroupsState['groups'] {
+ // find the gap in the existing notifications
+ const gapIndex = groups.findIndex(
+ (groupOrGap) =>
+ groupOrGap.type === 'gap' &&
+ groupOrGap.sinceId === gap.sinceId &&
+ groupOrGap.maxId === gap.maxId,
+ );
+
+ if (gapIndex < 0)
+ // We do not know where to insert, let's return
+ return groups;
+
+ // Filling a disconnection gap means we're getting historical data
+ // about groups we may know or may not know about.
+
+ // The notifications timeline is split in two by the gap, with
+ // group information newer than the gap, and group information older
+ // than the gap.
+
+ // Filling a gap should not touch anything before the gap, so any
+ // information on groups already appearing before the gap should be
+ // discarded, while any information on groups appearing after the gap
+ // can be updated and re-ordered.
+
+ const oldestPageNotification = notifications.at(-1)?.page_min_id;
+
+ // replace the gap with the notifications + a new gap
+
+ const newerGroupKeys = groups
+ .slice(0, gapIndex)
+ .filter(isNotificationGroup)
+ .map((group) => group.group_key);
+
+ const toInsert: NotificationGroupsState['groups'] = notifications
+ .map((json) => createNotificationGroupFromJSON(json))
+ .filter((notification) => !newerGroupKeys.includes(notification.group_key));
+
+ const apiGroupKeys = (toInsert as NotificationGroup[]).map(
+ (group) => group.group_key,
+ );
+
+ const sinceId = gap.sinceId;
+ if (
+ notifications.length > 0 &&
+ !(
+ oldestPageNotification &&
+ sinceId &&
+ compareId(oldestPageNotification, sinceId) <= 0
+ )
+ ) {
+ // If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap
+ // Similarly, if we've fetched more than the gap's, this means we have completely filled it
+ toInsert.push({
+ type: 'gap',
+ maxId: notifications.at(-1)?.page_max_id,
+ sinceId,
+ } as NotificationGap);
+ }
+
+ // Remove older groups covered by the API
+ groups = groups.filter(
+ (groupOrGap) =>
+ groupOrGap.type !== 'gap' && !apiGroupKeys.includes(groupOrGap.group_key),
+ );
+
+ // Replace the gap with API results (+ the new gap if needed)
+ groups.splice(gapIndex, 1, ...toInsert);
+
+ // Finally, merge any adjacent gaps that could have been created by filtering
+ // groups earlier
+ mergeGaps(groups);
+
+ return groups;
+}
+
+// Ensure the groups list starts with a gap, mutating it to prepend one if needed
+function ensureLeadingGap(
+ groups: NotificationGroupsState['groups'],
+): NotificationGap {
+ if (groups[0]?.type === 'gap') {
+ // We're expecting new notifications, so discard the maxId if there is one
+ groups[0].maxId = undefined;
+
+ return groups[0];
+ } else {
+ const gap: NotificationGap = {
+ type: 'gap',
+ sinceId: groups[0]?.page_min_id,
+ };
+
+ groups.unshift(gap);
+ return gap;
+ }
+}
+
export const notificationGroupsReducer = createReducer(
initialState,
(builder) => {
@@ -293,105 +409,59 @@ export const notificationGroupsReducer = createReducer(
json.type === 'gap' ? json : createNotificationGroupFromJSON(json),
);
state.isLoading = false;
+ state.mergedNotifications = 'ok';
updateLastReadId(state);
})
.addCase(fetchNotificationsGap.fulfilled, (state, action) => {
- const { notifications } = action.payload;
-
- // find the gap in the existing notifications
- const gapIndex = state.groups.findIndex(
- (groupOrGap) =>
- groupOrGap.type === 'gap' &&
- groupOrGap.sinceId === action.meta.arg.gap.sinceId &&
- groupOrGap.maxId === action.meta.arg.gap.maxId,
+ state.groups = fillNotificationsGap(
+ state.groups,
+ action.meta.arg.gap,
+ action.payload.notifications,
);
-
- if (gapIndex < 0)
- // We do not know where to insert, let's return
- return;
-
- // Filling a disconnection gap means we're getting historical data
- // about groups we may know or may not know about.
-
- // The notifications timeline is split in two by the gap, with
- // group information newer than the gap, and group information older
- // than the gap.
-
- // Filling a gap should not touch anything before the gap, so any
- // information on groups already appearing before the gap should be
- // discarded, while any information on groups appearing after the gap
- // can be updated and re-ordered.
-
- const oldestPageNotification = notifications.at(-1)?.page_min_id;
-
- // replace the gap with the notifications + a new gap
-
- const newerGroupKeys = state.groups
- .slice(0, gapIndex)
- .filter(isNotificationGroup)
- .map((group) => group.group_key);
-
- const toInsert: NotificationGroupsState['groups'] = notifications
- .map((json) => createNotificationGroupFromJSON(json))
- .filter(
- (notification) => !newerGroupKeys.includes(notification.group_key),
- );
-
- const apiGroupKeys = (toInsert as NotificationGroup[]).map(
- (group) => group.group_key,
- );
-
- const sinceId = action.meta.arg.gap.sinceId;
- if (
- notifications.length > 0 &&
- !(
- oldestPageNotification &&
- sinceId &&
- compareId(oldestPageNotification, sinceId) <= 0
- )
- ) {
- // If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap
- // Similarly, if we've fetched more than the gap's, this means we have completely filled it
- toInsert.push({
- type: 'gap',
- maxId: notifications.at(-1)?.page_max_id,
- sinceId,
- } as NotificationGap);
- }
-
- // Remove older groups covered by the API
- state.groups = state.groups.filter(
- (groupOrGap) =>
- groupOrGap.type !== 'gap' &&
- !apiGroupKeys.includes(groupOrGap.group_key),
- );
-
- // Replace the gap with API results (+ the new gap if needed)
- state.groups.splice(gapIndex, 1, ...toInsert);
-
- // Finally, merge any adjacent gaps that could have been created by filtering
- // groups earlier
- mergeGaps(state.groups);
-
state.isLoading = false;
updateLastReadId(state);
})
- .addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
- const notification = action.payload;
- processNewNotification(
- usePendingItems ? state.pendingGroups : state.groups,
- notification,
- );
+ .addCase(pollRecentNotifications.fulfilled, (state, action) => {
+ if (usePendingItems) {
+ const gap = ensureLeadingGap(state.pendingGroups);
+ state.pendingGroups = fillNotificationsGap(
+ state.pendingGroups,
+ gap,
+ action.payload.notifications,
+ );
+ } else {
+ const gap = ensureLeadingGap(state.groups);
+ state.groups = fillNotificationsGap(
+ state.groups,
+ gap,
+ action.payload.notifications,
+ );
+ }
+
+ state.isLoading = false;
+
updateLastReadId(state);
trimNotifications(state);
})
+ .addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
+ const notification = action.payload;
+ if (notification) {
+ processNewNotification(
+ usePendingItems ? state.pendingGroups : state.groups,
+ notification,
+ );
+ updateLastReadId(state);
+ trimNotifications(state);
+ }
+ })
.addCase(disconnectTimeline, (state, action) => {
if (action.payload.timeline === 'home') {
- if (state.groups.length > 0 && state.groups[0]?.type !== 'gap') {
- state.groups.unshift({
+ const groups = usePendingItems ? state.pendingGroups : state.groups;
+ if (groups.length > 0 && groups[0]?.type !== 'gap') {
+ groups.unshift({
type: 'gap',
- sinceId: state.groups[0]?.page_min_id,
+ sinceId: groups[0]?.page_min_id,
});
}
}
@@ -438,14 +508,15 @@ export const notificationGroupsReducer = createReducer(
}
}
}
- trimNotifications(state);
});
// Then build the consolidated list and clear pending groups
state.groups = state.pendingGroups.concat(state.groups);
state.pendingGroups = [];
+ mergeGaps(state.groups);
+ trimNotifications(state);
})
- .addCase(updateScrollPosition, (state, action) => {
+ .addCase(updateScrollPosition.fulfilled, (state, action) => {
state.scrolledToTop = action.payload.top;
updateLastReadId(state);
trimNotifications(state);
@@ -457,6 +528,7 @@ export const notificationGroupsReducer = createReducer(
compareId(state.lastReadId, mostRecentGroup.page_max_id) < 0
)
state.lastReadId = mostRecentGroup.page_max_id;
+ commitLastReadId(state);
})
.addCase(fetchMarkers.fulfilled, (state, action) => {
if (
@@ -465,11 +537,15 @@ export const notificationGroupsReducer = createReducer(
state.lastReadId,
action.payload.markers.notifications.last_read_id,
) < 0
- )
+ ) {
state.lastReadId = action.payload.markers.notifications.last_read_id;
+ state.readMarkerId =
+ action.payload.markers.notifications.last_read_id;
+ }
})
- .addCase(mountNotifications, (state) => {
+ .addCase(mountNotifications.fulfilled, (state) => {
state.mounted += 1;
+ commitLastReadId(state);
updateLastReadId(state);
})
.addCase(unmountNotifications, (state) => {
@@ -477,11 +553,16 @@ export const notificationGroupsReducer = createReducer(
})
.addCase(focusApp, (state) => {
state.isTabVisible = true;
+ commitLastReadId(state);
updateLastReadId(state);
})
.addCase(unfocusApp, (state) => {
state.isTabVisible = false;
})
+ .addCase(refreshStaleNotificationGroups.fulfilled, (state, action) => {
+ if (action.payload.deferredRefresh)
+ state.mergedNotifications = 'needs-reload';
+ })
.addMatcher(
isAnyOf(authorizeFollowRequestSuccess, rejectFollowRequestSuccess),
(state, action) => {
@@ -493,13 +574,21 @@ export const notificationGroupsReducer = createReducer(
},
)
.addMatcher(
- isAnyOf(fetchNotifications.pending, fetchNotificationsGap.pending),
+ isAnyOf(
+ fetchNotifications.pending,
+ fetchNotificationsGap.pending,
+ pollRecentNotifications.pending,
+ ),
(state) => {
state.isLoading = true;
},
)
.addMatcher(
- isAnyOf(fetchNotifications.rejected, fetchNotificationsGap.rejected),
+ isAnyOf(
+ fetchNotifications.rejected,
+ fetchNotificationsGap.rejected,
+ pollRecentNotifications.rejected,
+ ),
(state) => {
state.isLoading = false;
},
diff --git a/app/javascript/flavours/glitch/reducers/notification_policy.ts b/app/javascript/flavours/glitch/reducers/notification_policy.ts
index 2d5450ce44..0ec68f8950 100644
--- a/app/javascript/flavours/glitch/reducers/notification_policy.ts
+++ b/app/javascript/flavours/glitch/reducers/notification_policy.ts
@@ -2,17 +2,25 @@ import { createReducer, isAnyOf } from '@reduxjs/toolkit';
import {
fetchNotificationPolicy,
+ decreasePendingNotificationsCount,
updateNotificationsPolicy,
} from 'flavours/glitch/actions/notification_policies';
import type { NotificationPolicy } from 'flavours/glitch/models/notification_policy';
export const notificationPolicyReducer =
createReducer(null, (builder) => {
- builder.addMatcher(
- isAnyOf(
- fetchNotificationPolicy.fulfilled,
- updateNotificationsPolicy.fulfilled,
- ),
- (_state, action) => action.payload,
- );
+ builder
+ .addCase(decreasePendingNotificationsCount, (state, action) => {
+ if (state) {
+ state.summary.pending_notifications_count -= action.payload;
+ state.summary.pending_requests_count -= 1;
+ }
+ })
+ .addMatcher(
+ isAnyOf(
+ fetchNotificationPolicy.fulfilled,
+ updateNotificationsPolicy.fulfilled,
+ ),
+ (_state, action) => action.payload,
+ );
});
diff --git a/app/javascript/flavours/glitch/reducers/notification_requests.js b/app/javascript/flavours/glitch/reducers/notification_requests.js
index c1da951f6c..0a033682da 100644
--- a/app/javascript/flavours/glitch/reducers/notification_requests.js
+++ b/app/javascript/flavours/glitch/reducers/notification_requests.js
@@ -1,5 +1,6 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+import { blockAccountSuccess, muteAccountSuccess } from 'flavours/glitch/actions/accounts';
import {
NOTIFICATION_REQUESTS_EXPAND_REQUEST,
NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
@@ -12,6 +13,8 @@ import {
NOTIFICATION_REQUEST_FETCH_FAIL,
NOTIFICATION_REQUEST_ACCEPT_REQUEST,
NOTIFICATION_REQUEST_DISMISS_REQUEST,
+ NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
+ NOTIFICATION_REQUESTS_DISMISS_REQUEST,
NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
@@ -51,6 +54,14 @@ const removeRequest = (state, id) => {
return state.update('items', list => list.filterNot(item => item.get('id') === id));
};
+const removeRequestByAccount = (state, account_id) => {
+ if (state.getIn(['current', 'item', 'account']) === account_id) {
+ state = state.setIn(['current', 'removed'], true);
+ }
+
+ return state.update('items', list => list.filterNot(item => item.get('account') === account_id));
+};
+
export const notificationRequestsReducer = (state = initialState, action) => {
switch(action.type) {
case NOTIFICATION_REQUESTS_FETCH_SUCCESS:
@@ -74,6 +85,13 @@ export const notificationRequestsReducer = (state = initialState, action) => {
case NOTIFICATION_REQUEST_ACCEPT_REQUEST:
case NOTIFICATION_REQUEST_DISMISS_REQUEST:
return removeRequest(state, action.id);
+ case NOTIFICATION_REQUESTS_ACCEPT_REQUEST:
+ case NOTIFICATION_REQUESTS_DISMISS_REQUEST:
+ return action.ids.reduce((state, id) => removeRequest(state, id), state);
+ case blockAccountSuccess.type:
+ return removeRequestByAccount(state, action.payload.relationship.id);
+ case muteAccountSuccess.type:
+ return action.payload.relationship.muting_notifications ? removeRequestByAccount(state, action.payload.relationship.id) : state;
case NOTIFICATION_REQUEST_FETCH_REQUEST:
return state.set('current', initialState.get('current').set('isLoading', true));
case NOTIFICATION_REQUEST_FETCH_SUCCESS:
diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js
index 3006315a0e..a2c4bc5350 100644
--- a/app/javascript/flavours/glitch/reducers/settings.js
+++ b/app/javascript/flavours/glitch/reducers/settings.js
@@ -53,6 +53,7 @@ const initialState = ImmutableMap({
dismissPermissionBanner: false,
showUnread: true,
+ minimizeFilteredBanner: false,
shows: ImmutableMap({
follow: true,
diff --git a/app/javascript/flavours/glitch/selectors/notifications.ts b/app/javascript/flavours/glitch/selectors/notifications.ts
index 7cc64aa6c5..34983bd49a 100644
--- a/app/javascript/flavours/glitch/selectors/notifications.ts
+++ b/app/javascript/flavours/glitch/selectors/notifications.ts
@@ -1,15 +1,62 @@
import { createSelector } from '@reduxjs/toolkit';
import { compareId } from 'flavours/glitch/compare_id';
+import type { NotificationGroup } from 'flavours/glitch/models/notification_group';
+import type { NotificationGap } from 'flavours/glitch/reducers/notification_groups';
import type { RootState } from 'flavours/glitch/store';
+import {
+ selectSettingsNotificationsExcludedTypes,
+ selectSettingsNotificationsQuickFilterActive,
+ selectSettingsNotificationsQuickFilterShow,
+} from './settings';
+
+const filterNotificationsByAllowedTypes = (
+ showFilterBar: boolean,
+ allowedType: string,
+ excludedTypes: string[],
+ notifications: (NotificationGroup | NotificationGap)[],
+) => {
+ if (!showFilterBar || allowedType === 'all') {
+ // used if user changed the notification settings after loading the notifications from the server
+ // otherwise a list of notifications will come pre-filtered from the backend
+ // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
+ return notifications.filter(
+ (item) => item.type === 'gap' || !excludedTypes.includes(item.type),
+ );
+ }
+ return notifications.filter(
+ (item) => item.type === 'gap' || allowedType === item.type,
+ );
+};
+
+export const selectNotificationGroups = createSelector(
+ [
+ selectSettingsNotificationsQuickFilterShow,
+ selectSettingsNotificationsQuickFilterActive,
+ selectSettingsNotificationsExcludedTypes,
+ (state: RootState) => state.notificationGroups.groups,
+ ],
+ filterNotificationsByAllowedTypes,
+);
+
+const selectPendingNotificationGroups = createSelector(
+ [
+ selectSettingsNotificationsQuickFilterShow,
+ selectSettingsNotificationsQuickFilterActive,
+ selectSettingsNotificationsExcludedTypes,
+ (state: RootState) => state.notificationGroups.pendingGroups,
+ ],
+ filterNotificationsByAllowedTypes,
+);
+
export const selectUnreadNotificationGroupsCount = createSelector(
[
(s: RootState) => s.notificationGroups.lastReadId,
- (s: RootState) => s.notificationGroups.pendingGroups,
- (s: RootState) => s.notificationGroups.groups,
+ selectNotificationGroups,
+ selectPendingNotificationGroups,
],
- (notificationMarker, pendingGroups, groups) => {
+ (notificationMarker, groups, pendingGroups) => {
return (
groups.filter(
(group) =>
@@ -27,8 +74,24 @@ export const selectUnreadNotificationGroupsCount = createSelector(
},
);
+// Whether there is any unread notification according to the user-facing state
+export const selectAnyPendingNotification = createSelector(
+ [
+ (s: RootState) => s.notificationGroups.readMarkerId,
+ selectNotificationGroups,
+ ],
+ (notificationMarker, groups) => {
+ return groups.some(
+ (group) =>
+ group.type !== 'gap' &&
+ group.page_max_id &&
+ compareId(group.page_max_id, notificationMarker) > 0,
+ );
+ },
+);
+
export const selectPendingNotificationGroupsCount = createSelector(
- [(s: RootState) => s.notificationGroups.pendingGroups],
+ [selectPendingNotificationGroups],
(pendingGroups) =>
pendingGroups.filter((group) => group.type !== 'gap').length,
);
diff --git a/app/javascript/flavours/glitch/selectors/settings.ts b/app/javascript/flavours/glitch/selectors/settings.ts
index ce2b8b15e5..9a1a2c990b 100644
--- a/app/javascript/flavours/glitch/selectors/settings.ts
+++ b/app/javascript/flavours/glitch/selectors/settings.ts
@@ -37,4 +37,9 @@ export const selectNeedsNotificationPermission = (state: RootState) =>
'dismissPermissionBanner',
])) as boolean;
+export const selectSettingsNotificationsMinimizeFilteredBanner = (
+ state: RootState,
+) =>
+ state.settings.getIn(['notifications', 'minimizeFilteredBanner']) as boolean;
+
/* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
diff --git a/app/javascript/flavours/glitch/store/typed_functions.ts b/app/javascript/flavours/glitch/store/typed_functions.ts
index e5820149db..cd0f95cef9 100644
--- a/app/javascript/flavours/glitch/store/typed_functions.ts
+++ b/app/javascript/flavours/glitch/store/typed_functions.ts
@@ -1,9 +1,8 @@
+import type { GetThunkAPI } from '@reduxjs/toolkit';
import { createAsyncThunk } from '@reduxjs/toolkit';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
-import type { BaseThunkAPI } from '@reduxjs/toolkit/dist/createAsyncThunk';
-
import type { AppDispatch, RootState } from './store';
export const useAppDispatch = useDispatch.withTypes();
@@ -25,29 +24,20 @@ export const createAppAsyncThunk = createAsyncThunk.withTypes<{
rejectValue: AsyncThunkRejectValue;
}>();
-type AppThunkApi = Pick<
- BaseThunkAPI<
- RootState,
- unknown,
- AppDispatch,
- AsyncThunkRejectValue,
- AppMeta,
- AppMeta
- >,
- 'getState' | 'dispatch'
->;
-
-interface AppThunkOptions {
- skipLoading?: boolean;
-}
-
-const createBaseAsyncThunk = createAsyncThunk.withTypes<{
+interface AppThunkConfig {
state: RootState;
dispatch: AppDispatch;
rejectValue: AsyncThunkRejectValue;
fulfilledMeta: AppMeta;
rejectedMeta: AppMeta;
-}>();
+}
+type AppThunkApi = Pick, 'getState' | 'dispatch'>;
+
+interface AppThunkOptions {
+ skipLoading?: boolean;
+}
+
+const createBaseAsyncThunk = createAsyncThunk.withTypes();
export function createThunk(
name: string,
diff --git a/app/javascript/flavours/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss
index a59bb2d441..f139bef21b 100644
--- a/app/javascript/flavours/glitch/styles/_mixins.scss
+++ b/app/javascript/flavours/glitch/styles/_mixins.scss
@@ -1,15 +1,3 @@
-@mixin avatar-radius() {
- border-radius: $ui-avatar-border-size;
- background-position: 50%;
- background-clip: padding-box;
-}
-
-@mixin avatar-size($size: 48px) {
- width: $size;
- height: $size;
- background-size: $size $size;
-}
-
@mixin fullwidth-gallery {
&.full-width {
margin-left: -14px;
@@ -29,7 +17,7 @@
background: $ui-base-color;
color: $darker-text-color;
border-radius: 4px;
- border: 1px solid lighten($ui-base-color, 8%);
+ border: 1px solid var(--background-border-color);
font-size: 17px;
line-height: normal;
margin: 0;
diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss
index eba786ff9f..c4e0300776 100644
--- a/app/javascript/flavours/glitch/styles/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/accounts.scss
@@ -49,8 +49,6 @@
flex: 0 0 auto;
width: 48px;
height: 48px;
- @include avatar-size(48px);
-
padding-top: 2px;
img {
@@ -59,8 +57,6 @@
display: block;
margin: 0;
border-radius: 4px;
- @include avatar-radius;
-
background: darken($ui-base-color, 8%);
object-fit: cover;
}
@@ -134,21 +130,11 @@
.older {
float: left;
padding-inline-start: 0;
-
- .fa {
- display: inline-block;
- margin-inline-end: 5px;
- }
}
.newer {
float: right;
padding-inline-start: 0;
-
- .fa {
- display: inline-block;
- margin-inline-start: 5px;
- }
}
.disabled {
@@ -359,6 +345,10 @@
color: $primary-text-color;
font-weight: 700;
}
+
+ .warning-hint {
+ font-weight: normal !important;
+ }
}
&__body {
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index f4289a9ac3..c4a16247b8 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -1,6 +1,6 @@
@use 'sass:math';
-$no-columns-breakpoint: 600px;
+$no-columns-breakpoint: 890px;
$sidebar-width: 300px;
$content-width: 840px;
@@ -10,6 +10,13 @@ $content-width: 840px;
width: 100%;
min-height: 100vh;
+ .icon {
+ width: 16px;
+ height: 16px;
+ vertical-align: top;
+ margin: 0 2px;
+ }
+
.sidebar-wrapper {
min-height: 100vh;
overflow: hidden;
@@ -58,16 +65,16 @@ $content-width: 840px;
background: $ui-base-color;
}
- .fa-times {
+ .material-close {
display: none;
}
&.active {
- .fa-times {
+ .material-close {
display: block;
}
- .fa-bars {
+ .material-menu {
display: none;
}
}
@@ -115,10 +122,6 @@ $content-width: 840px;
overflow: hidden;
text-overflow: ellipsis;
- i.fa {
- margin-inline-end: 5px;
- }
-
&:hover {
color: $primary-text-color;
transition: all 100ms linear;
@@ -241,6 +244,11 @@ $content-width: 840px;
display: inline-flex;
flex-flow: wrap;
gap: 5px;
+ align-items: center;
+
+ .time-period {
+ padding: 0 10px;
+ }
}
h2 small {
@@ -299,10 +307,6 @@ $content-width: 840px;
box-shadow: none;
}
- .directory__tag .table-action-link .fa {
- color: inherit;
- }
-
.directory__tag h4 {
font-size: 18px;
font-weight: 700;
@@ -711,7 +715,7 @@ body,
top: 15px;
.avatar {
- border-radius: 4px;
+ border-radius: var(--avatar-border-radius);
width: 40px;
height: 40px;
}
@@ -762,7 +766,7 @@ body,
top: 15px;
.avatar {
- border-radius: 4px;
+ border-radius: var(--avatar-border-radius);
width: 40px;
height: 40px;
}
@@ -886,6 +890,7 @@ a.name-tag,
.account {
padding: 0;
+ border: none;
&__avatar-wrapper {
margin-inline-start: 0;
@@ -1609,7 +1614,7 @@ a.sparkline {
position: absolute;
inset-inline-start: 15px;
top: 15px;
- border-radius: 4px;
+ border-radius: var(--avatar-border-radius);
width: 40px;
height: 40px;
}
diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss
index e59dba3b65..2223893336 100644
--- a/app/javascript/flavours/glitch/styles/basics.scss
+++ b/app/javascript/flavours/glitch/styles/basics.scss
@@ -66,10 +66,6 @@ body {
}
}
- &.lighter {
- background: $ui-base-color;
- }
-
&.with-modals {
overflow-x: hidden;
overflow-y: scroll;
@@ -109,7 +105,6 @@ body {
}
&.embed {
- background: lighten($ui-base-color, 4%);
margin: 0;
padding-bottom: 0;
@@ -122,15 +117,12 @@ body {
}
&.admin {
- background: var(--background-color);
padding: 0;
}
&.error {
position: absolute;
text-align: center;
- color: $darker-text-color;
- background: $ui-base-color;
width: 100%;
height: 100%;
padding: 0;
diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss
index 2fc1bfd03b..675d900f8a 100644
--- a/app/javascript/flavours/glitch/styles/components.scss
+++ b/app/javascript/flavours/glitch/styles/components.scss
@@ -403,7 +403,7 @@ body > [data-popper-placement] {
&__suggestions {
box-shadow: var(--dropdown-shadow);
background: $ui-base-color;
- border: 1px solid lighten($ui-base-color, 14%);
+ border: 1px solid var(--background-border-color);
border-radius: 0 0 4px 4px;
color: $secondary-text-color;
font-size: 14px;
@@ -426,10 +426,17 @@ body > [data-popper-placement] {
&:hover,
&:focus,
- &:active,
+ &:active {
+ background: var(--dropdown-border-color);
+
+ .autosuggest-account .display-name__account {
+ color: inherit;
+ }
+ }
+
&.selected {
background: $ui-highlight-color;
- color: $primary-text-color;
+ color: $ui-button-color;
.autosuggest-account .display-name__account {
color: inherit;
@@ -465,7 +472,7 @@ body > [data-popper-placement] {
display: block;
line-height: 16px;
font-size: 12px;
- color: $dark-text-color;
+ color: $ui-primary-color;
}
}
@@ -804,16 +811,6 @@ body > [data-popper-placement] {
gap: 12px;
flex-wrap: wrap;
- .button {
- display: block; // Otherwise text-ellipsis doesn't work
- font-size: 14px;
- line-height: normal;
- font-weight: 700;
- flex: 1 1 auto;
- padding: 5px 12px;
- border-radius: 4px;
- }
-
.icon-button {
box-sizing: content-box;
color: $highlight-text-color;
@@ -926,6 +923,13 @@ body > [data-popper-placement] {
text-overflow: ellipsis;
white-space: nowrap;
+ &[disabled] {
+ cursor: default;
+ color: $highlight-text-color;
+ border-color: $highlight-text-color;
+ opacity: 0.5;
+ }
+
.icon {
width: 15px;
height: 15px;
@@ -1989,6 +1993,7 @@ body > [data-popper-placement] {
.account {
padding: 10px; // glitch: reduced padding
+ border-bottom: 1px solid var(--background-border-color);
.account__display-name {
flex: 1 1 auto;
@@ -2175,17 +2180,16 @@ body > [data-popper-placement] {
}
.account__avatar {
- @include avatar-radius;
-
display: block;
position: relative;
- overflow: hidden;
+ border-radius: var(--avatar-border-radius);
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
+ border-radius: var(--avatar-border-radius);
}
&-inline {
@@ -2195,8 +2199,6 @@ body > [data-popper-placement] {
}
&-composite {
- @include avatar-radius;
-
overflow: hidden;
position: relative;
@@ -2223,6 +2225,29 @@ body > [data-popper-placement] {
font-size: 15px;
}
}
+
+ &__counter {
+ $height: 16px;
+ $h-padding: 5px;
+
+ position: absolute;
+ bottom: -3px;
+ inset-inline-end: -3px;
+ padding-left: $h-padding;
+ padding-right: $h-padding;
+ height: $height;
+ border-radius: $height;
+ min-width: $height - 2 * $h-padding; // to ensure that it is never narrower than a circle
+ line-height: $height + 1px; // to visually center the numbers
+ background-color: $ui-button-background-color;
+ color: $white;
+ border-width: 1px;
+ border-style: solid;
+ border-color: var(--background-color);
+ font-size: 11px;
+ font-weight: 500;
+ text-align: center;
+ }
}
a .account__avatar {
@@ -2955,6 +2980,11 @@ $ui-header-logo-wordmark-width: 99px;
&.privacy-policy {
border-top: 1px solid var(--background-border-color);
border-radius: 4px;
+
+ @media screen and (max-width: $no-gap-breakpoint) {
+ border-top: 0;
+ border-bottom: 0;
+ }
}
}
}
@@ -2993,7 +3023,7 @@ $ui-header-logo-wordmark-width: 99px;
overflow: hidden;
}
-@media screen and (width >= 631px) {
+@media screen and (width > $mobile-breakpoint) {
.columns-area {
padding: 0;
}
@@ -3056,10 +3086,6 @@ $ui-header-logo-wordmark-width: 99px;
padding-inline-end: 30px;
}
- .search__icon .fa {
- top: 15px;
- }
-
.scrollable {
overflow: visible;
@@ -3591,26 +3617,6 @@ $ui-header-logo-wordmark-width: 99px;
height: calc(100% - 10px);
overflow-y: hidden;
- .hero-widget {
- box-shadow: none;
-
- &__text,
- &__img,
- &__img img {
- border-radius: 0;
- }
-
- &__text {
- padding: 15px;
- color: $secondary-text-color;
-
- strong {
- font-weight: 700;
- color: $primary-text-color;
- }
- }
- }
-
.compose-form {
flex: 1 1 auto;
min-height: 0;
@@ -4075,18 +4081,17 @@ input.glitch-setting-text {
display: block;
box-sizing: border-box;
margin: 0;
- color: $inverted-text-color;
- background: $white;
+ color: $primary-text-color;
+ background: $ui-base-color;
padding: 7px 10px;
font-family: inherit;
font-size: 14px;
line-height: 22px;
border-radius: 4px;
- border: 1px solid $white;
+ border: 1px solid var(--background-border-color);
&:focus {
outline: 0;
- border-color: lighten($ui-highlight-color, 12%);
}
&__wrapper {
@@ -4396,7 +4401,7 @@ a.status-card {
text-decoration: none;
&:hover {
- background: lighten($ui-base-color, 2%);
+ background: var(--on-surface-color);
}
}
@@ -4406,11 +4411,12 @@ a.status-card {
.timeline-hint {
text-align: center;
- color: $darker-text-color;
- padding: 15px;
+ color: $dark-text-color;
+ padding: 16px;
box-sizing: border-box;
width: 100%;
- cursor: default;
+ font-size: 14px;
+ line-height: 21px;
strong {
font-weight: 500;
@@ -4427,6 +4433,10 @@ a.status-card {
color: lighten($highlight-text-color, 4%);
}
}
+
+ &--with-descendants {
+ border-top: 1px solid var(--background-border-color);
+ }
}
.regeneration-indicator {
@@ -4508,6 +4518,35 @@ a.status-card {
}
}
+.column-header__select-row {
+ border-width: 0 1px 1px;
+ border-style: solid;
+ border-color: var(--background-border-color);
+ padding: 15px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ &__checkbox .check-box {
+ display: flex;
+ }
+
+ &__select-menu:disabled {
+ visibility: hidden;
+ }
+
+ &__mode-button {
+ margin-left: auto;
+ color: $highlight-text-color;
+ font-weight: bold;
+ font-size: 14px;
+
+ &:hover {
+ color: lighten($highlight-text-color, 6%);
+ }
+ }
+}
+
.column-header {
display: flex;
font-size: 16px;
@@ -4688,10 +4727,19 @@ a.status-card {
opacity: 1;
z-index: 1;
position: relative;
+ border-left: 1px solid var(--background-border-color);
+ border-right: 1px solid var(--background-border-color);
+ border-bottom: 1px solid var(--background-border-color);
+
+ @media screen and (max-width: $no-gap-breakpoint) {
+ border-left: 0;
+ border-right: 0;
+ }
&.collapsed {
max-height: 0;
opacity: 0.5;
+ border-bottom: 0;
}
&.animating {
@@ -4718,7 +4766,6 @@ a.status-card {
}
.column-header__collapsible-inner {
- border: 1px solid var(--background-border-color);
border-top: 0;
}
@@ -4848,6 +4895,7 @@ a.status-card {
padding: 0;
font-family: inherit;
font-size: inherit;
+ font-weight: inherit;
color: inherit;
border: 0;
background: transparent;
@@ -6142,7 +6190,7 @@ a.status-card {
user-select: text;
display: flex;
- @media screen and (width <= 630px) {
+ @media screen and (width <= $mobile-breakpoint) {
margin-top: auto;
}
}
@@ -6501,7 +6549,7 @@ a.status-card {
border-radius: 0 0 16px 16px;
border-top: 0;
- @media screen and (max-width: $no-gap-breakpoint) {
+ @media screen and (max-width: $mobile-breakpoint) {
border-radius: 0;
border-bottom: 0;
padding-bottom: 32px;
@@ -6540,6 +6588,25 @@ a.status-card {
}
}
+ &__confirmation {
+ font-size: 14px;
+ line-height: 20px;
+ color: $darker-text-color;
+
+ h1 {
+ font-size: 16px;
+ line-height: 24px;
+ color: $primary-text-color;
+ font-weight: 500;
+ margin-bottom: 8px;
+ }
+
+ strong {
+ font-weight: 700;
+ color: $primary-text-color;
+ }
+ }
+
&__bullet-points {
display: flex;
flex-direction: column;
@@ -6626,11 +6693,8 @@ a.status-card {
.doodle-modal,
.boost-modal,
-.confirmation-modal,
.report-modal,
.actions-modal,
-.mute-modal,
-.block-modal,
.compare-history-modal {
background: lighten($ui-secondary-color, 8%);
color: $inverted-text-color;
@@ -6655,7 +6719,7 @@ a.status-card {
}
.boost-modal__container {
- overflow-x: scroll;
+ overflow-y: auto;
padding: 10px;
.status {
@@ -6665,10 +6729,7 @@ a.status-card {
}
.doodle-modal__action-bar,
-.boost-modal__action-bar,
-.confirmation-modal__action-bar,
-.mute-modal__action-bar,
-.block-modal__action-bar {
+.boost-modal__action-bar {
display: flex;
justify-content: space-between;
align-items: center;
@@ -6691,16 +6752,6 @@ a.status-card {
}
}
-.mute-modal,
-.block-modal {
- line-height: 24px;
-}
-
-.mute-modal .react-toggle,
-.block-modal .react-toggle {
- vertical-align: middle;
-}
-
.report-modal {
width: 90vw;
max-width: 700px;
@@ -6710,9 +6761,10 @@ a.status-card {
max-width: 90vw;
width: 480px;
height: 80vh;
- background: lighten($ui-secondary-color, 8%);
- color: $inverted-text-color;
- border-radius: 8px;
+ background: var(--background-color);
+ color: $primary-text-color;
+ border-radius: 4px;
+ border: 1px solid var(--background-border-color);
overflow: hidden;
position: relative;
flex-direction: column;
@@ -6720,7 +6772,7 @@ a.status-card {
&__container {
box-sizing: border-box;
- border-top: 1px solid $ui-secondary-color;
+ border-top: 1px solid var(--background-border-color);
padding: 20px;
flex-grow: 1;
display: flex;
@@ -6750,7 +6802,7 @@ a.status-card {
&__lead {
font-size: 17px;
line-height: 22px;
- color: lighten($inverted-text-color, 16%);
+ color: $secondary-text-color;
margin-bottom: 30px;
a {
@@ -6785,7 +6837,7 @@ a.status-card {
.status__content,
.status__content p {
- color: $inverted-text-color;
+ color: $primary-text-color;
}
.status__content__spoiler-link {
@@ -6830,7 +6882,7 @@ a.status-card {
.poll__option.dialog-option {
padding: 15px 0;
flex: 0 0 auto;
- border-bottom: 1px solid $ui-secondary-color;
+ border-bottom: 1px solid var(--background-border-color);
&:last-child {
border-bottom: 0;
@@ -6838,13 +6890,13 @@ a.status-card {
& > .poll__option__text {
font-size: 13px;
- color: lighten($inverted-text-color, 16%);
+ color: $secondary-text-color;
strong {
font-size: 17px;
font-weight: 500;
line-height: 22px;
- color: $inverted-text-color;
+ color: $primary-text-color;
display: block;
margin-bottom: 4px;
@@ -6863,22 +6915,19 @@ a.status-card {
display: block;
box-sizing: border-box;
width: 100%;
- color: $inverted-text-color;
- background: $simple-background-color;
+ color: $primary-text-color;
+ background: $ui-base-color;
padding: 10px;
font-family: inherit;
font-size: 17px;
line-height: 22px;
resize: vertical;
border: 0;
+ border: 1px solid var(--background-border-color);
outline: 0;
border-radius: 4px;
margin: 20px 0;
- &::placeholder {
- color: $dark-text-color;
- }
-
&:focus {
outline: 0;
}
@@ -6899,16 +6948,16 @@ a.status-card {
}
.button.button-secondary {
- border-color: $inverted-text-color;
- color: $inverted-text-color;
+ border-color: $ui-button-destructive-background-color;
+ color: $ui-button-destructive-background-color;
flex: 0 0 auto;
&:hover,
&:focus,
&:active {
- background: transparent;
- border-color: $ui-button-background-color;
- color: $ui-button-background-color;
+ background: $ui-button-destructive-background-color;
+ border-color: $ui-button-destructive-background-color;
+ color: $white;
}
}
@@ -7095,31 +7144,7 @@ a.status-card {
}
}
-.confirmation-modal__action-bar,
-.mute-modal__action-bar,
-.block-modal__action-bar {
- .confirmation-modal__secondary-button {
- flex-shrink: 1;
- }
-}
-
-.confirmation-modal__secondary-button,
-.confirmation-modal__cancel-button,
-.mute-modal__cancel-button,
-.block-modal__cancel-button {
- background-color: transparent;
- color: $lighter-text-color;
- font-size: 14px;
- font-weight: 500;
-
- &:hover,
- &:focus,
- &:active {
- color: darken($lighter-text-color, 4%);
- background-color: transparent;
- }
-}
-
+// TODO
.confirmation-modal__do_not_ask_again {
padding-inline-start: 20px;
padding-inline-end: 20px;
@@ -7132,9 +7157,6 @@ a.status-card {
}
}
-.confirmation-modal__container,
-.mute-modal__container,
-.block-modal__container,
.report-modal__target {
padding: 30px;
font-size: 16px;
@@ -7168,31 +7190,10 @@ a.status-card {
}
}
-.confirmation-modal__container,
.report-modal__target {
text-align: center;
}
-.block-modal,
-.mute-modal {
- &__explanation {
- margin-top: 20px;
- }
-
- .setting-toggle {
- margin-top: 20px;
- margin-bottom: 24px;
- display: flex;
- align-items: center;
-
- &__label {
- color: $inverted-text-color;
- margin: 0;
- margin-inline-start: 8px;
- }
- }
-}
-
.report-modal__target {
padding: 15px;
@@ -7907,9 +7908,18 @@ img.modal-warning {
}
.scrollable .account-card__title__avatar {
- img,
+ img {
+ border: 2px solid var(--background-color);
+ }
+
.account__avatar {
- border-color: lighten($ui-base-color, 8%);
+ border: none;
+ }
+}
+
+.scrollable .account-card__header {
+ img {
+ border-radius: 4px;
}
}
@@ -7949,7 +7959,7 @@ img.modal-warning {
display: flex;
flex-shrink: 0;
- @media screen and (max-width: $no-gap-breakpoint) {
+ @media screen and (max-width: $no-gap-breakpoint - 1px) {
border-right: 0;
border-left: 0;
}
@@ -8046,20 +8056,9 @@ img.modal-warning {
flex: 0 0 auto;
border-radius: 50%;
- &.checked {
+ &.checked,
+ &.indeterminate {
border-color: $ui-highlight-color;
-
- &::before {
- position: absolute;
- left: 2px;
- top: 2px;
- content: '';
- display: block;
- border-radius: 50%;
- width: 12px;
- height: 12px;
- background: $ui-highlight-color;
- }
}
.icon {
@@ -8069,27 +8068,32 @@ img.modal-warning {
}
}
+.radio-button.checked::before {
+ position: absolute;
+ left: 2px;
+ top: 2px;
+ content: '';
+ display: block;
+ border-radius: 50%;
+ width: 12px;
+ height: 12px;
+ background: $ui-highlight-color;
+}
+
.check-box {
&__input {
width: 18px;
height: 18px;
border-radius: 2px;
- &.checked {
+ &.checked,
+ &.indeterminate {
background: $ui-highlight-color;
color: $white;
-
- &::before {
- display: none;
- }
}
}
}
-::-webkit-scrollbar-thumb {
- border-radius: 0;
-}
-
noscript {
text-align: center;
@@ -8250,6 +8254,11 @@ noscript {
width: 100%;
}
}
+
+ @media screen and (max-width: $no-gap-breakpoint) {
+ border-left: 0;
+ border-right: 0;
+ }
}
.drawer__backdrop {
@@ -8510,7 +8519,8 @@ noscript {
.account__avatar {
background: var(--background-color);
- border: 2px solid var(--background-border-color);
+ border: 1px solid var(--background-border-color);
+ border-radius: var(--avatar-border-radius);
}
}
}
@@ -8670,16 +8680,17 @@ noscript {
.verified {
border: 1px solid rgba($valid-value-color, 0.5);
margin-top: -1px;
+ margin-inline: -1px;
&:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
- margin-top: 0;
}
&:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
+ margin-bottom: -1px;
}
dt,
@@ -9385,10 +9396,13 @@ noscript {
flex: 1 1 auto;
display: flex;
flex-direction: column;
- border: 1px solid var(--background-border-color);
- border-top: 0;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
+
+ @media screen and (min-width: $no-gap-breakpoint) {
+ border: 1px solid var(--background-border-color);
+ border-top: 0;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ }
}
.story {
@@ -9635,8 +9649,9 @@ noscript {
backdrop-filter: var(--background-filter);
border: 1px solid var(--modal-border-color);
padding: 24px;
+ box-sizing: border-box;
- @media screen and (max-width: $no-gap-breakpoint) {
+ @media screen and (max-width: $mobile-breakpoint) {
border-radius: 16px 16px 0 0;
border-bottom: 0;
padding-bottom: 32px;
@@ -10794,35 +10809,36 @@ noscript {
}
&__badge {
- display: flex;
- align-items: center;
- border-radius: 999px;
- background: var(--background-border-color);
- color: $darker-text-color;
- padding: 4px;
- padding-inline-end: 8px;
- gap: 6px;
- font-weight: 500;
- font-size: 11px;
- line-height: 16px;
- word-break: keep-all;
-
- &__badge {
- background: $ui-button-background-color;
- color: $white;
- border-radius: 100px;
- padding: 2px 8px;
- }
+ background: $ui-button-background-color;
+ color: $white;
+ border-radius: 100px;
+ padding: 2px 8px;
}
}
.notification-request {
+ $padding: 15px;
+
display: flex;
- align-items: center;
- gap: 16px;
- padding: 15px;
+ padding: $padding;
+ gap: 8px;
+ position: relative;
border-bottom: 1px solid var(--background-border-color);
+ &__checkbox {
+ position: absolute;
+ inset-inline-start: $padding;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 0;
+ overflow: hidden;
+ opacity: 0;
+
+ .check-box {
+ display: flex;
+ }
+ }
+
&__link {
display: flex;
align-items: center;
@@ -10853,6 +10869,12 @@ noscript {
letter-spacing: 0.5px;
line-height: 24px;
color: $secondary-text-color;
+
+ bdi {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
}
.filtered-notifications-banner__badge {
@@ -10874,6 +10896,31 @@ noscript {
padding: 5px;
}
}
+
+ .notification-request__link {
+ transition: padding-inline-start 0.1s ease-in-out;
+ }
+
+ &--forced-checkbox {
+ cursor: pointer;
+
+ &:hover {
+ background: var(--on-surface-color);
+ }
+
+ .notification-request__checkbox {
+ opacity: 1;
+ width: 30px;
+ }
+
+ .notification-request__link {
+ padding-inline-start: 30px;
+ }
+
+ .notification-request__actions {
+ display: none;
+ }
+ }
}
.more-from-author {
@@ -10982,6 +11029,13 @@ noscript {
gap: 8px;
flex: 1 1 auto;
overflow: hidden;
+ container-type: inline-size;
+
+ @container (width < 350px) {
+ &__header time {
+ display: none;
+ }
+ }
&__header {
display: flex;
@@ -11021,6 +11075,13 @@ noscript {
border-radius: 8px;
padding: 8px;
}
+
+ &__additional-content {
+ color: $dark-text-color;
+ margin-top: -8px; // to offset the parent's `gap` property
+ font-size: 15px;
+ line-height: 22px;
+ }
}
&__avatar-group {
@@ -11073,6 +11134,19 @@ noscript {
}
}
+.notification-group__actions,
+.compose-form__actions {
+ .button {
+ display: block; // Otherwise text-ellipsis doesn't work
+ font-size: 14px;
+ line-height: normal;
+ font-weight: 700;
+ flex: 1 1 auto;
+ padding: 5px 12px;
+ border-radius: 4px;
+ }
+}
+
.notification-ungrouped {
padding: 10px 14px; // glitch: reduced padding
border-bottom: 1px solid var(--background-border-color);
@@ -11230,6 +11304,25 @@ noscript {
}
}
+ &__note {
+ &-label {
+ color: $dark-text-color;
+ font-size: 12px;
+ font-weight: 500;
+ text-transform: uppercase;
+ }
+
+ dd {
+ white-space: pre-line;
+ color: $secondary-text-color;
+ overflow: hidden;
+ line-clamp: 3; // Not yet supported in browers
+ display: -webkit-box; // The next 3 properties are needed
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ }
+ }
+
.display-name {
font-size: 15px;
line-height: 22px;
diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss
index 5330b5a20a..ac1f862a09 100644
--- a/app/javascript/flavours/glitch/styles/containers.scss
+++ b/app/javascript/flavours/glitch/styles/containers.scss
@@ -72,20 +72,16 @@
}
.avatar {
- width: 40px;
- height: 40px;
+ width: 48px;
+ height: 48px;
flex: 0 0 auto;
- @include avatar-size(40px);
-
- margin-inline-end: 10px;
img {
width: 100%;
height: 100%;
display: block;
margin: 0;
- border-radius: 4px;
- @include avatar-radius;
+ border-radius: var(--avatar-border-radius);
}
}
diff --git a/app/javascript/flavours/glitch/styles/contrast/variables.scss b/app/javascript/flavours/glitch/styles/contrast/variables.scss
index e38d24b271..766591ba40 100644
--- a/app/javascript/flavours/glitch/styles/contrast/variables.scss
+++ b/app/javascript/flavours/glitch/styles/contrast/variables.scss
@@ -1,10 +1,10 @@
// Dependent colors
$black: #000000;
-$classic-base-color: #282c37;
-$classic-primary-color: #9baec8;
-$classic-secondary-color: #d9e1e8;
-$classic-highlight-color: #6364ff;
+$classic-base-color: hsl(240deg, 16%, 19%);
+$classic-primary-color: hsl(240deg, 29%, 70%);
+$classic-secondary-color: hsl(255deg, 25%, 88%);
+$classic-highlight-color: hsl(240deg, 100%, 69%);
$ui-base-color: $classic-base-color !default;
$ui-primary-color: $classic-primary-color !default;
diff --git a/app/javascript/flavours/glitch/styles/dashboard.scss b/app/javascript/flavours/glitch/styles/dashboard.scss
index 12d0a6b92f..1621220ccb 100644
--- a/app/javascript/flavours/glitch/styles/dashboard.scss
+++ b/app/javascript/flavours/glitch/styles/dashboard.scss
@@ -113,10 +113,6 @@
flex: 1 1 auto;
}
- .fa {
- flex: 0 0 auto;
- }
-
strong {
font-weight: 700;
}
diff --git a/app/javascript/flavours/glitch/styles/emoji_picker.scss b/app/javascript/flavours/glitch/styles/emoji_picker.scss
index 3652ad4abb..3189000588 100644
--- a/app/javascript/flavours/glitch/styles/emoji_picker.scss
+++ b/app/javascript/flavours/glitch/styles/emoji_picker.scss
@@ -83,11 +83,6 @@
max-height: 35vh;
padding: 0 6px 6px;
will-change: transform;
-
- &::-webkit-scrollbar-track:hover,
- &::-webkit-scrollbar-track:active {
- background-color: rgba($base-overlay-background, 0.3);
- }
}
.emoji-mart-search {
@@ -116,7 +111,6 @@
&:focus {
outline: none !important;
border-width: 1px !important;
- border-color: $ui-button-background-color;
}
&::-webkit-search-cancel-button {
diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index 4877170275..81ce9a695f 100644
--- a/app/javascript/flavours/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
@@ -291,6 +291,10 @@ code {
flex: 0;
}
+ .input.select.select--languages {
+ min-width: 32ch;
+ }
+
.required abbr {
text-decoration: none;
color: lighten($error-value-color, 12%);
@@ -309,7 +313,7 @@ code {
margin-bottom: 10px;
max-width: 100%;
height: auto;
- border-radius: 4px;
+ border-radius: var(--avatar-border-radius);
background: url('images/void.png');
&[src$='missing.png'] {
@@ -442,11 +446,6 @@ code {
border-radius: 4px;
padding: 10px 16px;
- &::placeholder {
- color: $dark-text-color;
- opacity: 1;
- }
-
&:invalid {
box-shadow: none;
}
@@ -608,8 +607,7 @@ code {
inset-inline-end: 3px;
top: 1px;
padding: 10px;
- padding-bottom: 9px;
- font-size: 16px;
+ font-size: 14px;
color: $dark-text-color;
font-family: inherit;
pointer-events: none;
@@ -626,11 +624,6 @@ code {
inset-inline-end: 0;
bottom: 1px;
width: 5px;
- background-image: linear-gradient(
- to right,
- rgba(darken($ui-base-color, 10%), 0),
- darken($ui-base-color, 10%)
- );
}
}
}
@@ -938,10 +931,6 @@ code {
font-weight: 700;
}
}
-
- .fa {
- font-weight: 400;
- }
}
}
}
diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss
index 2d7ae74e22..d4f67113e1 100644
--- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss
+++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss
@@ -1,10 +1,6 @@
// Notes!
// Sass color functions, "darken" and "lighten" are automatically replaced.
-html {
- scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
-}
-
.simple_form .button.button-tertiary {
color: $highlight-text-color;
}
@@ -52,10 +48,6 @@ html {
color: darken($action-button-color, 25%);
}
-.account__header__bar .avatar .account__avatar {
- border-color: $white;
-}
-
.getting-started__footer a {
color: $ui-secondary-color;
text-decoration: underline;
@@ -214,12 +206,6 @@ html {
border-top-color: lighten($ui-base-color, 8%);
}
-.column-header__collapsible-inner {
- background: darken($ui-base-color, 4%);
- border: 1px solid var(--background-border-color);
- border-bottom: 0;
-}
-
.column-settings__hashtags .column-select__option {
color: $white;
}
@@ -298,12 +284,6 @@ html {
.directory__tag > div {
background: $white;
border: 1px solid var(--background-border-color);
-
- @media screen and (max-width: $no-gap-breakpoint) {
- border-left: 0;
- border-right: 0;
- border-top: 0;
- }
}
.picture-in-picture-placeholder {
@@ -318,10 +298,6 @@ html {
&:focus {
background: $ui-base-color;
}
-
- @media screen and (max-width: $no-gap-breakpoint) {
- border: 0;
- }
}
.batch-table {
@@ -438,9 +414,6 @@ html {
border-color: transparent transparent $white;
}
-.hero-widget,
-.moved-account-widget,
-.memoriam-widget,
.activity-stream,
.nothing-here,
.directory__tag > a,
@@ -569,11 +542,11 @@ html {
.compose-form .autosuggest-textarea__textarea,
.compose-form__highlightable,
+.autosuggest-textarea__suggestions,
.search__input,
.search__popout,
.emoji-mart-search input,
.language-dropdown__dropdown .emoji-mart-search input,
-// .strike-card,
.poll__option input[type='text'] {
background: darken($ui-base-color, 10%);
}
@@ -630,3 +603,28 @@ a.sparkline {
background: darken($ui-base-color, 10%);
}
}
+
+.setting-text {
+ background: darken($ui-base-color, 10%);
+}
+
+.report-dialog-modal__textarea {
+ background: darken($ui-base-color, 10%);
+}
+
+.autosuggest-account {
+ .display-name__account {
+ color: $dark-text-color;
+ }
+}
+
+@supports not selector(::-webkit-scrollbar) {
+ html {
+ scrollbar-color: rgba($action-button-color, 0.25)
+ var(--background-border-color);
+ }
+}
+
+::-webkit-scrollbar-thumb {
+ opacity: 0.25;
+}
diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss
index 9f571b3f26..76ede26233 100644
--- a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss
+++ b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss
@@ -2,26 +2,26 @@
$black: #000000;
$white: #ffffff;
-$classic-base-color: #282c37;
-$classic-primary-color: #9baec8;
-$classic-secondary-color: #d9e1e8;
-$classic-highlight-color: #6364ff;
+$classic-base-color: hsl(240deg, 16%, 19%);
+$classic-primary-color: hsl(240deg, 29%, 70%);
+$classic-secondary-color: hsl(255deg, 25%, 88%);
+$classic-highlight-color: hsl(240deg, 100%, 69%);
-$blurple-600: #563acc; // Iris
-$blurple-500: #6364ff; // Brand purple
-$blurple-300: #858afa; // Faded Blue
-$grey-600: #4e4c5a; // Trout
-$grey-100: #dadaf3; // Topaz
+$blurple-600: hsl(252deg, 59%, 51%); // Iris
+$blurple-500: hsl(240deg, 100%, 69%); // Brand purple
+$blurple-300: hsl(237deg, 92%, 75%); // Faded Blue
+$grey-600: hsl(240deg, 8%, 33%); // Trout
+$grey-100: hsl(240deg, 51%, 90%); // Topaz
// Differences
-$success-green: lighten(#3c754d, 8%);
+$success-green: lighten(hsl(138deg, 32%, 35%), 8%);
$base-overlay-background: $white !default;
$valid-value-color: $success-green !default;
$ui-base-color: $classic-secondary-color !default;
-$ui-base-lighter-color: #b0c0cf;
-$ui-primary-color: #9bcbed;
+$ui-base-lighter-color: hsl(250deg, 24%, 75%);
+$ui-primary-color: $classic-primary-color !default;
$ui-secondary-color: $classic-base-color !default;
$ui-highlight-color: $classic-highlight-color !default;
@@ -35,12 +35,12 @@ $ui-button-tertiary-border-color: $blurple-500 !default;
$primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default;
$highlight-text-color: $ui-highlight-color !default;
-$dark-text-color: #444b5d;
-$action-button-color: #606984;
+$dark-text-color: hsl(240deg, 16%, 32%);
+$action-button-color: hsl(240deg, 16%, 45%);
$inverted-text-color: $black !default;
$lighter-text-color: $classic-base-color !default;
-$light-text-color: #444b5d;
+$light-text-color: hsl(240deg, 16%, 32%);
// Newly added colors
$account-background-color: $white !default;
@@ -57,12 +57,13 @@ $account-background-color: $white !default;
$emojis-requiring-inversion: 'chains';
body {
- --dropdown-border-color: #d9e1e8;
+ --dropdown-border-color: hsl(240deg, 25%, 88%);
--dropdown-background-color: #fff;
- --modal-border-color: #d9e1e8;
+ --modal-border-color: hsl(240deg, 25%, 88%);
--modal-background-color: var(--background-color-tint);
- --background-border-color: #d9e1e8;
+ --background-border-color: hsl(240deg, 25%, 88%);
--background-color: #fff;
--background-color-tint: rgba(255, 255, 255, 80%);
--background-filter: blur(10px);
+ --on-surface-color: #{transparentize($ui-base-color, 0.65)};
}
diff --git a/app/javascript/flavours/glitch/styles/reset.scss b/app/javascript/flavours/glitch/styles/reset.scss
index f54ed5bc79..5a4152826d 100644
--- a/app/javascript/flavours/glitch/styles/reset.scss
+++ b/app/javascript/flavours/glitch/styles/reset.scss
@@ -53,41 +53,29 @@ table {
border-spacing: 0;
}
-html {
- scrollbar-color: lighten($ui-base-color, 4%) rgba($base-overlay-background, 0.1);
+@supports not selector(::-webkit-scrollbar) {
+ html {
+ scrollbar-color: $action-button-color var(--background-border-color);
+ scrollbar-width: thin;
+ }
}
::-webkit-scrollbar {
- width: 12px;
- height: 12px;
+ width: 8px;
+ height: 8px;
}
::-webkit-scrollbar-thumb {
- background: lighten($ui-base-color, 4%);
- border: 0px none $base-border-color;
- border-radius: 50px;
-}
-
-::-webkit-scrollbar-thumb:hover {
- background: lighten($ui-base-color, 6%);
-}
-
-::-webkit-scrollbar-thumb:active {
- background: lighten($ui-base-color, 4%);
+ background-color: $action-button-color;
+ border: 2px var(--background-border-color);
+ border-radius: 12px;
+ width: 6px;
+ box-shadow: inset 0 0 0 2px var(--background-border-color);
}
::-webkit-scrollbar-track {
- border: 0px none $base-border-color;
- border-radius: 0;
- background: rgba($base-overlay-background, 0.1);
-}
-
-::-webkit-scrollbar-track:hover {
- background: $ui-base-color;
-}
-
-::-webkit-scrollbar-track:active {
- background: $ui-base-color;
+ background-color: var(--background-border-color);
+ border-radius: 0px;
}
::-webkit-scrollbar-corner {
diff --git a/app/javascript/flavours/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss
index 1091a13238..f6140b87e2 100644
--- a/app/javascript/flavours/glitch/styles/rtl.scss
+++ b/app/javascript/flavours/glitch/styles/rtl.scss
@@ -90,30 +90,12 @@ body.rtl {
direction: rtl;
}
- .simple_form .label_input__append {
- &::after {
- background-image: linear-gradient(
- to left,
- rgba(darken($ui-base-color, 10%), 0),
- darken($ui-base-color, 10%)
- );
- }
- }
-
.simple_form select {
background: $ui-base-color
url("data:image/svg+xml;utf8,")
no-repeat left 8px center / auto 16px;
}
- .fa-chevron-left::before {
- content: '\F054';
- }
-
- .fa-chevron-right::before {
- content: '\F053';
- }
-
.dismissable-banner,
.warning-banner {
&__action {
diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss
index caebb952b8..6c7723b60d 100644
--- a/app/javascript/flavours/glitch/styles/tables.scss
+++ b/app/javascript/flavours/glitch/styles/tables.scss
@@ -142,11 +142,6 @@ a.table-action-link {
color: $highlight-text-color;
}
- i.fa {
- font-weight: 400;
- margin-inline-end: 5px;
- }
-
&:first-child {
padding-inline-start: 0;
}
@@ -273,8 +268,8 @@ a.table-action-link {
}
}
- &:nth-child(even) {
- background: var(--background-color);
+ &:last-child {
+ border-radius: 0 0 4px 4px;
}
&__content {
@@ -286,6 +281,10 @@ a.table-action-link {
padding: 0;
}
+ &--padded {
+ padding: 12px 16px 16px;
+ }
+
&--with-image {
display: flex;
align-items: center;
diff --git a/app/javascript/flavours/glitch/styles/variables.scss b/app/javascript/flavours/glitch/styles/variables.scss
index ab945f6d26..107754c9aa 100644
--- a/app/javascript/flavours/glitch/styles/variables.scss
+++ b/app/javascript/flavours/glitch/styles/variables.scss
@@ -7,8 +7,8 @@ $blurple-600: #563acc; // Iris
$blurple-500: #6364ff; // Brand purple
$blurple-400: #7477fd; // Medium slate blue
$blurple-300: #858afa; // Faded Blue
-$grey-600: #4e4c5a; // Trout
-$grey-100: #dadaf3; // Topaz
+$grey-600: hsl(240deg, 8%, 33%); // Trout
+$grey-100: hsl(240deg, 51%, 90%); // Topaz
$success-green: #79bd9a !default; // Padua
$error-red: $red-500 !default; // Cerise
@@ -18,10 +18,10 @@ $gold-star: #ca8f04 !default; // Dark Goldenrod
$red-bookmark: $warning-red;
// Values from the classic Mastodon UI
-$classic-base-color: #282c37; // Midnight Express
-$classic-primary-color: #9baec8; // Echo Blue
-$classic-secondary-color: #d9e1e8; // Pattens Blue
-$classic-highlight-color: #6364ff; // Brand purple
+$classic-base-color: hsl(240deg, 16%, 19%);
+$classic-primary-color: hsl(240deg, 29%, 70%);
+$classic-secondary-color: hsl(255deg, 25%, 88%);
+$classic-highlight-color: $blurple-500;
// Variables for defaults in UI
$base-shadow-color: $black !default;
@@ -88,6 +88,7 @@ $media-modal-media-max-width: 100%;
$media-modal-media-max-height: 80%;
$no-gap-breakpoint: 1175px;
+$mobile-breakpoint: 630px;
$font-sans-serif: 'Pretendard Variable', 'Pretendard JP Variable', 'Pretendard', 'Pretendard JP', 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default;
@@ -114,4 +115,6 @@ $dismiss-overlay-width: 4rem;
--surface-background-color: #{darken($ui-base-color, 4%)};
--surface-variant-background-color: #{$ui-base-color};
--surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
+ --on-surface-color: #{transparentize($ui-base-color, 0.5)};
+ --avatar-border-radius: 8px;
}
diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss
index dd349526ad..d810ee4bfc 100644
--- a/app/javascript/flavours/glitch/styles/widgets.scss
+++ b/app/javascript/flavours/glitch/styles/widgets.scss
@@ -1,207 +1,4 @@
-@use 'sass:math';
-
-.hero-widget {
- margin-bottom: 10px;
- box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
-
- &__img {
- width: 100%;
- position: relative;
- overflow: hidden;
- border-radius: 4px 4px 0 0;
- background: $base-shadow-color;
-
- img {
- object-fit: cover;
- display: block;
- width: 100%;
- height: 100%;
- margin: 0;
- border-radius: 4px 4px 0 0;
- }
- }
-
- &__text {
- background: $ui-base-color;
- padding: 20px;
- border-radius: 0 0 4px 4px;
- font-size: 15px;
- color: $darker-text-color;
- line-height: 20px;
- word-wrap: break-word;
- font-weight: 400;
-
- .emojione {
- width: 20px;
- height: 20px;
- margin: -3px 0 0;
- }
-
- p {
- margin-bottom: 20px;
-
- &:last-child {
- margin-bottom: 0;
- }
- }
-
- em {
- display: inline;
- margin: 0;
- padding: 0;
- font-weight: 700;
- background: transparent;
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
- color: lighten($darker-text-color, 10%);
- }
-
- a {
- color: $secondary-text-color;
- text-decoration: none;
-
- &:hover {
- text-decoration: underline;
- }
- }
- }
-
- @media screen and (max-width: $no-gap-breakpoint) {
- display: none;
- }
-}
-
-.endorsements-widget {
- margin-bottom: 10px;
- padding-bottom: 10px;
-
- h4 {
- padding: 10px;
- text-transform: uppercase;
- font-weight: 700;
- font-size: 13px;
- color: $darker-text-color;
- }
-
- .account {
- padding: 10px 0;
-
- &:last-child {
- border-bottom: 0;
- }
-
- .account__display-name {
- display: flex;
- align-items: center;
- }
- }
-
- .trends__item {
- padding: 10px;
- }
-}
-
-.trends-widget {
- h4 {
- color: $darker-text-color;
- }
-}
-
-.placeholder-widget {
- padding: 16px;
- border-radius: 4px;
- border: 2px dashed $dark-text-color;
- text-align: center;
- color: $darker-text-color;
- margin-bottom: 10px;
-}
-
-.moved-account-widget {
- padding: 15px;
- padding-bottom: 20px;
- border-radius: 4px;
- background: $ui-base-color;
- box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
- color: $secondary-text-color;
- font-weight: 400;
- margin-bottom: 10px;
-
- strong,
- a {
- font-weight: 500;
-
- @each $lang in $cjk-langs {
- &:lang(#{$lang}) {
- font-weight: 700;
- }
- }
- }
-
- a {
- color: inherit;
- text-decoration: underline;
-
- &.mention {
- text-decoration: none;
-
- span {
- text-decoration: none;
- }
-
- &:focus,
- &:hover,
- &:active {
- text-decoration: none;
-
- span {
- text-decoration: underline;
- }
- }
- }
- }
-
- &__message {
- margin-bottom: 15px;
-
- .fa {
- margin-inline-end: 5px;
- color: $darker-text-color;
- }
- }
-
- &__card {
- .detailed-status__display-avatar {
- position: relative;
- cursor: pointer;
- }
-
- .detailed-status__display-name {
- margin-bottom: 0;
- text-decoration: none;
-
- span {
- font-weight: 400;
- }
- }
- }
-}
-
-.memoriam-widget {
- padding: 20px;
- border-radius: 4px;
- background: $base-shadow-color;
- box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
- font-size: 14px;
- color: $darker-text-color;
- margin-bottom: 10px;
-}
-
.directory {
- background: var(--background-color);
- border-radius: 4px;
- box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
-
&__tag {
box-sizing: border-box;
margin-bottom: 10px;
@@ -211,7 +8,7 @@
display: flex;
align-items: center;
justify-content: space-between;
- border: 1px solid lighten($ui-base-color, 8%);
+ border: 1px solid var(--background-border-color);
border-radius: 4px;
padding: 15px;
text-decoration: none;
@@ -262,7 +59,8 @@
&.active h4 {
&,
.fa,
- small {
+ small,
+ .trends__item__current {
color: $primary-text-color;
}
}
@@ -275,6 +73,10 @@
&.active .avatar-stack .account__avatar {
border-color: $ui-highlight-color;
}
+
+ .trends__item__current {
+ padding-inline-end: 0;
+ }
}
}
@@ -350,13 +152,12 @@
vertical-align: initial !important;
}
- &__interrelationships {
+ tbody td.accounts-table__interrelationships {
width: 21px;
+ padding-inline-end: 16px;
}
- .fa {
- font-size: 16px;
-
+ .icon {
&.active {
color: $highlight-text-color;
}
@@ -376,27 +177,3 @@
}
}
}
-
-.moved-account-widget,
-.memoriam-widget,
-.directory {
- @media screen and (max-width: $no-gap-breakpoint) {
- margin-bottom: 0;
- box-shadow: none;
- border-radius: 0;
- }
-}
-
-.placeholder-widget {
- a {
- text-decoration: none;
- font-weight: 500;
- color: $ui-highlight-color;
-
- &:hover,
- &:focus,
- &:active {
- text-decoration: underline;
- }
- }
-}
diff --git a/app/javascript/flavours/glitch/utils/log_out.ts b/app/javascript/flavours/glitch/utils/log_out.ts
index d4db471a6e..a399fa4d0a 100644
--- a/app/javascript/flavours/glitch/utils/log_out.ts
+++ b/app/javascript/flavours/glitch/utils/log_out.ts
@@ -1,38 +1,20 @@
-import { signOutLink } from 'flavours/glitch/utils/backend_links';
+import api from 'flavours/glitch/api';
-export const logOut = () => {
- const form = document.createElement('form');
+export async function logOut() {
+ try {
+ const response = await api(false).delete<{ redirect_to?: string }>(
+ '/auth/sign_out',
+ { headers: { Accept: 'application/json' }, withCredentials: true },
+ );
- const methodInput = document.createElement('input');
- methodInput.setAttribute('name', '_method');
- methodInput.setAttribute('value', 'delete');
- methodInput.setAttribute('type', 'hidden');
- form.appendChild(methodInput);
-
- const csrfToken = document.querySelector(
- 'meta[name=csrf-token]',
- );
-
- const csrfParam = document.querySelector(
- 'meta[name=csrf-param]',
- );
-
- if (csrfParam && csrfToken) {
- const csrfInput = document.createElement('input');
- csrfInput.setAttribute('name', csrfParam.content);
- csrfInput.setAttribute('value', csrfToken.content);
- csrfInput.setAttribute('type', 'hidden');
- form.appendChild(csrfInput);
+ if (response.status === 200 && response.data.redirect_to)
+ window.location.href = response.data.redirect_to;
+ else
+ console.error(
+ 'Failed to log out, got an unexpected non-redirect response from the server',
+ response,
+ );
+ } catch (error) {
+ console.error('Failed to log out, response was an error', error);
}
-
- const submitButton = document.createElement('input');
- submitButton.setAttribute('type', 'submit');
- form.appendChild(submitButton);
-
- form.method = 'post';
- form.action = signOutLink;
- form.style.display = 'none';
-
- document.body.appendChild(form);
- submitButton.click();
-};
+}
diff --git a/app/javascript/images/filter-stripes.svg b/app/javascript/images/filter-stripes.svg
new file mode 100755
index 0000000000..4c1b58cb74
--- /dev/null
+++ b/app/javascript/images/filter-stripes.svg
@@ -0,0 +1,24 @@
+
diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index b296a5006a..d3538a8850 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/actions/interactions.js
@@ -437,12 +437,12 @@ export function unpinFail(status, error) {
};
}
-function toggleReblogWithoutConfirmation(status, privacy) {
+function toggleReblogWithoutConfirmation(status, visibility) {
return (dispatch) => {
if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') }));
} else {
- dispatch(reblog({ statusId: status.get('id'), privacy }));
+ dispatch(reblog({ statusId: status.get('id'), visibility }));
}
};
}
diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts
index 8fdec6e48b..51f83f1d24 100644
--- a/app/javascript/mastodon/actions/notification_groups.ts
+++ b/app/javascript/mastodon/actions/notification_groups.ts
@@ -11,10 +11,12 @@ import type {
} from 'mastodon/api_types/notifications';
import { allNotificationTypes } from 'mastodon/api_types/notifications';
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
+import { usePendingItems } from 'mastodon/initial_state';
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
import {
selectSettingsNotificationsExcludedTypes,
selectSettingsNotificationsQuickFilterActive,
+ selectSettingsNotificationsShows,
} from 'mastodon/selectors/settings';
import type { AppDispatch } from 'mastodon/store';
import {
@@ -38,10 +40,6 @@ function dispatchAssociatedRecords(
const fetchedStatuses: ApiStatusJSON[] = [];
notifications.forEach((notification) => {
- if ('sample_accounts' in notification) {
- fetchedAccounts.push(...notification.sample_accounts);
- }
-
if (notification.type === 'admin.report') {
fetchedAccounts.push(notification.report.target_account);
}
@@ -50,7 +48,7 @@ function dispatchAssociatedRecords(
fetchedAccounts.push(notification.moderation_warning.target_account);
}
- if ('status' in notification) {
+ if ('status' in notification && notification.status) {
fetchedStatuses.push(notification.status);
}
});
@@ -75,7 +73,9 @@ export const fetchNotifications = createDataLoadingThunk(
: excludeAllTypesExcept(activeFilter),
});
},
- ({ notifications }, { dispatch }) => {
+ ({ notifications, accounts, statuses }, { dispatch }) => {
+ dispatch(importFetchedAccounts(accounts));
+ dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
const payload: (ApiNotificationGroupJSON | NotificationGap)[] =
notifications;
@@ -95,7 +95,31 @@ export const fetchNotificationsGap = createDataLoadingThunk(
async (params: { gap: NotificationGap }) =>
apiFetchNotifications({ max_id: params.gap.maxId }),
- ({ notifications }, { dispatch }) => {
+ ({ notifications, accounts, statuses }, { dispatch }) => {
+ dispatch(importFetchedAccounts(accounts));
+ dispatch(importFetchedStatuses(statuses));
+ dispatchAssociatedRecords(dispatch, notifications);
+
+ return { notifications };
+ },
+);
+
+export const pollRecentNotifications = createDataLoadingThunk(
+ 'notificationGroups/pollRecentNotifications',
+ async (_params, { getState }) => {
+ return apiFetchNotifications({
+ max_id: undefined,
+ // In slow mode, we don't want to include notifications that duplicate the already-displayed ones
+ since_id: usePendingItems
+ ? getState().notificationGroups.groups.find(
+ (group) => group.type !== 'gap',
+ )?.page_max_id
+ : undefined,
+ });
+ },
+ ({ notifications, accounts, statuses }, { dispatch }) => {
+ dispatch(importFetchedAccounts(accounts));
+ dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
return { notifications };
@@ -104,7 +128,31 @@ export const fetchNotificationsGap = createDataLoadingThunk(
export const processNewNotificationForGroups = createAppAsyncThunk(
'notificationGroups/processNew',
- (notification: ApiNotificationJSON, { dispatch }) => {
+ (notification: ApiNotificationJSON, { dispatch, getState }) => {
+ const state = getState();
+ const activeFilter = selectSettingsNotificationsQuickFilterActive(state);
+ const notificationShows = selectSettingsNotificationsShows(state);
+
+ const showInColumn =
+ activeFilter === 'all'
+ ? notificationShows[notification.type]
+ : activeFilter === notification.type;
+
+ if (!showInColumn) return;
+
+ if (
+ (notification.type === 'mention' || notification.type === 'update') &&
+ notification.status?.filtered
+ ) {
+ const filters = notification.status.filtered.filter((result) =>
+ result.filter.context.includes('notifications'),
+ );
+
+ if (filters.some((result) => result.filter.filter_action === 'hide')) {
+ return;
+ }
+ }
+
dispatchAssociatedRecords(dispatch, [notification]);
return notification;
@@ -113,8 +161,18 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
export const loadPending = createAction('notificationGroups/loadPending');
-export const updateScrollPosition = createAction<{ top: boolean }>(
+export const updateScrollPosition = createAppAsyncThunk(
'notificationGroups/updateScrollPosition',
+ ({ top }: { top: boolean }, { dispatch, getState }) => {
+ if (
+ top &&
+ getState().notificationGroups.mergedNotifications === 'needs-reload'
+ ) {
+ void dispatch(fetchNotifications());
+ }
+
+ return { top };
+ },
);
export const setNotificationsFilter = createAppAsyncThunk(
@@ -140,5 +198,34 @@ export const markNotificationsAsRead = createAction(
'notificationGroups/markAsRead',
);
-export const mountNotifications = createAction('notificationGroups/mount');
+export const mountNotifications = createAppAsyncThunk(
+ 'notificationGroups/mount',
+ (_, { dispatch, getState }) => {
+ const state = getState();
+
+ if (
+ state.notificationGroups.mounted === 0 &&
+ state.notificationGroups.mergedNotifications === 'needs-reload'
+ ) {
+ void dispatch(fetchNotifications());
+ }
+ },
+);
+
export const unmountNotifications = createAction('notificationGroups/unmount');
+
+export const refreshStaleNotificationGroups = createAppAsyncThunk<{
+ deferredRefresh: boolean;
+}>('notificationGroups/refreshStale', (_, { dispatch, getState }) => {
+ const state = getState();
+
+ if (
+ state.notificationGroups.scrolledToTop ||
+ !state.notificationGroups.mounted
+ ) {
+ void dispatch(fetchNotifications());
+ return { deferredRefresh: false };
+ }
+
+ return { deferredRefresh: true };
+});
diff --git a/app/javascript/mastodon/actions/notification_policies.ts b/app/javascript/mastodon/actions/notification_policies.ts
index fcc9919c49..b182bcf699 100644
--- a/app/javascript/mastodon/actions/notification_policies.ts
+++ b/app/javascript/mastodon/actions/notification_policies.ts
@@ -1,3 +1,5 @@
+import { createAction } from '@reduxjs/toolkit';
+
import {
apiGetNotificationPolicy,
apiUpdateNotificationsPolicy,
@@ -14,3 +16,7 @@ export const updateNotificationsPolicy = createDataLoadingThunk(
'notificationPolicy/update',
(policy: Partial) => apiUpdateNotificationsPolicy(policy),
);
+
+export const decreasePendingNotificationsCount = createAction(
+ 'notificationPolicy/decreasePendingNotificationCount',
+);
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index fe3272d385..f6e95d2098 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -18,6 +18,7 @@ import {
importFetchedStatuses,
} from './importer';
import { submitMarkers } from './markers';
+import { decreasePendingNotificationsCount } from './notification_policies';
import { notificationsUpdate } from "./notifications_typed";
import { register as registerPushNotifications } from './push_notifications';
import { saveSettings } from './settings';
@@ -63,6 +64,14 @@ export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMIS
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
+export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST';
+export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS';
+export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL';
+
+export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISMISS_REQUEST';
+export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS';
+export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL';
+
export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
@@ -84,6 +93,12 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
}
};
+const selectNotificationCountForRequest = (state, id) => {
+ const requests = state.getIn(['notificationRequests', 'items']);
+ const thisRequest = requests.find(request => request.get('id') === id);
+ return thisRequest ? thisRequest.get('notifications_count') : 0;
+};
+
export const loadPending = () => ({
type: NOTIFICATIONS_LOAD_PENDING,
});
@@ -174,8 +189,8 @@ const noOp = () => {};
let expandNotificationsController = new AbortController();
-export function expandNotifications({ maxId, forceLoad = false } = {}, done = noOp) {
- return (dispatch, getState) => {
+export function expandNotifications({ maxId = undefined, forceLoad = false }) {
+ return async (dispatch, getState) => {
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
const notifications = getState().get('notifications');
const isLoadingMore = !!maxId;
@@ -185,7 +200,6 @@ export function expandNotifications({ maxId, forceLoad = false } = {}, done = no
expandNotificationsController.abort();
expandNotificationsController = new AbortController();
} else {
- done();
return;
}
}
@@ -212,7 +226,8 @@ export function expandNotifications({ maxId, forceLoad = false } = {}, done = no
dispatch(expandNotificationsRequest(isLoadingMore));
- api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }).then(response => {
+ try {
+ const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal });
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
@@ -222,11 +237,9 @@ export function expandNotifications({ maxId, forceLoad = false } = {}, done = no
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
fetchRelatedRelationships(dispatch, response.data);
dispatch(submitMarkers());
- }).catch(error => {
+ } catch(error) {
dispatch(expandNotificationsFail(error, isLoadingMore));
- }).finally(() => {
- done();
- });
+ }
};
}
@@ -434,11 +447,13 @@ export const fetchNotificationRequestFail = (id, error) => ({
error,
});
-export const acceptNotificationRequest = id => (dispatch) => {
+export const acceptNotificationRequest = (id) => (dispatch, getState) => {
+ const count = selectNotificationCountForRequest(getState(), id);
dispatch(acceptNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
dispatch(acceptNotificationRequestSuccess(id));
+ dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(acceptNotificationRequestFail(id, err));
});
@@ -460,11 +475,13 @@ export const acceptNotificationRequestFail = (id, error) => ({
error,
});
-export const dismissNotificationRequest = id => (dispatch) => {
+export const dismissNotificationRequest = (id) => (dispatch, getState) => {
+ const count = selectNotificationCountForRequest(getState(), id);
dispatch(dismissNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
dispatch(dismissNotificationRequestSuccess(id));
+ dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(dismissNotificationRequestFail(id, err));
});
@@ -486,6 +503,62 @@ export const dismissNotificationRequestFail = (id, error) => ({
error,
});
+export const acceptNotificationRequests = (ids) => (dispatch, getState) => {
+ const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
+ dispatch(acceptNotificationRequestsRequest(ids));
+
+ api().post(`/api/v1/notifications/requests/accept`, { id: ids }).then(() => {
+ dispatch(acceptNotificationRequestsSuccess(ids));
+ dispatch(decreasePendingNotificationsCount(count));
+ }).catch(err => {
+ dispatch(acceptNotificationRequestFail(ids, err));
+ });
+};
+
+export const acceptNotificationRequestsRequest = ids => ({
+ type: NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
+ ids,
+});
+
+export const acceptNotificationRequestsSuccess = ids => ({
+ type: NOTIFICATION_REQUESTS_ACCEPT_SUCCESS,
+ ids,
+});
+
+export const acceptNotificationRequestsFail = (ids, error) => ({
+ type: NOTIFICATION_REQUESTS_ACCEPT_FAIL,
+ ids,
+ error,
+});
+
+export const dismissNotificationRequests = (ids) => (dispatch, getState) => {
+ const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
+ dispatch(acceptNotificationRequestsRequest(ids));
+
+ api().post(`/api/v1/notifications/requests/dismiss`, { id: ids }).then(() => {
+ dispatch(dismissNotificationRequestsSuccess(ids));
+ dispatch(decreasePendingNotificationsCount(count));
+ }).catch(err => {
+ dispatch(dismissNotificationRequestFail(ids, err));
+ });
+};
+
+export const dismissNotificationRequestsRequest = ids => ({
+ type: NOTIFICATION_REQUESTS_DISMISS_REQUEST,
+ ids,
+});
+
+export const dismissNotificationRequestsSuccess = ids => ({
+ type: NOTIFICATION_REQUESTS_DISMISS_SUCCESS,
+ ids,
+});
+
+export const dismissNotificationRequestsFail = (ids, error) => ({
+ type: NOTIFICATION_REQUESTS_DISMISS_FAIL,
+ ids,
+ error,
+});
+
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']);
const params = { account_id: accountId };
diff --git a/app/javascript/mastodon/actions/notifications_migration.tsx b/app/javascript/mastodon/actions/notifications_migration.tsx
index f856e56828..c245dc7565 100644
--- a/app/javascript/mastodon/actions/notifications_migration.tsx
+++ b/app/javascript/mastodon/actions/notifications_migration.tsx
@@ -13,6 +13,6 @@ export const initializeNotifications = createAppAsyncThunk(
) as boolean;
if (enableBeta) void dispatch(fetchNotifications());
- else dispatch(expandNotifications());
+ else void dispatch(expandNotifications({}));
},
);
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index f50f41b0d9..04f5e6b88c 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -10,7 +10,7 @@ import {
deleteAnnouncement,
} from './announcements';
import { updateConversations } from './conversations';
-import { processNewNotificationForGroups } from './notification_groups';
+import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups';
import { updateNotifications, expandNotifications } from './notifications';
import { updateStatus } from './statuses';
import {
@@ -37,7 +37,7 @@ const randomUpTo = max =>
* @param {string} channelName
* @param {Object.} params
* @param {Object} options
- * @param {function(Function, Function): void} [options.fallback]
+ * @param {function(Function, Function): Promise} [options.fallback]
* @param {function(): void} [options.fillGaps]
* @param {function(object): boolean} [options.accept]
* @returns {function(): void}
@@ -52,14 +52,13 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
let pollingId;
/**
- * @param {function(Function, Function): void} fallback
+ * @param {function(Function, Function): Promise} fallback
*/
- const useFallback = fallback => {
- fallback(dispatch, () => {
- // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
- pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
- });
+ const useFallback = async fallback => {
+ await fallback(dispatch, getState);
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
+ pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
};
return {
@@ -104,11 +103,19 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
const notificationJSON = JSON.parse(data.payload);
dispatch(updateNotifications(notificationJSON, messages, locale));
// TODO: remove this once the groups feature replaces the previous one
- if(getState().notificationGroups.groups.length > 0) {
+ if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) {
dispatch(processNewNotificationForGroups(notificationJSON));
}
break;
}
+ case 'notifications_merged':
+ const state = getState();
+ if (state.notifications.top || !state.notifications.mounted)
+ dispatch(expandNotifications({ forceLoad: true, maxId: undefined }));
+ if(state.settings.getIn(['notifications', 'groupingBeta'], false)) {
+ dispatch(refreshStaleNotificationGroups());
+ }
+ break;
case 'conversation':
// @ts-expect-error
dispatch(updateConversations(JSON.parse(data.payload)));
@@ -132,21 +139,30 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
/**
* @param {Function} dispatch
- * @param {function(): void} done
+ * @param {Function} getState
*/
-const refreshHomeTimelineAndNotification = (dispatch, done) => {
- // @ts-expect-error
- dispatch(expandHomeTimeline({}, () =>
- // @ts-expect-error
- dispatch(expandNotifications({}, () =>
- dispatch(fetchAnnouncements(done))))));
-};
+async function refreshHomeTimelineAndNotification(dispatch, getState) {
+ await dispatch(expandHomeTimeline({ maxId: undefined }));
+
+ // TODO: remove this once the groups feature replaces the previous one
+ if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) {
+ // TODO: polling for merged notifications
+ try {
+ await dispatch(pollRecentGroupNotifications());
+ } catch (error) {
+ // TODO
+ }
+ } else {
+ await dispatch(expandNotifications({}));
+ }
+
+ await dispatch(fetchAnnouncements());
+}
/**
* @returns {function(): void}
*/
export const connectUserStream = () =>
- // @ts-expect-error
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
/**
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index f0ea46118e..65b6d80451 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -76,21 +76,18 @@ export function clearTimeline(timeline) {
};
}
-const noOp = () => {};
-
const parseTags = (tags = {}, mode) => {
return (tags[mode] || []).map((tag) => {
return tag.value;
});
};
-export function expandTimeline(timelineId, path, params = {}, done = noOp) {
- return (dispatch, getState) => {
+export function expandTimeline(timelineId, path, params = {}) {
+ return async (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
const isLoadingMore = !!params.max_id;
if (timeline.get('isLoading')) {
- done();
return;
}
@@ -109,7 +106,8 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
- api().get(path, { params }).then(response => {
+ try {
+ const response = await api().get(path, { params });
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
@@ -127,52 +125,48 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
if (timelineId === 'home') {
dispatch(submitMarkers());
}
- }).catch(error => {
+ } catch(error) {
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
- }).finally(() => {
- done();
- });
+ }
};
}
-export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
- return (dispatch, getState) => {
+export function fillTimelineGaps(timelineId, path, params = {}) {
+ return async (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
const items = timeline.get('items');
const nullIndexes = items.map((statusId, index) => statusId === null ? index : null);
const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null);
// Only expand at most two gaps to avoid doing too many requests
- done = gaps.take(2).reduce((done, maxId) => {
- return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done)));
- }, done);
-
- done();
+ for (const maxId of gaps.take(2)) {
+ await dispatch(expandTimeline(timelineId, path, { ...params, maxId }));
+ }
};
}
-export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
-export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
-export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
+export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId });
+export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia });
+export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia });
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
-export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
-export const expandLinkTimeline = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done);
-export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
+export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId });
+export const expandLinkTimeline = (url, { maxId } = {}) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId });
+export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}) => {
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
max_id: maxId,
any: parseTags(tags, 'any'),
all: parseTags(tags, 'all'),
none: parseTags(tags, 'none'),
local: local,
- }, done);
+ });
};
-export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
-export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia }, done);
-export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
-export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
+export const fillHomeTimelineGaps = () => fillTimelineGaps('home', '/api/v1/timelines/home', {});
+export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote } = {}) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia });
+export const fillCommunityTimelineGaps = ({ onlyMedia } = {}) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia });
+export const fillListTimelineGaps = (id) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {});
export function expandTimelineRequest(timeline, isLoadingMore) {
return {
diff --git a/app/javascript/mastodon/api/notification_policies.ts b/app/javascript/mastodon/api/notification_policies.ts
index 4032134fb5..3bc8174139 100644
--- a/app/javascript/mastodon/api/notification_policies.ts
+++ b/app/javascript/mastodon/api/notification_policies.ts
@@ -2,8 +2,8 @@ import { apiRequestGet, apiRequestPut } from 'mastodon/api';
import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies';
export const apiGetNotificationPolicy = () =>
- apiRequestGet('/v1/notifications/policy');
+ apiRequestGet('v2/notifications/policy');
export const apiUpdateNotificationsPolicy = (
policy: Partial,
-) => apiRequestPut('/v1/notifications/policy', policy);
+) => apiRequestPut('v2/notifications/policy', policy);
diff --git a/app/javascript/mastodon/api/notifications.ts b/app/javascript/mastodon/api/notifications.ts
index c1ab6f70ca..cb07e4114c 100644
--- a/app/javascript/mastodon/api/notifications.ts
+++ b/app/javascript/mastodon/api/notifications.ts
@@ -1,17 +1,25 @@
import api, { apiRequest, getLinks } from 'mastodon/api';
-import type { ApiNotificationGroupJSON } from 'mastodon/api_types/notifications';
+import type { ApiNotificationGroupsResultJSON } from 'mastodon/api_types/notifications';
export const apiFetchNotifications = async (params?: {
exclude_types?: string[];
max_id?: string;
+ since_id?: string;
}) => {
- const response = await api().request({
+ const response = await api().request({
method: 'GET',
url: '/api/v2_alpha/notifications',
params,
});
- return { notifications: response.data, links: getLinks(response) };
+ const { statuses, accounts, notification_groups } = response.data;
+
+ return {
+ statuses,
+ accounts,
+ notifications: notification_groups,
+ links: getLinks(response),
+ };
};
export const apiClearNotifications = () =>
diff --git a/app/javascript/mastodon/api_types/notification_policies.ts b/app/javascript/mastodon/api_types/notification_policies.ts
index 0f4a2d132e..1c3970782c 100644
--- a/app/javascript/mastodon/api_types/notification_policies.ts
+++ b/app/javascript/mastodon/api_types/notification_policies.ts
@@ -1,10 +1,13 @@
// See app/serializers/rest/notification_policy_serializer.rb
+export type NotificationPolicyValue = 'accept' | 'filter' | 'drop';
+
export interface NotificationPolicyJSON {
- filter_not_following: boolean;
- filter_not_followers: boolean;
- filter_new_accounts: boolean;
- filter_private_mentions: boolean;
+ for_not_following: NotificationPolicyValue;
+ for_not_followers: NotificationPolicyValue;
+ for_new_accounts: NotificationPolicyValue;
+ for_private_mentions: NotificationPolicyValue;
+ for_limited_accounts: NotificationPolicyValue;
summary: {
pending_requests_count: number;
pending_notifications_count: number;
diff --git a/app/javascript/mastodon/api_types/notifications.ts b/app/javascript/mastodon/api_types/notifications.ts
index d7cbbca73b..4ab9a4c90a 100644
--- a/app/javascript/mastodon/api_types/notifications.ts
+++ b/app/javascript/mastodon/api_types/notifications.ts
@@ -51,7 +51,7 @@ export interface BaseNotificationGroupJSON {
group_key: string;
notifications_count: number;
type: NotificationType;
- sample_accounts: ApiAccountJSON[];
+ sample_account_ids: string[];
latest_page_notification_at: string; // FIXME: This will only be present if the notification group is returned in a paginated list, not requested directly
most_recent_notification_id: string;
page_min_id?: string;
@@ -60,12 +60,12 @@ export interface BaseNotificationGroupJSON {
interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
type: NotificationWithStatusType;
- status: ApiStatusJSON;
+ status_id: string | null;
}
interface NotificationWithStatusJSON extends BaseNotificationJSON {
type: NotificationWithStatusType;
- status: ApiStatusJSON;
+ status: ApiStatusJSON | null;
}
interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
@@ -143,3 +143,9 @@ export type ApiNotificationGroupJSON =
| AccountRelationshipSeveranceNotificationGroupJSON
| NotificationGroupWithStatusJSON
| ModerationWarningNotificationGroupJSON;
+
+export interface ApiNotificationGroupsResultJSON {
+ accounts: ApiAccountJSON[];
+ statuses: ApiStatusJSON[];
+ notification_groups: ApiNotificationGroupJSON[];
+}
diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts
index a934faeb7a..2c59645ea7 100644
--- a/app/javascript/mastodon/api_types/statuses.ts
+++ b/app/javascript/mastodon/api_types/statuses.ts
@@ -58,6 +58,29 @@ export interface ApiPreviewCardJSON {
authors: ApiPreviewCardAuthorJSON[];
}
+export type FilterContext =
+ | 'home'
+ | 'notifications'
+ | 'public'
+ | 'thread'
+ | 'account';
+
+export interface ApiFilterJSON {
+ id: string;
+ title: string;
+ context: FilterContext;
+ expires_at: string;
+ filter_action: 'warn' | 'hide';
+ keywords?: unknown[]; // TODO: FilterKeywordSerializer
+ statuses?: unknown[]; // TODO: FilterStatusSerializer
+}
+
+export interface ApiFilterResultJSON {
+ filter: ApiFilterJSON;
+ keyword_matches: string[];
+ status_matches: string[];
+}
+
export interface ApiStatusJSON {
id: string;
created_at: string;
@@ -80,8 +103,7 @@ export interface ApiStatusJSON {
bookmarked?: boolean;
pinned?: boolean;
- // filtered: FilterResult[]
- filtered: unknown; // TODO
+ filtered?: ApiFilterResultJSON[];
content?: string;
text?: string;
diff --git a/app/javascript/mastodon/common.js b/app/javascript/mastodon/common.js
index 511568aa0f..28857de534 100644
--- a/app/javascript/mastodon/common.js
+++ b/app/javascript/mastodon/common.js
@@ -1,5 +1,4 @@
import Rails from '@rails/ujs';
-import 'font-awesome/css/font-awesome.css';
export function start() {
require.context('../images/', true, /\.(jpg|png|svg)$/);
diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx
index 2a748e67ff..265c68697b 100644
--- a/app/javascript/mastodon/components/account.jsx
+++ b/app/javascript/mastodon/components/account.jsx
@@ -106,7 +106,7 @@ const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifica
>
);
} else if (defaultAction === 'mute') {
- buttons = ;
+ buttons = ;
} else if (defaultAction === 'block') {
buttons = ;
} else if (!account.get('suspended') && !account.get('moved') || following) {
diff --git a/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx b/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx
index cc05e5c163..351f1c949e 100644
--- a/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx
+++ b/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx
@@ -153,7 +153,7 @@ class ReportReasonSelector extends PureComponent {
-
+
{rules.map(rule => )}