Skip to content

Commit c1d8439

Browse files
authored
Merge pull request #5004 from RSSNext/release/mobile/0.5.2
release(mobile): Release v0.5.2
2 parents 65fafb3 + 3b336b9 commit c1d8439

44 files changed

Lines changed: 912 additions & 114 deletions

Some content is hidden

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

apps/desktop/changelog/1.7.0.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# What's new in v1.7.0
2+
3+
## Improvements
4+
5+
- Removed the connection status indicator from the desktop app
6+
7+
## No longer broken
8+
9+
- Fixed duplicate desktop auth session cookies
10+
- Fixed session refresh after cookie updates
11+
- Fixed returning through the Discover route
12+
- Fixed desktop download link
13+
- Fixed MAS review state detection from OTA versions
14+
- Extended API request timeouts
15+
16+
## Thanks
17+
18+
Special thanks to volunteer contributor @cuikaipeng for their valuable contribution

apps/desktop/layer/main/src/lib/auth-cookies.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Session } from "electron"
2-
import { describe, expect, it, vi } from "vitest"
2+
import { beforeEach, describe, expect, it, vi } from "vitest"
33

44
import {
55
buildManagedAuthCookieHeader,
@@ -10,6 +10,10 @@ import {
1010
} from "./auth-cookies"
1111

1212
describe("auth cookies", () => {
13+
beforeEach(() => {
14+
vi.useRealTimers()
15+
})
16+
1317
it("builds a cookie header from managed auth cookies only", () => {
1418
const header = buildManagedAuthCookieHeader([
1519
{ name: "__Secure-better-auth.session_token", value: "session-token" },
@@ -104,6 +108,53 @@ describe("auth cookies", () => {
104108
expect(remove).not.toHaveBeenCalled()
105109
})
106110

111+
it("persists session token cookies across app restarts when the server sends Max-Age", async () => {
112+
vi.useFakeTimers()
113+
vi.setSystemTime(new Date("2026-05-12T00:00:00.000Z"))
114+
115+
const set = vi.fn().mockImplementation(async () => {})
116+
const remove = vi.fn().mockImplementation(async () => {})
117+
const get = vi.fn().mockResolvedValue([])
118+
119+
await persistManagedAuthCookiesFromSetCookieHeader({
120+
apiURL: "https://api.folo.is",
121+
session: {
122+
cookies: { get, set, remove },
123+
} as unknown as Session,
124+
setCookieHeader:
125+
"__Secure-better-auth.session_token=session-token; Max-Age=2592000; Path=/; HttpOnly; Secure; SameSite=None",
126+
})
127+
128+
expect(set).toHaveBeenCalledWith(
129+
expect.objectContaining({
130+
name: "__Secure-better-auth.session_token",
131+
value: "session-token",
132+
expirationDate: 1_781_136_000,
133+
}),
134+
)
135+
})
136+
137+
it("keeps rememberMe=false session token cookies session-scoped", async () => {
138+
const set = vi.fn().mockImplementation(async () => {})
139+
const remove = vi.fn().mockImplementation(async () => {})
140+
const get = vi.fn().mockResolvedValue([])
141+
142+
await persistManagedAuthCookiesFromSetCookieHeader({
143+
apiURL: "https://api.folo.is",
144+
session: {
145+
cookies: { get, set, remove },
146+
} as unknown as Session,
147+
setCookieHeader:
148+
"__Secure-better-auth.session_token=session-token; Path=/; HttpOnly; Secure; SameSite=None",
149+
})
150+
151+
expect(set).toHaveBeenCalledWith(
152+
expect.not.objectContaining({
153+
expirationDate: expect.any(Number),
154+
}),
155+
)
156+
})
157+
107158
it("removes stale duplicate session token cookies while keeping the secure host-only cookie", async () => {
108159
const remove = vi.fn().mockImplementation(async () => {})
109160
const get = vi.fn().mockResolvedValue([

apps/desktop/layer/main/src/lib/auth-cookies.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,18 @@ const shouldRemoveCookie = (cookie: ParsedSetCookie) => {
224224
return false
225225
}
226226

227+
const getCookieExpirationDate = (cookie: ParsedSetCookie) => {
228+
if (cookie.expirationDate !== undefined) {
229+
return cookie.expirationDate
230+
}
231+
232+
if (cookie.maxAge !== undefined && cookie.maxAge > 0) {
233+
return Math.floor(Date.now() / 1000) + cookie.maxAge
234+
}
235+
236+
return
237+
}
238+
227239
export const getManagedAuthCookieNames = () => {
228240
return [...MANAGED_AUTH_COOKIE_NAMES]
229241
}
@@ -368,6 +380,7 @@ export const persistManagedAuthCookiesFromSetCookieHeader = async ({
368380
continue
369381
}
370382

383+
const expirationDate = getCookieExpirationDate(cookie)
371384
const details: CookiesSetDetails = {
372385
url: apiURL,
373386
name: cookie.name,
@@ -377,7 +390,7 @@ export const persistManagedAuthCookiesFromSetCookieHeader = async ({
377390
secure: cookie.secure,
378391
...(cookie.sameSite ? { sameSite: cookie.sameSite } : {}),
379392
...(cookie.domain ? { domain: cookie.domain } : {}),
380-
...(cookie.expirationDate ? { expirationDate: cookie.expirationDate } : {}),
393+
...(expirationDate ? { expirationDate } : {}),
381394
}
382395

383396
await session.cookies.set(details)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest"
2+
3+
const mocks = vi.hoisted(() => {
4+
const env = {
5+
isMacOS: false,
6+
isMAS: false,
7+
isWindows: false,
8+
}
9+
10+
return {
11+
app: {
12+
getName: vi.fn(() => "Folo"),
13+
quit: vi.fn(),
14+
},
15+
buildFromTemplate: vi.fn((template) => ({ template })),
16+
env,
17+
getBadgeCount: vi.fn(() => 0),
18+
getTrayIconPath: vi.fn(() => "/icon.png"),
19+
logger: {
20+
info: vi.fn(),
21+
},
22+
nativeImage: {
23+
createFromPath: vi.fn(() => {
24+
const image = {
25+
resize: vi.fn(() => image),
26+
setTemplateImage: vi.fn(),
27+
}
28+
return image
29+
}),
30+
},
31+
store: {
32+
get: vi.fn(() => true),
33+
set: vi.fn(),
34+
},
35+
trayInstances: [] as Array<{
36+
destroy: ReturnType<typeof vi.fn>
37+
on: ReturnType<typeof vi.fn>
38+
setContextMenu: ReturnType<typeof vi.fn>
39+
setToolTip: ReturnType<typeof vi.fn>
40+
}>,
41+
Tray: class {
42+
constructor() {
43+
const tray = {
44+
destroy: vi.fn(),
45+
on: vi.fn(),
46+
setContextMenu: vi.fn(),
47+
setToolTip: vi.fn(),
48+
}
49+
mocks.trayInstances.push(tray)
50+
return tray
51+
}
52+
},
53+
}
54+
})
55+
56+
vi.mock("electron", () => ({
57+
app: {
58+
getName: mocks.app.getName,
59+
getBadgeCount: mocks.getBadgeCount,
60+
quit: mocks.app.quit,
61+
setBadgeCount: vi.fn(),
62+
},
63+
Menu: {
64+
buildFromTemplate: mocks.buildFromTemplate,
65+
},
66+
nativeImage: mocks.nativeImage,
67+
Tray: mocks.Tray,
68+
}))
69+
70+
vi.mock("~/env", () => mocks.env)
71+
72+
vi.mock("~/helper", () => ({
73+
getTrayIconPath: mocks.getTrayIconPath,
74+
}))
75+
76+
vi.mock("~/logger", () => ({
77+
logger: mocks.logger,
78+
revealLogFile: vi.fn(),
79+
}))
80+
81+
vi.mock("~/manager/window", () => ({
82+
WindowManager: {
83+
getMainWindowOrCreate: vi.fn(() => ({
84+
isMinimized: vi.fn(() => false),
85+
show: vi.fn(),
86+
webContents: {
87+
reload: vi.fn(),
88+
toggleDevTools: vi.fn(),
89+
},
90+
})),
91+
},
92+
}))
93+
94+
vi.mock("~/updater", () => ({
95+
checkForAppUpdates: vi.fn(),
96+
}))
97+
98+
vi.mock("./i18n", () => ({
99+
t: vi.fn((key: string, options?: { name?: string }) =>
100+
options?.name ? `${key} ${options.name}` : key,
101+
),
102+
}))
103+
104+
vi.mock("./store", () => ({
105+
store: mocks.store,
106+
}))
107+
108+
describe("tray", () => {
109+
beforeEach(() => {
110+
vi.resetModules()
111+
vi.clearAllMocks()
112+
113+
mocks.env.isMacOS = false
114+
mocks.env.isMAS = false
115+
mocks.env.isWindows = false
116+
mocks.getBadgeCount.mockReturnValue(0)
117+
mocks.store.get.mockReturnValue(true)
118+
mocks.trayInstances.length = 0
119+
})
120+
121+
it("refreshes the existing tray menu instead of recreating the native tray", async () => {
122+
const { registerAppTray } = await import("./tray")
123+
124+
registerAppTray()
125+
registerAppTray()
126+
127+
expect(mocks.trayInstances).toHaveLength(1)
128+
expect(mocks.trayInstances[0]!.destroy).not.toHaveBeenCalled()
129+
expect(mocks.trayInstances[0]!.setContextMenu).toHaveBeenCalledTimes(2)
130+
})
131+
})

apps/desktop/layer/main/src/lib/tray.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,19 @@ const getTrayContextMenu = () => {
7575
},
7676
])
7777
}
78+
79+
const refreshTrayContextMenu = () => {
80+
if (!tray) return
81+
82+
tray.setContextMenu(getTrayContextMenu())
83+
tray.setToolTip(app.getName())
84+
}
85+
7886
export const registerAppTray = () => {
7987
if (!getTrayConfig()) return
8088
if (tray) {
81-
destroyAppTray()
89+
refreshTrayContextMenu()
90+
return
8291
}
8392

8493
const icon = nativeImage.createFromPath(getTrayIconPath())
@@ -87,10 +96,9 @@ export const registerAppTray = () => {
8796
trayIcon.setTemplateImage(true)
8897
tray = new Tray(trayIcon)
8998

90-
tray.setContextMenu(getTrayContextMenu())
91-
tray.setToolTip(app.getName())
99+
refreshTrayContextMenu()
92100
tray.on("mouse-enter", () => {
93-
tray?.setContextMenu(getTrayContextMenu())
101+
refreshTrayContextMenu()
94102
})
95103
if (isWindows) {
96104
tray.on("click", showWindow)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import * as React from "react"
2+
import { act } from "react"
3+
import type { Root } from "react-dom/client"
4+
import { createRoot } from "react-dom/client"
5+
import { afterEach, beforeAll, describe, expect, test, vi } from "vitest"
6+
7+
import { MarkdownRenderActionContext } from "../context"
8+
import { MarkdownBlockImage } from "./BlockImage"
9+
10+
const { mediaMock, getWrappedElementSizeMock } = vi.hoisted(() => ({
11+
mediaMock: vi.fn(
12+
({
13+
blurhash: _blurhash,
14+
mediaContainerClassName: _mediaContainerClassName,
15+
popper: _popper,
16+
proxy: _proxy,
17+
showFallback: _showFallback,
18+
type: _type,
19+
...props
20+
}: React.ImgHTMLAttributes<HTMLImageElement> & Record<string, unknown>) => (
21+
<img alt="" data-testid="media" {...props} />
22+
),
23+
),
24+
getWrappedElementSizeMock: vi.fn(() => ({ h: 0, w: 640 })),
25+
}))
26+
27+
vi.mock("../../media/Media", () => ({
28+
Media: mediaMock,
29+
}))
30+
31+
vi.mock("~/providers/wrapped-element-provider", () => ({
32+
useWrappedElementSize: getWrappedElementSizeMock,
33+
}))
34+
35+
const renderComponent = async (element: React.ReactNode) => {
36+
const container = document.createElement("div")
37+
document.body.append(container)
38+
39+
const root = createRoot(container)
40+
41+
await act(async () => {
42+
root.render(element)
43+
})
44+
45+
return { container, root }
46+
}
47+
48+
describe("MarkdownBlockImage", () => {
49+
let root: Root | null = null
50+
let container: HTMLElement | null = null
51+
52+
beforeAll(() => {
53+
;(globalThis as typeof globalThis & { React: typeof React }).React = React
54+
;(
55+
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }
56+
).IS_REACT_ACT_ENVIRONMENT = true
57+
})
58+
59+
afterEach(async () => {
60+
if (root) {
61+
await act(async () => {
62+
root?.unmount()
63+
})
64+
}
65+
66+
container?.remove()
67+
root = null
68+
container = null
69+
vi.clearAllMocks()
70+
})
71+
72+
test("passes image context menu events with the resolved image URL", async () => {
73+
const onImageContextMenu = vi.fn()
74+
75+
;({ container, root } = await renderComponent(
76+
<MarkdownRenderActionContext
77+
value={{
78+
ensureAndRenderTimeStamp: () => false,
79+
isAudio: () => false,
80+
onImageContextMenu,
81+
transformUrl: (url) => (url ? new URL(url, "https://example.com/post").href : url),
82+
}}
83+
>
84+
<MarkdownBlockImage src="./image.png" width={700} height={400} />
85+
</MarkdownRenderActionContext>,
86+
))
87+
88+
const image = container?.querySelector('[data-testid="media"]')
89+
const event = new MouseEvent("contextmenu", {
90+
bubbles: true,
91+
cancelable: true,
92+
clientX: 12,
93+
clientY: 34,
94+
})
95+
96+
await act(async () => {
97+
image?.dispatchEvent(event)
98+
})
99+
100+
expect(onImageContextMenu).toHaveBeenCalledWith(
101+
expect.objectContaining({ clientX: 12, clientY: 34 }),
102+
"https://example.com/image.png",
103+
)
104+
})
105+
})

0 commit comments

Comments
 (0)