From ab2c2a8e5c8548a7c5da10095c13e808e313f5b4 Mon Sep 17 00:00:00 2001 From: David Mzareulyan Date: Thu, 1 Jan 2026 16:28:02 +0300 Subject: [PATCH 1/7] Add pauseMe redux actions for temporary account suspension --- src/redux/action-creators.js | 8 ++++++++ src/redux/action-types.js | 1 + src/redux/reducers/settings-forms.js | 2 ++ src/services/api.js | 4 ++++ 4 files changed, 15 insertions(+) diff --git a/src/redux/action-creators.js b/src/redux/action-creators.js index 045790c40..00838f232 100644 --- a/src/redux/action-creators.js +++ b/src/redux/action-creators.js @@ -1137,6 +1137,14 @@ export function suspendMe(password) { }; } +export function pauseMe(password, message) { + return { + type: ActionTypes.PAUSE_USER, + apiRequest: Api.pauseMe, + payload: { password, message }, + }; +} + export function resumeMe(resumeToken) { return { type: ActionTypes.ACTIVATE_USER, diff --git a/src/redux/action-types.js b/src/redux/action-types.js index 11360d839..b27780564 100644 --- a/src/redux/action-types.js +++ b/src/redux/action-types.js @@ -152,6 +152,7 @@ export const DELETE_HOME_FEED = 'DELETE_HOME_FEED'; export const REORDER_HOME_FEEDS = 'REORDER_HOME_FEEDS'; export const GET_ALL_SUBSCRIPTIONS = 'GET_ALL_SUBSCRIPTIONS'; export const DEACTIVATE_USER = 'DEACTIVATE_USER'; +export const PAUSE_USER = 'PAUSE_USER'; export const ACTIVATE_USER = 'ACTIVATE_USER'; export const CREATE_ATTACHMENT = 'CREATE_ATTACHMENT'; export const SET_ATTACHMENT = 'SET_ATTACHMENT'; diff --git a/src/redux/reducers/settings-forms.js b/src/redux/reducers/settings-forms.js index d8a9895a9..c47208bb7 100644 --- a/src/redux/reducers/settings-forms.js +++ b/src/redux/reducers/settings-forms.js @@ -8,6 +8,7 @@ import { UPDATE_USER_NOTIFICATION_PREFERENCES, DEACTIVATE_USER, ACTIVATE_USER, + PAUSE_USER, } from '../action-types'; import { initialAsyncState, asyncState } from '../async-helpers'; import { setOnLocationChange } from './helpers'; @@ -26,5 +27,6 @@ export const settingsForms = combineReducers({ ), privacyStatus: asyncState(UPDATE_USER, setOnLocationChange(initialAsyncState)), deactivateStatus: asyncState(DEACTIVATE_USER, setOnLocationChange(initialAsyncState)), + pauseStatus: asyncState(PAUSE_USER, setOnLocationChange(initialAsyncState)), activateStatus: asyncState(ACTIVATE_USER, setOnLocationChange(initialAsyncState)), }); diff --git a/src/services/api.js b/src/services/api.js index 822b6b54e..66c3e52e2 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -726,6 +726,10 @@ export function suspendMe({ password }) { return fetch(`${apiPrefix}/users/suspend-me`, postRequestOptions('POST', { password })); } +export function pauseMe({ password, message }) { + return fetch(`${apiPrefix}/users/pause-me`, postRequestOptions('POST', { password, message })); +} + export function resumeMe({ resumeToken }) { return fetch(`${apiPrefix}/users/resume-me`, postRequestOptions('POST', { resumeToken })); } From 68a559fe2b15be3721d9324567fae170140ff170 Mon Sep 17 00:00:00 2001 From: David Mzareulyan Date: Fri, 2 Jan 2026 13:47:37 +0300 Subject: [PATCH 2/7] Add pause account UI and distinguish paused from deleted accounts --- src/components/settings/pause.jsx | 158 +++++++++++++++++++++++++++ src/components/settings/profile.jsx | 3 +- src/components/settings/routes.jsx | 2 + src/components/user-feed-status.jsx | 3 +- src/components/user-feed.jsx | 12 +- src/components/user-profile-head.jsx | 2 +- 6 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 src/components/settings/pause.jsx diff --git a/src/components/settings/pause.jsx b/src/components/settings/pause.jsx new file mode 100644 index 000000000..d6a9a9a1a --- /dev/null +++ b/src/components/settings/pause.jsx @@ -0,0 +1,158 @@ +import { useState, useCallback, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import cn from 'classnames'; +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; + +import { Throbber } from '../throbber'; +import { doSequence } from '../../redux/async-helpers'; +import { pluralForm } from '../../utils'; +import UserName from '../user-name'; +import { Icon } from '../fontawesome-icons'; +import { SettingsPage } from './layout'; +import { pauseMe, unauthenticated } from '../../redux/action-creators'; +import styles from './settings.module.scss'; + +export default function PausePage() { + const dispatch = useDispatch(); + const userInfo = useSelector((state) => state.user); + const formStatus = useSelector((state) => state.settingsForms.pauseStatus); + + const usernameS = useMemo(() => { + if (userInfo.username.endsWith('s')) { + return `${userInfo.username}\u2019`; + } + return `${userInfo.username}\u2019s`; + }, [userInfo.username]); + + const [password, setPassword] = useState(''); + const [message, setMessage] = useState(userInfo.preferences.pauseMessage || ''); + const onPasswordChange = useCallback(({ target }) => setPassword(target.value), []); + const onMessageChange = useCallback(({ target }) => setMessage(target.value), []); + + const canSubmit = useMemo(() => password.trim() !== '', [password]); + const submit = useCallback( + (e) => { + e.preventDefault(); + if (!canSubmit) { + alert('Please enter password'); + return; + } + doSequence(dispatch)( + (dispatch) => dispatch(pauseMe(password, message.trim())), + (dispatch) => dispatch(unauthenticated()), + ); + }, + [canSubmit, dispatch, password, message], + ); + + return ( + +
+

When an account is paused, the following will be hidden:

+
    +
  • All your posts
  • +
  • All your images and other attachments
  • +
  • All your likes
  • +
+

+ Your comments in other user’s posts will stay, and your username will be used for + attribution. +

+

Once you pause your account, you can reactivate it at any time.

+
+ +
+
+
+ + +
+
+ + +
+
+ {' '} + {formStatus.loading && } +

(You will be signed out immediately)

+
+ {formStatus.error && ( +

+ {formStatus.errorText} +

+ )} +
+
+
+ ); +} + +function OrphanGroupsWarning() { + const managedGroups = useSelector((state) => state.managedGroups); + const orphanGroups = managedGroups.filter( + (g) => g.administrators.length === 1 && g.isRestricted === '0', + ); + + if (orphanGroups.length === 0) { + return null; + } + + if (orphanGroups.length === 1) { + const [g] = orphanGroups; + return ( +
+

+ Please note: there is a non-restricted group{' '} + {g.screenName} in which you are the only administrator. +

+

+ If you pause your account, this group will become restricted and no one will be able to + create posts in it. Please add additional administrators to these groups before you pause + your account to prevent that. +

+
+ ); + } + + return ( +
+

+ Please note: there are{' '} + {pluralForm(orphanGroups.length, 'non-restricted group')} in which you are the only + administrator: +

+
    + {orphanGroups.map((g) => ( +
  1. + {g.screenName} +
  2. + ))} +
+

+ If you pause your account, these groups will become restricted and no one will be able to + create posts in them. Please add additional administrators to these groups before you pause + your account to prevent that. +

+
+ ); +} diff --git a/src/components/settings/profile.jsx b/src/components/settings/profile.jsx index 1586800fa..4ea22da98 100644 --- a/src/components/settings/profile.jsx +++ b/src/components/settings/profile.jsx @@ -18,7 +18,8 @@ export default function ProfilePage() {

- You can delete your account here. + You can pause your account or{' '} + delete it.

diff --git a/src/components/settings/routes.jsx b/src/components/settings/routes.jsx index b83216ff8..76fc6d97c 100644 --- a/src/components/settings/routes.jsx +++ b/src/components/settings/routes.jsx @@ -9,6 +9,7 @@ const AppearancePage = lazyRetry(() => import('./appearance')); const PrivacyPage = lazyRetry(() => import('./privacy')); const NotificationsPage = lazyRetry(() => import('./notififications')); const DeactivatePage = lazyRetry(() => import('./deactivate')); +const PausePage = lazyRetry(() => import('./pause')); const AuthSessionsPage = lazyRetry(() => import('./auth-sessions')); const SanitizeMediaPage = lazyRetry(() => import('./sanitize-media')); @@ -23,6 +24,7 @@ export function settingsRoute(rootPath) { + {tokensRoute('app-tokens')} diff --git a/src/components/user-feed-status.jsx b/src/components/user-feed-status.jsx index 25b40ca1c..9dd27e32a 100644 --- a/src/components/user-feed-status.jsx +++ b/src/components/user-feed-status.jsx @@ -11,7 +11,8 @@ export default function UserFeedStatus(props) { {props.isGone ? ( - Deleted + {' '} + {props.goneStatus === 'paused' ? 'Paused' : 'Deleted'} ) : props.isPrivate === '1' ? ( diff --git a/src/components/user-feed.jsx b/src/components/user-feed.jsx index 412b82a77..2f583fffb 100644 --- a/src/components/user-feed.jsx +++ b/src/components/user-feed.jsx @@ -39,8 +39,16 @@ class UserFeed extends Component { return (

- {viewUser.screenName} account has been deleted. This page still exists as a stub - for the username, but this {viewUser.type} is not in FreeFeed anymore. + {viewUser.goneStatus === 'paused' ? ( + <> + {viewUser.screenName} has paused their account. They may return someday. + + ) : ( + <> + {viewUser.screenName} account has been deleted. This page still exists as a + stub for the username, but this {viewUser.type} is not in FreeFeed anymore. + + )}

); diff --git a/src/components/user-profile-head.jsx b/src/components/user-profile-head.jsx index 8254834ce..ca4eeeb06 100644 --- a/src/components/user-profile-head.jsx +++ b/src/components/user-profile-head.jsx @@ -359,7 +359,7 @@ function PrivacyIndicator({ user }) { if (user.isGone) { icon = faUserSlash; - label = 'Deleted user'; + label = user.goneStatus === 'paused' ? 'Paused user' : 'Deleted user'; } else if (user.isPrivate === '1') { icon = faLock; label = `Private ${userOrGroup}`; From 1ddcbf7b2e1bcf958a85b1f0b22946f4e2c9e39b Mon Sep 17 00:00:00 2001 From: David Mzareulyan Date: Fri, 2 Jan 2026 13:49:26 +0300 Subject: [PATCH 3/7] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2b751d09..0bf09c7c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.144.0] - Not released +### Added +- Account pause functionality. Users can now temporarily suspend their account + from the Settings page. Paused accounts are distinguished from deleted + accounts in the UI. ## [1.143.0] - 2025-12-17 ### Added From 84f5cdbafb925de30d1778f64a724dbaea3a03ba Mon Sep 17 00:00:00 2001 From: David Mzareulyan Date: Fri, 2 Jan 2026 14:27:52 +0300 Subject: [PATCH 4/7] Prevent errors when userInfo properties are undefined (after the deactivation) --- src/components/settings/deactivate.jsx | 2 +- src/components/settings/pause.jsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/settings/deactivate.jsx b/src/components/settings/deactivate.jsx index 4bd2d720c..05afb76f0 100644 --- a/src/components/settings/deactivate.jsx +++ b/src/components/settings/deactivate.jsx @@ -18,7 +18,7 @@ export default function PrivacyPage() { const formStatus = useSelector((state) => state.settingsForms.deactivateStatus); const usernameS = useMemo(() => { - if (userInfo.username.endsWith('s')) { + if (userInfo.username?.endsWith('s')) { return `${userInfo.username}\u2019`; } return `${userInfo.username}\u2019s`; diff --git a/src/components/settings/pause.jsx b/src/components/settings/pause.jsx index d6a9a9a1a..5cd7cc01d 100644 --- a/src/components/settings/pause.jsx +++ b/src/components/settings/pause.jsx @@ -18,14 +18,14 @@ export default function PausePage() { const formStatus = useSelector((state) => state.settingsForms.pauseStatus); const usernameS = useMemo(() => { - if (userInfo.username.endsWith('s')) { + if (userInfo.username?.endsWith('s')) { return `${userInfo.username}\u2019`; } return `${userInfo.username}\u2019s`; }, [userInfo.username]); const [password, setPassword] = useState(''); - const [message, setMessage] = useState(userInfo.preferences.pauseMessage || ''); + const [message, setMessage] = useState(userInfo.preferences?.pauseMessage || ''); const onPasswordChange = useCallback(({ target }) => setPassword(target.value), []); const onMessageChange = useCallback(({ target }) => setMessage(target.value), []); From 2afc450479e53bd21f12b44ec3eed109652facc4 Mon Sep 17 00:00:00 2001 From: David Mzareulyan Date: Mon, 12 Jan 2026 12:11:43 +0300 Subject: [PATCH 5/7] Navigate to user profile page before logout when pausing or deactivating account --- src/components/settings/deactivate.jsx | 5 ++++- src/components/settings/pause.jsx | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/settings/deactivate.jsx b/src/components/settings/deactivate.jsx index 05afb76f0..d33edd1bf 100644 --- a/src/components/settings/deactivate.jsx +++ b/src/components/settings/deactivate.jsx @@ -11,9 +11,11 @@ import { Icon } from '../fontawesome-icons'; import { SettingsPage } from './layout'; import { suspendMe, unauthenticated } from './../../redux/action-creators'; import styles from './settings.module.scss'; +import { useNouter } from '../../services/nouter'; export default function PrivacyPage() { const dispatch = useDispatch(); + const { navigate } = useNouter(); const userInfo = useSelector((state) => state.user); const formStatus = useSelector((state) => state.settingsForms.deactivateStatus); @@ -37,10 +39,11 @@ export default function PrivacyPage() { } doSequence(dispatch)( (dispatch) => dispatch(suspendMe(password)), + () => navigate(`/${userInfo.username}`), (dispatch) => dispatch(unauthenticated()), ); }, - [canSubmit, dispatch, password], + [canSubmit, dispatch, navigate, password, userInfo.username], ); return ( diff --git a/src/components/settings/pause.jsx b/src/components/settings/pause.jsx index 5cd7cc01d..630f84222 100644 --- a/src/components/settings/pause.jsx +++ b/src/components/settings/pause.jsx @@ -11,9 +11,11 @@ import { Icon } from '../fontawesome-icons'; import { SettingsPage } from './layout'; import { pauseMe, unauthenticated } from '../../redux/action-creators'; import styles from './settings.module.scss'; +import { useNouter } from '../../services/nouter'; export default function PausePage() { const dispatch = useDispatch(); + const { navigate } = useNouter(); const userInfo = useSelector((state) => state.user); const formStatus = useSelector((state) => state.settingsForms.pauseStatus); @@ -39,10 +41,11 @@ export default function PausePage() { } doSequence(dispatch)( (dispatch) => dispatch(pauseMe(password, message.trim())), + () => navigate(`/${userInfo.username}`), (dispatch) => dispatch(unauthenticated()), ); }, - [canSubmit, dispatch, password, message], + [canSubmit, dispatch, navigate, password, message, userInfo.username], ); return ( From 3be37e364a7525b7f2da5d5928f143e62c0b03ca Mon Sep 17 00:00:00 2001 From: David Mzareulyan Date: Mon, 12 Jan 2026 13:11:54 +0300 Subject: [PATCH 6/7] Hide user description for gone (paused/deleted) accounts --- src/components/user-profile-head.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/user-profile-head.jsx b/src/components/user-profile-head.jsx index ca4eeeb06..c8dbdfacf 100644 --- a/src/components/user-profile-head.jsx +++ b/src/components/user-profile-head.jsx @@ -289,7 +289,7 @@ export const UserProfileHead = withNouter(
- + {user.isGone ? null : }
{isAuthenticated && !isCurrentUser && ( <> From 3dd21accd0330dcf57b927271b9173a1f22180a9 Mon Sep 17 00:00:00 2001 From: David Mzareulyan Date: Mon, 12 Jan 2026 13:12:27 +0300 Subject: [PATCH 7/7] Display pause message when available --- src/components/user-feed.jsx | 38 ++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/components/user-feed.jsx b/src/components/user-feed.jsx index 2f583fffb..610fbc41c 100644 --- a/src/components/user-feed.jsx +++ b/src/components/user-feed.jsx @@ -39,16 +39,7 @@ class UserFeed extends Component { return (

- {viewUser.goneStatus === 'paused' ? ( - <> - {viewUser.screenName} has paused their account. They may return someday. - - ) : ( - <> - {viewUser.screenName} account has been deleted. This page still exists as a - stub for the username, but this {viewUser.type} is not in FreeFeed anymore. - - )} +

); @@ -114,3 +105,30 @@ function select(state) { } export default connect(select)(UserFeed); + +function UserGonePanel({ user }) { + if (!user.isGone) { + return null; + } + if (user.goneStatus === 'paused') { + if (user.description) { + return ( + <> + {user.screenName} has paused their account and left a message:{' '} + {user.description} + + ); + } + return ( + <> + {user.screenName} has paused their account. They may return someday. + + ); + } + return ( + <> + {user.screenName} account has been deleted. This page still exists as a stub for the + username, but this {user.type} is not in FreeFeed anymore. + + ); +}