Skip to content

Commit 3c521a8

Browse files
Merge branch 'main' into fix/25020-tag-disappears-dropdown
2 parents 159c0f8 + d99fb48 commit 3c521a8

113 files changed

Lines changed: 1610 additions & 1325 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/commands/commit.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git diff:*), Bash(git commit:*)
3+
description: Generate a Ghost-style commit message and optionally commit changes
4+
---
5+
6+
## Context
7+
- Current git status: !`git status`
8+
- Current git diff (staged and unstaged changes): !`git diff HEAD`
9+
- Stage modified files: !`git add`
10+
11+
## Your task
12+
13+
Create a commit message following Ghost's commit conventions:
14+
15+
1. Based on the uncommitted changes shown above, draft a commit message following the format and style described in @.github/CONTRIBUTING.md
16+
2. Show the commit message to the user
17+
3. Ask: "Would you like me to commit these changes? (yes/no)"
18+
4. If the user says yes:
19+
- Stage all modified files
20+
- Commit using the generated message
21+
- Run current git status to confirm
22+
5. If a user says no, just stop
23+
24+
Important:
25+
- DO NOT push to remote unless explicitly asked
26+
- Follow the exact Ghost commit format

.github/workflows/ci.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,3 +1203,47 @@ jobs:
12031203
token: ${{ secrets.TINYBIRD_TOKEN }}
12041204
workspace: 'Production'
12051205
slack-webhook: ${{ secrets.ANALYTICS_SLACK_WEBHOOK_URL }}
1206+
1207+
deploy_admin:
1208+
needs: [
1209+
job_setup,
1210+
job_required_tests
1211+
]
1212+
name: Deploy Admin
1213+
runs-on: ubuntu-latest
1214+
if: |
1215+
always()
1216+
&& needs.job_setup.result == 'success'
1217+
&& needs.job_setup.outputs.is_main == 'true'
1218+
&& needs.job_required_tests.result == 'success'
1219+
steps:
1220+
- uses: actions/checkout@v4
1221+
- uses: actions/setup-node@v4
1222+
env:
1223+
FORCE_COLOR: 0
1224+
with:
1225+
node-version: ${{ env.NODE_VERSION }}
1226+
- name: Restore caches
1227+
uses: ./.github/actions/restore-cache
1228+
env:
1229+
DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }}
1230+
- name: Build Admin
1231+
env:
1232+
GHOST_CDN_URL: https://assets.ghost.io/admin-forward/
1233+
run: yarn nx run admin:build
1234+
- name: Upload build artifact
1235+
id: upload-artifacts
1236+
uses: actions/upload-artifact@v4
1237+
with:
1238+
name: admin-build
1239+
path: apps/admin/dist
1240+
- name: Deploy Admin
1241+
uses: aurelien-baudet/workflow-dispatch@v4
1242+
with:
1243+
token: ${{ secrets.CANARY_DOCKER_BUILD }}
1244+
workflow: .github/workflows/deploy-admin.yml
1245+
ref: 'refs/heads/main' # this is the ref of Ghost-Moya
1246+
repo: TryGhost/Ghost-Moya
1247+
inputs: '{"admin_build_artifact_id":"${{ steps.upload-artifacts.outputs.artifact-id }}", "admin_build_artifact_run_id":"${{ github.run_id }}"}'
1248+
wait-for-completion-timeout: 25m
1249+
wait-for-completion-interval: 30s

apps/activitypub/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tryghost/activitypub",
3-
"version": "1.0.15",
3+
"version": "1.0.18",
44
"license": "MIT",
55
"repository": {
66
"type": "git",

apps/activitypub/src/api/activitypub.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,10 @@ export class ActivityPubAPI {
450450
return this.getPaginatedPosts('.ghost/activitypub/v1/feed/reader', next);
451451
}
452452

453+
async getGlobalFeed(next?: string): Promise<PaginatedPostsResponse> {
454+
return this.getPaginatedPosts('.ghost/activitypub/v1/feed/global', next);
455+
}
456+
453457
async getPostsByAccount(handle: string, next?: string): Promise<PaginatedPostsResponse> {
454458
return this.getPaginatedPosts(`.ghost/activitypub/v1/posts/${handle}`, next);
455459
}

apps/activitypub/src/components/feed/TableOfContents.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ const HEADING_PADDINGS = {
141141

142142
const TableOfContentsView: React.FC<TableOfContentsViewProps> = ({items, activeHeading, onItemClick, onOpenChange}) => {
143143
const [open, setOpen] = useState(false);
144-
const timeoutRef = React.useRef<NodeJS.Timeout>();
144+
const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>();
145145

146146
React.useEffect(() => {
147147
return () => {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@tryghost/shade';
3+
import {useFeatureFlags} from '@src/lib/feature-flags';
4+
import {useFeedMode} from '@src/hooks/use-feed-mode';
5+
6+
const FeedModeDropdown: React.FC = () => {
7+
const {isEnabled} = useFeatureFlags();
8+
const {feedMode, setFeedMode} = useFeedMode(isEnabled('global-feed'));
9+
10+
return (
11+
<Select value={feedMode} onValueChange={setFeedMode}>
12+
<SelectTrigger className='w-[140px]'>
13+
<SelectValue />
14+
</SelectTrigger>
15+
<SelectContent>
16+
<SelectItem value='discover'>Discover</SelectItem>
17+
<SelectItem value='following'>Following</SelectItem>
18+
</SelectContent>
19+
</Select>
20+
);
21+
};
22+
23+
export default FeedModeDropdown;

apps/activitypub/src/components/layout/Header/Header.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import BackButton from '@src/components/global/BackButton';
2+
import FeedModeDropdown from './FeedModeDropdown';
23
import React from 'react';
34
import useActiveRoute from '@src/hooks/use-active-route';
45
import {Button, H1, LucideIcon} from '@tryghost/shade';
56
import {useBaseRoute, useNavigationStack, useRouteHasParams} from '@tryghost/admin-x-framework';
7+
import {useFeatureFlags} from '@src/lib/feature-flags';
68

79
interface HeaderTitleProps {
810
title: string;
911
backIcon: boolean;
12+
showFeedModeDropdown?: boolean;
1013
}
1114

1215
interface MobileMenuButtonProps {
@@ -17,10 +20,20 @@ interface HeaderProps {
1720
onToggleMobileSidebar: () => void;
1821
}
1922

20-
const HeaderTitle: React.FC<HeaderTitleProps> = ({title, backIcon}) => {
23+
const HeaderTitle: React.FC<HeaderTitleProps> = ({title, backIcon, showFeedModeDropdown}) => {
2124
if (backIcon) {
2225
return <BackButton className='-ml-2' />;
2326
}
27+
28+
if (showFeedModeDropdown) {
29+
return (
30+
<div className='flex grow items-center justify-between'>
31+
<H1 className='max-md:text-[2.4rem]'>{title}</H1>
32+
<FeedModeDropdown />
33+
</div>
34+
);
35+
}
36+
2437
return (
2538
<H1 className='max-md:text-[2.4rem]'>{title}</H1>
2639
);
@@ -43,6 +56,7 @@ const Header: React.FC<HeaderProps> = ({onToggleMobileSidebar}) => {
4356
const baseRoute = useBaseRoute();
4457
const routeHasParams = useRouteHasParams();
4558
const activeRoute = useActiveRoute();
59+
const {isEnabled} = useFeatureFlags();
4660

4761
// Logic for special pages
4862
let onlyBackButton = false;
@@ -57,6 +71,9 @@ const Header: React.FC<HeaderProps> = ({onToggleMobileSidebar}) => {
5771
// Avoid back button on main routes
5872
const backActive = (canGoBack && routeHasParams) || activeRoute?.showBackButton === true;
5973

74+
// Show feed mode dropdown on reader route with global-feed flag enabled
75+
const showFeedModeDropdown = baseRoute === 'reader' && isEnabled('global-feed');
76+
6077
return (
6178
<>
6279
{onlyBackButton ?
@@ -69,6 +86,7 @@ const Header: React.FC<HeaderProps> = ({onToggleMobileSidebar}) => {
6986
<div className='relative flex h-[102px] items-center justify-between gap-5 px-[min(4vw,32px)] before:absolute before:inset-x-[min(4vw,32px)] before:bottom-0 before:block before:border-b before:border-gray-200 before:content-[""] max-md:h-[68px] before:dark:border-gray-950'>
7087
<HeaderTitle
7188
backIcon={backActive}
89+
showFeedModeDropdown={showFeedModeDropdown}
7290
title={activeRoute?.pageTitle || ''}
7391
/>
7492
<MobileMenuButton onToggleMobileSidebar={onToggleMobileSidebar} />

apps/activitypub/src/components/layout/Sidebar/Sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const Sidebar: React.FC<SidebarProps> = ({isMobileSidebarOpen}) => {
4646
<div className='isolate flex w-full flex-col items-start gap-6 pl-6 pt-6'>
4747
<div className='flex h-[52px] w-full items-center'>
4848
<Dialog open={isSearchOpen} onOpenChange={setIsSearchOpen}>
49-
<DialogTrigger className='w-full'>
49+
<DialogTrigger className='mt-0.5 w-full'>
5050
<SearchInput />
5151
</DialogTrigger>
5252
<DialogContent>

apps/activitypub/src/hooks/use-activity-pub-queries.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ const QUERY_KEYS = {
8585
},
8686
feed: ['feed'],
8787
inbox: ['inbox'],
88+
globalFeed: ['global_feed'],
8889
postsByAccount: ['account_posts'],
8990
postsLikedByAccount: ['account_liked_posts'],
9091
notifications: (handle: string) => ['notifications', handle],
@@ -97,6 +98,7 @@ function updateLikeCache(queryClient: QueryClient, id: string, liked: boolean) {
9798
const queryKeys = [
9899
QUERY_KEYS.feed,
99100
QUERY_KEYS.inbox,
101+
QUERY_KEYS.globalFeed,
100102
QUERY_KEYS.postsLikedByAccount,
101103
QUERY_KEYS.profilePosts(null)
102104
];
@@ -166,6 +168,7 @@ function updateFollowCache(queryClient: QueryClient, handle: string, authorHandl
166168
const queryKeys = [
167169
QUERY_KEYS.feed,
168170
QUERY_KEYS.inbox,
171+
QUERY_KEYS.globalFeed,
169172
QUERY_KEYS.profilePosts('index')
170173
];
171174

@@ -493,6 +496,7 @@ function updateReplyCache(queryClient: QueryClient, id: string, delta: number) {
493496
const queryKeys = [
494497
QUERY_KEYS.feed,
495498
QUERY_KEYS.inbox,
499+
QUERY_KEYS.globalFeed,
496500
QUERY_KEYS.profilePosts('index'),
497501
QUERY_KEYS.postsLikedByAccount
498502
];
@@ -744,6 +748,7 @@ export function useBlockMutationForUser(handle: string) {
744748
);
745749
queryClient.invalidateQueries({queryKey: QUERY_KEYS.feed});
746750
queryClient.invalidateQueries({queryKey: QUERY_KEYS.inbox});
751+
queryClient.invalidateQueries({queryKey: QUERY_KEYS.globalFeed});
747752
},
748753
onError(error: {message: string, statusCode: number}) {
749754
if (error.statusCode === 429) {
@@ -789,6 +794,7 @@ function updateRepostCache(queryClient: QueryClient, id: string, reposted: boole
789794
const queryKeys = [
790795
QUERY_KEYS.feed,
791796
QUERY_KEYS.inbox,
797+
QUERY_KEYS.globalFeed,
792798
QUERY_KEYS.profilePosts(null)
793799
];
794800

@@ -1718,6 +1724,42 @@ export function useInboxForUser(options: {enabled: boolean}) {
17181724
return {inboxQuery, updateInboxActivity};
17191725
}
17201726

1727+
export function useGlobalFeedForUser(options: {enabled: boolean}) {
1728+
const queryKey = QUERY_KEYS.globalFeed;
1729+
const queryClient = useQueryClient();
1730+
1731+
const globalFeedQuery = useInfiniteQuery({
1732+
queryKey,
1733+
enabled: options.enabled,
1734+
staleTime: 20 * 1000, // 20s
1735+
async queryFn({pageParam}: {pageParam?: string}) {
1736+
const siteUrl = await getSiteUrl();
1737+
const api = createActivityPubAPI('index', siteUrl);
1738+
return api.getGlobalFeed(pageParam).then((response) => {
1739+
return {
1740+
posts: response.posts.map(mapPostToActivity),
1741+
next: response.next
1742+
};
1743+
});
1744+
},
1745+
getNextPageParam(prevPage) {
1746+
return prevPage.next;
1747+
}
1748+
});
1749+
1750+
const updateGlobalFeedActivity = (id: string, updated: Partial<Activity>) => {
1751+
updateActivityInPaginatedCollection(
1752+
queryClient,
1753+
queryKey,
1754+
'posts',
1755+
id,
1756+
activity => ({...activity, ...updated})
1757+
);
1758+
};
1759+
1760+
return {globalFeedQuery, updateGlobalFeedActivity};
1761+
}
1762+
17211763
export function usePostsByAccount(profileHandle: string, options: {enabled: boolean}) {
17221764
const queryKey = QUERY_KEYS.profilePosts(profileHandle === 'me' ? 'index' : profileHandle);
17231765
const queryClient = useQueryClient();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {useState} from 'react';
2+
3+
export type FeedMode = 'discover' | 'following';
4+
5+
export const FEED_MODE_STORAGE_KEY = 'ghost-ap-reader-feed';
6+
7+
export const useFeedMode = (enabled: boolean) => {
8+
const [feedMode, setFeedModeState] = useState<FeedMode>(() => {
9+
if (!enabled) {
10+
return 'following';
11+
}
12+
13+
if (typeof window !== 'undefined') {
14+
const stored = localStorage.getItem(FEED_MODE_STORAGE_KEY);
15+
return (stored === 'discover' ? 'discover' : 'following') as FeedMode;
16+
}
17+
return 'following';
18+
});
19+
20+
const setFeedMode = (mode: FeedMode) => {
21+
setFeedModeState(mode);
22+
localStorage.setItem(FEED_MODE_STORAGE_KEY, mode);
23+
};
24+
25+
return {feedMode, setFeedMode};
26+
};

0 commit comments

Comments
 (0)