Skip to content

Commit 30310e6

Browse files
authored
Merge pull request #4835 from RSSNext/release/desktop/1.3.0
release(desktop): Release v1.3.0
2 parents 3c0612e + ef5fcc9 commit 30310e6

File tree

76 files changed

+1478
-2483
lines changed

Some content is hidden

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

76 files changed

+1478
-2483
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
---
2+
name: desktop-release
3+
description: Perform a regular desktop release from the dev branch. Gathers commits since last release, updates changelog, evaluates mainHash changes, bumps version, and creates release PR.
4+
disable-model-invocation: true
5+
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
6+
---
7+
8+
# Desktop Regular Release
9+
10+
Perform a regular desktop release. This skill handles the full release workflow from the `dev` branch.
11+
12+
## Pre-flight checks
13+
14+
1. Confirm the current branch is `dev`. If not, abort with a warning.
15+
2. Run `git pull --rebase` in the repo root to ensure the local branch is up to date.
16+
3. Read `apps/desktop/package.json` to get the current `version` and `mainHash`.
17+
18+
## Step 1: Gather changes since last release
19+
20+
1. Find the last release tag:
21+
```bash
22+
git tag --sort=-creatordate | grep '^desktop/v' | head -1
23+
```
24+
2. Get all commits since that tag on the current branch:
25+
```bash
26+
git log <last-tag>..HEAD --oneline --no-merges
27+
```
28+
3. Categorize commits into:
29+
- **Shiny new things** (feat: commits, new features)
30+
- **Improvements** (refactor:, perf:, chore: improvements, dependency updates)
31+
- **No longer broken** (fix: commits, bug fixes)
32+
- **Thanks** (identify external contributor GitHub usernames from commits)
33+
34+
## Step 2: Update changelog
35+
36+
1. Read `apps/desktop/changelog/next.md`.
37+
2. Present the categorized changes to the user and draft the changelog content.
38+
3. Wait for user confirmation or edits before writing.
39+
4. Write the final content to `apps/desktop/changelog/next.md`, following the template format:
40+
41+
```markdown
42+
# What's new in vNEXT_VERSION
43+
44+
## Shiny new things
45+
46+
- description of new feature
47+
48+
## Improvements
49+
50+
- description of improvement
51+
52+
## No longer broken
53+
54+
- description of fix
55+
56+
## Thanks
57+
58+
Special thanks to volunteer contributors @username for their valuable contributions
59+
```
60+
61+
5. Keep `NEXT_VERSION` as the placeholder - it will be replaced by `apply-changelog.ts` during bump.
62+
63+
## Step 3: Evaluate mainHash
64+
65+
This is critical for determining whether users need a full app update or can use the lightweight renderer hot update.
66+
67+
1. Check what files changed in `apps/desktop/layer/main/` since the last release tag:
68+
```bash
69+
git diff <last-tag>..HEAD --name-only -- apps/desktop/layer/main/
70+
```
71+
2. Also check changes to `apps/desktop/package.json` fields other than version/mainHash (since package.json is included in the hash calculation):
72+
```bash
73+
git diff <last-tag>..HEAD -- apps/desktop/package.json
74+
```
75+
76+
**Decision logic:**
77+
78+
- If there are **NO changes** in `layer/main/` and no meaningful `package.json` changes (only version/mainHash/changelog-related), then mainHash should NOT be updated. Users will get a fast renderer-only hot update.
79+
- If there are **trivial changes** in `layer/main/` (typo fixes, comment changes, logging tweaks) that don't affect runtime behavior, recommend NOT updating mainHash. Present the changes to the user and ask for confirmation.
80+
- If there are **meaningful changes** in `layer/main/` (new features, bug fixes, dependency changes, API changes), mainHash MUST be updated. Users will need a full app update.
81+
82+
Present your analysis to the user with:
83+
84+
- List of changed files in `layer/main/`
85+
- A summary of what changed
86+
- Your recommendation (update or skip mainHash)
87+
- Ask for explicit confirmation
88+
89+
## Step 4: Save old mainHash and execute bump
90+
91+
1. Save the current mainHash from `apps/desktop/package.json` for later comparison.
92+
2. Change directory to `apps/desktop/` and run the bump:
93+
```bash
94+
cd apps/desktop && pnpm bump
95+
```
96+
3. This command will:
97+
- Pull latest changes
98+
- Apply changelog (rename next.md to {version}.md, create new next.md)
99+
- Recalculate mainHash and write to package.json
100+
- Format package.json
101+
- Bump minor version
102+
- Commit with message `release(desktop): release v{NEW_VERSION}`
103+
- Create branch `release/desktop/{NEW_VERSION}`
104+
- Push branch and create PR to `main`
105+
106+
## Step 5: Restore mainHash if skipping update
107+
108+
If Step 3 decided mainHash should NOT be updated, restore the old value now. The bump has already committed, pushed, and created the PR on a new release branch, so we amend the commit and force push. This is safe because the release branch was just created.
109+
110+
1. Change back to the repo root first (Step 4 left the working directory at `apps/desktop/`):
111+
```bash
112+
cd ../..
113+
```
114+
2. Ensure you are on the `release/desktop/{NEW_VERSION}` branch (bump should have switched to it).
115+
3. Replace the recalculated mainHash with the saved old value in `apps/desktop/package.json`.
116+
4. Stage and amend the release commit:
117+
```bash
118+
git add apps/desktop/package.json && git commit --amend --no-edit
119+
```
120+
5. Force push the release branch:
121+
```bash
122+
git push --force origin release/desktop/{NEW_VERSION}
123+
```
124+
125+
If Step 3 decided mainHash SHOULD be updated, skip this step entirely — the bump already wrote the correct new value.
126+
127+
## Step 6: Verify
128+
129+
1. Confirm the PR was created successfully by checking the output.
130+
2. Report the new version number and PR URL to the user.
131+
3. Summarize:
132+
- New version: v{NEW_VERSION}
133+
- mainHash updated: yes/no (and why)
134+
- Changelog highlights
135+
- PR URL
136+
137+
## Reference
138+
139+
- Bump config: `apps/desktop/bump.config.ts`
140+
- Changelog dir: `apps/desktop/changelog/`
141+
- Changelog template: `apps/desktop/changelog/next.template.md`
142+
- mainHash generator: `apps/desktop/plugins/vite/generate-main-hash.ts`
143+
- Hot updater logic: `apps/desktop/layer/main/src/updater/hot-updater.ts`
144+
- CI build workflow: `.github/workflows/build-desktop.yml`
145+
- Tag workflow: `.github/workflows/tag.yml`

apps/desktop/changelog/1.3.0.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# What's new in v1.3.0
2+
3+
## Shiny new things
4+
5+
- Markdown link supports open share feed link directly in the app
6+
7+
## No longer broken
8+
9+
- Fix preserve session when switching API domain
10+
- Fix handle invalid URL parsing in useFeedSafeUrl hook
11+
- Fix shortcuts page freeze
12+
- Fix icon service URL and image proxy URL
13+
14+
## Thanks
15+
16+
Special thanks to volunteer contributors @cuikaipeng @yjl9903 for their valuable contributions
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import type { Cookie, CookiesSetDetails, Session } from "electron"
2+
3+
import { BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN } from "~/constants/app"
4+
5+
import { logger } from "../logger"
6+
7+
const LEGACY_PROD_API_URL = "https://api.follow.is"
8+
const BETTER_AUTH_SESSION_DATA_COOKIE_NAME = "better-auth.session_data"
9+
10+
const isBetterAuthSessionTokenCookie = (cookieName: string) => {
11+
return cookieName.includes(BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN)
12+
}
13+
14+
const isBetterAuthSessionCookie = (cookieName: string) => {
15+
return (
16+
cookieName.includes(BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN) ||
17+
cookieName.includes(BETTER_AUTH_SESSION_DATA_COOKIE_NAME)
18+
)
19+
}
20+
21+
const toCookieSetDetails = (cookie: Cookie, url: string, domain: string): CookiesSetDetails => {
22+
const details: CookiesSetDetails = {
23+
url,
24+
name: cookie.name,
25+
value: cookie.value,
26+
domain,
27+
path: cookie.path,
28+
secure: cookie.secure,
29+
httpOnly: cookie.httpOnly,
30+
sameSite: cookie.sameSite,
31+
}
32+
33+
if (!cookie.session && cookie.expirationDate) {
34+
details.expirationDate = cookie.expirationDate
35+
}
36+
37+
return details
38+
}
39+
40+
export const migrateAuthCookiesToNewApiDomain = async (
41+
cookieSession: Session,
42+
options: {
43+
currentApiURL: string
44+
legacyApiURL?: string
45+
},
46+
) => {
47+
const legacyApiURL = options.legacyApiURL ?? LEGACY_PROD_API_URL
48+
if (!options.currentApiURL || options.currentApiURL === legacyApiURL) {
49+
return
50+
}
51+
52+
const currentHost = new URL(options.currentApiURL).hostname
53+
const legacyHost = new URL(legacyApiURL).hostname
54+
55+
if (currentHost === legacyHost) {
56+
return
57+
}
58+
59+
const currentDomainCookies = await cookieSession.cookies.get({
60+
domain: currentHost,
61+
})
62+
const hasCurrentDomainSessionTokenCookie = currentDomainCookies.some((cookie) =>
63+
isBetterAuthSessionTokenCookie(cookie.name),
64+
)
65+
if (hasCurrentDomainSessionTokenCookie) {
66+
return
67+
}
68+
69+
const legacyDomainCookies = await cookieSession.cookies.get({
70+
domain: legacyHost,
71+
})
72+
const legacySessionCookies = legacyDomainCookies.filter((cookie) =>
73+
isBetterAuthSessionCookie(cookie.name),
74+
)
75+
76+
if (legacySessionCookies.length === 0) {
77+
return
78+
}
79+
80+
await Promise.all(
81+
legacySessionCookies.map((cookie) => {
82+
return cookieSession.cookies.set(
83+
toCookieSetDetails(cookie, options.currentApiURL, currentHost),
84+
)
85+
}),
86+
)
87+
88+
logger.info(
89+
`Migrated ${legacySessionCookies.length} auth cookie(s) from ${legacyHost} to ${currentHost}`,
90+
)
91+
}

apps/desktop/layer/main/src/manager/bootstrap.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { join } from "pathe"
1313
import { WindowManager } from "~/manager/window"
1414

1515
import { isMacOS } from "../env"
16+
import { migrateAuthCookiesToNewApiDomain } from "../lib/auth-cookie-migration"
1617
import { handleUrlRouting } from "../lib/router"
1718
import { store } from "../lib/store"
1819
import { updateNotificationsToken } from "../lib/user"
@@ -81,6 +82,10 @@ export class BootstrapManager {
8182
callback({ cancel: false, requestHeaders: details.requestHeaders })
8283
})
8384

85+
await migrateAuthCookiesToNewApiDomain(session.defaultSession, {
86+
currentApiURL: env.VITE_API_URL,
87+
})
88+
8489
// Bypass CORS for PostHog analytics
8590
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
8691
const url = new URL(details.url)

apps/desktop/layer/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ import {
77
TooltipTrigger,
88
} from "@follow/components/ui/tooltip/index.jsx"
99
import { useCorrectZIndex } from "@follow/components/ui/z-index/ctx.js"
10-
import { cn, stopPropagation } from "@follow/utils"
10+
import { env } from "@follow/shared/env.desktop"
11+
import { feedSyncServices } from "@follow/store/feed/store"
12+
import { cn, parseSafeUrl, stopPropagation } from "@follow/utils"
13+
import type { MouseEvent } from "react"
1114
import { use, useCallback } from "react"
1215
import { useTranslation } from "react-i18next"
1316
import { toast } from "sonner"
1417

18+
import { navigateEntry } from "~/hooks/biz/useNavigateEntry"
1519
import { copyToClipboard } from "~/lib/clipboard"
1620

1721
import { MarkdownRenderActionContext } from "../context"
@@ -21,6 +25,7 @@ export const MarkdownLink: Component<LinkProps> = (props) => {
2125
const { t } = useTranslation()
2226

2327
const populatedFullHref = transformUrl(props.href)
28+
const shareFeedInfo = parseShareFeedInfo(populatedFullHref)
2429

2530
const handleCopyLink = useCallback(async () => {
2631
try {
@@ -34,6 +39,25 @@ export const MarkdownLink: Component<LinkProps> = (props) => {
3439
}
3540
}, [populatedFullHref, t])
3641

42+
const handleClickLink = useCallback(
43+
async (event: MouseEvent<HTMLAnchorElement>) => {
44+
stopPropagation(event)
45+
46+
if (!shareFeedInfo) {
47+
return
48+
}
49+
event.preventDefault()
50+
51+
const view = await resolveShareFeedView(shareFeedInfo)
52+
navigateEntry({
53+
feedId: shareFeedInfo.id,
54+
entryId: null,
55+
view,
56+
})
57+
},
58+
[shareFeedInfo],
59+
)
60+
3761
const parseTimeStamp = isAudio(populatedFullHref)
3862
const zIndex = useCorrectZIndex(0)
3963
if (parseTimeStamp) {
@@ -58,7 +82,7 @@ export const MarkdownLink: Component<LinkProps> = (props) => {
5882
title={props.title}
5983
target="_blank"
6084
rel="noreferrer"
61-
onClick={stopPropagation}
85+
onClick={handleClickLink}
6286
>
6387
{props.children}
6488

@@ -93,3 +117,46 @@ export const MarkdownLink: Component<LinkProps> = (props) => {
93117
</Tooltip>
94118
)
95119
}
120+
121+
const parseShareFeedInfo = (href?: string) => {
122+
if (!href) return null
123+
124+
const baseUrl = parseSafeUrl(env.VITE_WEB_URL)
125+
if (!baseUrl) return null
126+
127+
let parsedUrl: URL
128+
try {
129+
parsedUrl = new URL(href, baseUrl)
130+
} catch {
131+
return null
132+
}
133+
134+
if (parsedUrl.host !== baseUrl.host) return null
135+
136+
const pathParts = parsedUrl.pathname.split("/").filter(Boolean)
137+
if (pathParts.length !== 3 || pathParts[0] !== "share" || pathParts[1] !== "feeds") {
138+
return null
139+
}
140+
141+
const viewParam = parsedUrl.searchParams.get("view")
142+
const view = viewParam ? Number.parseInt(viewParam, 10) : undefined
143+
144+
return {
145+
id: pathParts[2]!,
146+
view: Number.isNaN(view) ? undefined : view,
147+
}
148+
}
149+
150+
const resolveShareFeedView = async (info: { id: string; view?: number }) => {
151+
if (typeof info.view === "number") {
152+
return info.view
153+
}
154+
155+
const data = await feedSyncServices.fetchFeedById({ id: info.id }).catch(() => {})
156+
const analyticsView = data?.analytics?.view
157+
if (typeof analyticsView === "number") {
158+
return analyticsView
159+
}
160+
161+
return 0
162+
}

apps/desktop/layer/renderer/src/hooks/common/useFeedSafeUrl.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ export const useFeedSafeUrl = (entryId: string) => {
3131
}
3232

3333
if (href.startsWith("http")) {
34-
const domain = new URL(href).hostname
35-
if (domain === "localhost") return null
34+
try {
35+
const domain = new URL(href).hostname
36+
if (domain === "localhost") return null
37+
} catch {
38+
return null
39+
}
3640

3741
return href
3842
}

0 commit comments

Comments
 (0)