From 25da664f1b03a2ca6b086a69a792d485ed138808 Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Mon, 27 Jan 2025 19:40:01 +0000 Subject: [PATCH 1/9] Updated FollowButton with "unfollow" design ref https://linear.app/ghost/issue/AP-311/ui-to-unfollow-an-actor - Added a confirmation modal on unfollow - Made the button in "Following" state more subtle - Made the label switch from "Following" to "Unfollow" on hover so it's clearer what it does --- .../components/activities/ActivityItem.tsx | 2 +- .../src/components/global/FollowButton.tsx | 56 +++++++++++++++---- .../components/modals/ViewProfileModal.tsx | 7 ++- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx b/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx index b07cc4c73ee6..0cce368e931f 100644 --- a/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx +++ b/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx @@ -28,7 +28,7 @@ const ActivityItem: React.FC = ({children, url = null, onClic onClick(); } }}> -
+
{childrenArray[0]} {childrenArray[1]} {childrenArray[2]} diff --git a/apps/admin-x-activitypub/src/components/global/FollowButton.tsx b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx index d18ec3138857..6d70d13772a0 100644 --- a/apps/admin-x-activitypub/src/components/global/FollowButton.tsx +++ b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx @@ -1,29 +1,53 @@ +import NiceModal from '@ebay/nice-modal-react'; +import clsx from 'clsx'; +import {Button, Modal} from '@tryghost/admin-x-design-system'; import {useEffect, useState} from 'react'; - -import {Button} from '@tryghost/admin-x-design-system'; - import {useFollow} from '../../hooks/useActivityPubQueries'; interface FollowButtonProps { className?: string; following: boolean; + color?: 'black' | 'grey' | 'outline'; + size?: 'sm' | 'md'; handle: string; type?: 'button' | 'link'; onFollow?: () => void; onUnfollow?: () => void; } +const UnfollowModal = NiceModal.create(({onUnfollow}: {onUnfollow: () => void}) => { + const modal = NiceModal.useModal(); + + return ( + modal.remove()} + onOk={() => { + onUnfollow(); + modal.remove(); + }} + > +

Are you sure you want to unfollow this account?

+
+ ); +}); + const noop = () => {}; const FollowButton: React.FC = ({ className, + color = 'black', following, handle, + size = 'md', type = 'button', onFollow = noop, onUnfollow = noop }) => { const [isFollowing, setIsFollowing] = useState(following); + const [isHovered, setIsHovered] = useState(false); const mutation = useFollow('index', noop, @@ -35,10 +59,13 @@ const FollowButton: React.FC = ({ const handleClick = async () => { if (isFollowing) { - setIsFollowing(false); - onUnfollow(); - - // @TODO: Implement unfollow mutation + NiceModal.show(UnfollowModal, { + onUnfollow: () => { + setIsFollowing(false); + onUnfollow(); + // @TODO: Implement unfollow mutation + } + }); } else { setIsFollowing(true); onFollow(); @@ -53,17 +80,22 @@ const FollowButton: React.FC = ({ return (
@@ -324,8 +326,10 @@ const ViewProfileModal: React.FC = ({ />
@@ -347,9 +351,10 @@ const ViewProfileModal: React.FC = ({
)} {isOverflowing &&
)} From 90506c2eb9547ade193075dda170b77e4076ba5b Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Tue, 28 Jan 2025 11:06:50 +0000 Subject: [PATCH 2/9] Removed the confirmation modal ref https://linear.app/ghost/issue/AP-311/ui-to-unfollow-an-actor --- .../src/components/global/FollowButton.tsx | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/apps/admin-x-activitypub/src/components/global/FollowButton.tsx b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx index 6d70d13772a0..147eaa933a46 100644 --- a/apps/admin-x-activitypub/src/components/global/FollowButton.tsx +++ b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx @@ -1,6 +1,5 @@ -import NiceModal from '@ebay/nice-modal-react'; import clsx from 'clsx'; -import {Button, Modal} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/admin-x-design-system'; import {useEffect, useState} from 'react'; import {useFollow} from '../../hooks/useActivityPubQueries'; @@ -15,25 +14,6 @@ interface FollowButtonProps { onUnfollow?: () => void; } -const UnfollowModal = NiceModal.create(({onUnfollow}: {onUnfollow: () => void}) => { - const modal = NiceModal.useModal(); - - return ( - modal.remove()} - onOk={() => { - onUnfollow(); - modal.remove(); - }} - > -

Are you sure you want to unfollow this account?

-
- ); -}); - const noop = () => {}; const FollowButton: React.FC = ({ @@ -59,17 +39,12 @@ const FollowButton: React.FC = ({ const handleClick = async () => { if (isFollowing) { - NiceModal.show(UnfollowModal, { - onUnfollow: () => { - setIsFollowing(false); - onUnfollow(); - // @TODO: Implement unfollow mutation - } - }); + setIsFollowing(false); + onUnfollow(); + // @TODO: Implement unfollow mutation } else { setIsFollowing(true); onFollow(); - mutation.mutate(handle); } }; From a4935e43c9b7c99adc719847baacd7c272143fde Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Tue, 28 Jan 2025 21:04:21 +0000 Subject: [PATCH 3/9] Updated FollowButton design ref https://linear.app/ghost/issue/AP-311/ui-to-unfollow-an-actor - Refactored and simplified FollowButton so it only has 2 variants: primary (used on profiles, where it's the primary focus of the screen) and secondary (used in lists where there will probably be lots of FollowButtons next to each other) - Fixed import order in useActivityPubQueries.ts --- .../src/components/Search.tsx | 20 ++++++++----------- .../components/activities/ActivityItem.tsx | 4 ++-- .../src/components/global/FollowButton.tsx | 16 +++++++-------- .../components/modals/ViewProfileModal.tsx | 7 ++----- .../src/hooks/useActivityPubQueries.ts | 1 + 5 files changed, 20 insertions(+), 28 deletions(-) diff --git a/apps/admin-x-activitypub/src/components/Search.tsx b/apps/admin-x-activitypub/src/components/Search.tsx index 7062408724dd..0df8c1c83e86 100644 --- a/apps/admin-x-activitypub/src/components/Search.tsx +++ b/apps/admin-x-activitypub/src/components/Search.tsx @@ -57,17 +57,15 @@ const AccountSearchResultItem: React.FC = ({accoun name: account.name, handle: account.handle }}/> -
-
- {account.name} {account.handle} -
-
{new Intl.NumberFormat().format(account.followerCount)} followers
+
+ {account.name} + {account.handle}
@@ -122,17 +120,15 @@ const SuggestedProfile: React.FC = ({profile, update}) => }} > -
-
- {profile.actor.name} {profile.handle} -
-
{new Intl.NumberFormat().format(profile.followerCount)} followers
+
+ {profile.actor.name} + {profile.handle}
diff --git a/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx b/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx index 0cce368e931f..a569f0a9c247 100644 --- a/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx +++ b/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx @@ -23,12 +23,12 @@ const ActivityItem: React.FC = ({children, url = null, onClic const childrenArray = React.Children.toArray(children); const Item = ( -
{ +
{ if (!url && onClick) { onClick(); } }}> -
+
{childrenArray[0]} {childrenArray[1]} {childrenArray[2]} diff --git a/apps/admin-x-activitypub/src/components/global/FollowButton.tsx b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx index 147eaa933a46..f266f220632f 100644 --- a/apps/admin-x-activitypub/src/components/global/FollowButton.tsx +++ b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx @@ -6,10 +6,8 @@ import {useFollow} from '../../hooks/useActivityPubQueries'; interface FollowButtonProps { className?: string; following: boolean; - color?: 'black' | 'grey' | 'outline'; - size?: 'sm' | 'md'; handle: string; - type?: 'button' | 'link'; + type?: 'primary' | 'secondary'; onFollow?: () => void; onUnfollow?: () => void; } @@ -18,11 +16,9 @@ const noop = () => {}; const FollowButton: React.FC = ({ className, - color = 'black', following, handle, - size = 'md', - type = 'button', + type = 'secondary', onFollow = noop, onUnfollow = noop }) => { @@ -53,16 +49,18 @@ const FollowButton: React.FC = ({ setIsFollowing(following); }, [following]); + const color = (type === 'primary') ? 'black' : 'grey'; + const size = (type === 'primary') ? 'md' : 'sm'; + const minWidth = (type === 'primary') ? 'min-w-[96px]' : 'min-w-[88px]'; + return (
{index < actors.length - 1 && } @@ -326,10 +324,9 @@ const ViewProfileModal: React.FC = ({ />
diff --git a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts index c700db66753f..183d1fafbcb9 100644 --- a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts +++ b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts @@ -4,6 +4,7 @@ import { ActivityPubAPI, ActivityPubCollectionResponse, ActivityThread, + Actor, type GetAccountFollowsResponse, type Profile, type SearchResults From c58c15022985804bd068008735ed2d1e9b30e221 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Tue, 28 Jan 2025 19:07:36 +0700 Subject: [PATCH 4/9] Wired up FollowButton to /unfollow API ref https://linear.app/ghost/issue/AP-311 This is very similar to the /follow API so we've mostly copied the existing patterns there. --- .../src/api/activitypub.ts | 6 +++ .../src/components/global/FollowButton.tsx | 16 +++++-- .../src/hooks/useActivityPubQueries.ts | 42 ++++++++++++++++++- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/apps/admin-x-activitypub/src/api/activitypub.ts b/apps/admin-x-activitypub/src/api/activitypub.ts index d2fbed84daf5..2958f86c2382 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.ts @@ -195,6 +195,12 @@ export class ActivityPubAPI { return json as Actor; } + async unfollow(username: string): Promise { + const url = new URL(`.ghost/activitypub/actions/unfollow/${username}`, this.apiUrl); + const json = await this.fetchJSON(url, 'POST'); + return json as Actor; + } + get likedApiUrl() { return new URL(`.ghost/activitypub/liked/${this.handle}`, this.apiUrl); } diff --git a/apps/admin-x-activitypub/src/components/global/FollowButton.tsx b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx index f266f220632f..d03cd1770e61 100644 --- a/apps/admin-x-activitypub/src/components/global/FollowButton.tsx +++ b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import {Button} from '@tryghost/admin-x-design-system'; import {useEffect, useState} from 'react'; -import {useFollow} from '../../hooks/useActivityPubQueries'; +import {useFollow, useUnfollow} from '../../hooks/useActivityPubQueries'; interface FollowButtonProps { className?: string; @@ -25,7 +25,15 @@ const FollowButton: React.FC = ({ const [isFollowing, setIsFollowing] = useState(following); const [isHovered, setIsHovered] = useState(false); - const mutation = useFollow('index', + const unfollowMutation = useUnfollow('index', + noop, + () => { + setIsFollowing(false); + onUnfollow(); + } + ); + + const followMutation = useFollow('index', noop, () => { setIsFollowing(false); @@ -37,11 +45,11 @@ const FollowButton: React.FC = ({ if (isFollowing) { setIsFollowing(false); onUnfollow(); - // @TODO: Implement unfollow mutation + unfollowMutation.mutate(handle); } else { setIsFollowing(true); onFollow(); - mutation.mutate(handle); + followMutation.mutate(handle); } }; diff --git a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts index 183d1fafbcb9..fa1f187cbcca 100644 --- a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts +++ b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts @@ -7,7 +7,8 @@ import { Actor, type GetAccountFollowsResponse, type Profile, - type SearchResults + type SearchResults, + Actor } from '../api/activitypub'; import {Activity} from '../components/activities/ActivityItem'; import { @@ -203,6 +204,45 @@ export function useFollowingForUser(handle: string) { }); } +export function useUnfollow(handle: string, onSuccess: () => void, onError: () => void) { + const queryClient = useQueryClient(); + return useMutation({ + async mutationFn(username: string) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.unfollow(username); + }, + onSuccess(unfollowedActor, fullHandle) { + queryClient.setQueryData([`profile:${fullHandle}`], (currentProfile: unknown) => { + if (!currentProfile) { + return currentProfile; + } + return { + ...currentProfile, + isFollowing: false + }; + }); + + queryClient.setQueryData(['following:index'], (currentFollowing?: Actor[]) => { + if (!currentFollowing) { + return currentFollowing; + } + return currentFollowing.filter(item => item.id !== unfollowedActor.id); + }); + + queryClient.setQueryData(['followingCount:index'], (currentFollowingCount?: number) => { + if (!currentFollowingCount) { + return 0; + } + return currentFollowingCount - 1; + }); + + onSuccess(); + }, + onError + }); +} + export function useFollow(handle: string, onSuccess: () => void, onError: () => void) { const queryClient = useQueryClient(); return useMutation({ From 859d7c49a778425707989e9ca22a16ed50cd09d8 Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Thu, 30 Jan 2025 09:56:42 +0000 Subject: [PATCH 5/9] Fixed button width ref https://linear.app/ghost/issue/AP-311/ui-to-unfollow-an-actor --- apps/admin-x-activitypub/package.json | 2 +- .../admin-x-activitypub/src/components/global/FollowButton.tsx | 2 +- apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/admin-x-activitypub/package.json b/apps/admin-x-activitypub/package.json index 26a555f3578a..2f32386ba5aa 100644 --- a/apps/admin-x-activitypub/package.json +++ b/apps/admin-x-activitypub/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/admin-x-activitypub", - "version": "0.3.54", + "version": "0.3.55", "license": "MIT", "repository": { "type": "git", diff --git a/apps/admin-x-activitypub/src/components/global/FollowButton.tsx b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx index d03cd1770e61..b003ad63169d 100644 --- a/apps/admin-x-activitypub/src/components/global/FollowButton.tsx +++ b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx @@ -65,7 +65,7 @@ const FollowButton: React.FC = ({
+ {index < accounts.length - 1 && } @@ -253,6 +260,12 @@ const FollowersTab: React.FC = () => {
{account.handle}
+ {index < accounts.length - 1 && } From 332838525b4921e1a4d3d23b092709ac737e94ef Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Thu, 30 Jan 2025 18:10:22 +0700 Subject: [PATCH 7/9] fixup! Added FollowButtons to Profile page --- apps/admin-x-activitypub/src/api/activitypub.ts | 2 +- apps/admin-x-activitypub/src/components/Profile.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/admin-x-activitypub/src/api/activitypub.ts b/apps/admin-x-activitypub/src/api/activitypub.ts index 2958f86c2382..fa409b84d98f 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.ts @@ -69,7 +69,7 @@ export type AccountFollowsType = 'following' | 'followers'; type GetAccountResponse = Account -export type FollowAccount = Pick; +export type FollowAccount = Pick & {isFollowing: true}; export interface GetAccountFollowsResponse { accounts: FollowAccount[]; diff --git a/apps/admin-x-activitypub/src/components/Profile.tsx b/apps/admin-x-activitypub/src/components/Profile.tsx index 9d72aacef706..27c4819c0897 100644 --- a/apps/admin-x-activitypub/src/components/Profile.tsx +++ b/apps/admin-x-activitypub/src/components/Profile.tsx @@ -215,7 +215,7 @@ const FollowingTab: React.FC = () => { @@ -263,7 +263,7 @@ const FollowersTab: React.FC = () => { From 1c2f50427b927588e5ab953addcea71207c3fede Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Thu, 30 Jan 2025 18:20:58 +0700 Subject: [PATCH 8/9] Handled updating query cache when unfollowing --- .../src/hooks/useActivityPubQueries.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts index 9705de33d8ad..f18fe410ad8e 100644 --- a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts +++ b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts @@ -7,7 +7,8 @@ import { Actor, type GetAccountFollowsResponse, type Profile, - type SearchResults + type SearchResults, + FollowAccount } from '../api/activitypub'; import {Activity} from '../components/activities/ActivityItem'; import { @@ -229,6 +230,13 @@ export function useUnfollow(handle: string, onSuccess: () => void, onError: () = return currentFollowing.filter(item => item.id !== unfollowedActor.id); }); + queryClient.setQueryData(['follows:index:following'], (currentFollowing?: FollowAccount[]) => { + if (!currentFollowing) { + return currentFollowing + } + return currentFollowing.filter(item => item.id !== unfollowedActor.id); + }); + queryClient.setQueryData(['followingCount:index'], (currentFollowingCount?: number) => { if (!currentFollowingCount) { return 0; @@ -268,6 +276,8 @@ export function useFollow(handle: string, onSuccess: () => void, onError: () => return [followedActor].concat(currentFollowing); }); + queryClient.invalidateQueries(['follows:index:following']); + queryClient.setQueryData(['followingCount:index'], (currentFollowingCount?: number) => { if (!currentFollowingCount) { return 1; From ed3398cfd8022d637965e1932c10f5947c8a9b2c Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Thu, 30 Jan 2025 11:43:44 +0000 Subject: [PATCH 9/9] Fixed linting errors ref https://linear.app/ghost/issue/AP-311/ui-to-unfollow-an-actor --- apps/admin-x-activitypub/src/components/Profile.tsx | 4 ++-- apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/admin-x-activitypub/src/components/Profile.tsx b/apps/admin-x-activitypub/src/components/Profile.tsx index 27c4819c0897..ac573559287b 100644 --- a/apps/admin-x-activitypub/src/components/Profile.tsx +++ b/apps/admin-x-activitypub/src/components/Profile.tsx @@ -214,8 +214,8 @@ const FollowingTab: React.FC = () => { @@ -262,8 +262,8 @@ const FollowersTab: React.FC = () => { diff --git a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts index f18fe410ad8e..1ef6ce630cbc 100644 --- a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts +++ b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts @@ -5,10 +5,10 @@ import { ActivityPubCollectionResponse, ActivityThread, Actor, + FollowAccount, type GetAccountFollowsResponse, type Profile, - type SearchResults, - FollowAccount + type SearchResults } from '../api/activitypub'; import {Activity} from '../components/activities/ActivityItem'; import { @@ -232,7 +232,7 @@ export function useUnfollow(handle: string, onSuccess: () => void, onError: () = queryClient.setQueryData(['follows:index:following'], (currentFollowing?: FollowAccount[]) => { if (!currentFollowing) { - return currentFollowing + return currentFollowing; } return currentFollowing.filter(item => item.id !== unfollowedActor.id); });