From 4ecb6c957712f91efa9b73fa4423e96b30e803d4 Mon Sep 17 00:00:00 2001 From: Dylan Martin Date: Tue, 29 Sep 2026 21:39:45 -0500 Subject: [PATCH 1/3] fix(mobilehog): upload attachments with Expo files Use expo-file-system File objects for multipart photo uploads so Expo fetch can read each form-data part. Generated-By: PostHog Desktop Task-Id: b9f2e11a-748d-45c9-8f24-2b6b33235ff4 --- products/desktop/apps/mobilehog/package.json | 1 + .../desktop/apps/mobilehog/src/lib/attachments.ts | 11 +++++------ products/desktop/pnpm-lock.yaml | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/products/desktop/apps/mobilehog/package.json b/products/desktop/apps/mobilehog/package.json index 93ded301f66c..3f667a55ca8e 100644 --- a/products/desktop/apps/mobilehog/package.json +++ b/products/desktop/apps/mobilehog/package.json @@ -46,6 +46,7 @@ "expo-crypto": "~57.0.1", "expo-dev-client": "~57.0.19", "expo-device": "~57.0.2", + "expo-file-system": "~57.0.7", "expo-font": "~57.0.4", "expo-glass-effect": "~57.0.3", "expo-haptics": "~57.0.3", diff --git a/products/desktop/apps/mobilehog/src/lib/attachments.ts b/products/desktop/apps/mobilehog/src/lib/attachments.ts index de400f062dc7..b346473a1284 100644 --- a/products/desktop/apps/mobilehog/src/lib/attachments.ts +++ b/products/desktop/apps/mobilehog/src/lib/attachments.ts @@ -2,6 +2,7 @@ import type { PreparedTaskArtifactUpload, TaskArtifactUploadRequest, } from "@posthog/api-client/posthog-client"; +import { File } from "expo-file-system"; import * as ImagePicker from "expo-image-picker"; import { getClient } from "@/lib/client"; @@ -51,7 +52,6 @@ function uploadRequests(photos: Photo[]): TaskArtifactUploadRequest[] { })); } -// Streams each file straight from disk to object storage; nothing is base64 encoded. async function postFiles( photos: Photo[], prepared: PreparedTaskArtifactUpload[], @@ -65,11 +65,10 @@ async function postFiles( )) { form.append(key, value); } - form.append("file", { - uri: photo.uri, - name: photo.name, - type: photo.mimeType, - } as unknown as Blob); + // Expo fetch rejects React Native's legacy { uri, name, type } part. + // File supplies the bytes() contract that its multipart encoder requires; + // React Native's FormData type does not yet include Expo's File support. + form.append("file", new File(photo.uri) as unknown as Blob); const response = await fetch(artifact.presigned_post.url, { method: "POST", body: form, diff --git a/products/desktop/pnpm-lock.yaml b/products/desktop/pnpm-lock.yaml index 09704342729b..5164d3d04384 100644 --- a/products/desktop/pnpm-lock.yaml +++ b/products/desktop/pnpm-lock.yaml @@ -695,6 +695,9 @@ importers: expo-device: specifier: ~57.0.2 version: 57.0.2(expo@57.0.23) + expo-file-system: + specifier: ~57.0.7 + version: 57.0.7(expo@57.0.23)(react-native@0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.2.17)(react@19.2.6)) expo-font: specifier: ~57.0.4 version: 57.0.4(expo@57.0.23)(react-native@0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.2.17)(react@19.2.6))(react@19.2.6) From 9d70d5cfe0a783350670e9c513c66caba03379f4 Mon Sep 17 00:00:00 2001 From: Dylan Martin Date: Tue, 29 Sep 2026 21:45:15 -0500 Subject: [PATCH 2/3] fix(mobilehog): show only the user's tasks Scope recent and searched task lists to the signed-in user and separate cached results by user ID. Generated-By: PostHog Desktop Task-Id: b9f2e11a-748d-45c9-8f24-2b6b33235ff4 --- .../desktop/apps/mobilehog/src/lib/queries.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/products/desktop/apps/mobilehog/src/lib/queries.ts b/products/desktop/apps/mobilehog/src/lib/queries.ts index 2522ad1a60b3..9910895055bd 100644 --- a/products/desktop/apps/mobilehog/src/lib/queries.ts +++ b/products/desktop/apps/mobilehog/src/lib/queries.ts @@ -44,13 +44,22 @@ export function useTaskPages(search: string, archived: boolean) { const scope = archived ? "archived" : "active"; return useInfiniteQuery({ queryKey: search - ? [...keys.tasks, "pages", scope, "search", search] - : [...keys.tasks, "pages", scope], + ? [ + ...keys.tasks, + "created-by", + session?.userId, + "pages", + scope, + "search", + search, + ] + : [...keys.tasks, "created-by", session?.userId, "pages", scope], initialPageParam: 0, queryFn: async ({ pageParam }) => { const page = await getClient().getTasksPage({ basic: true, archived, + createdBy: session?.userId, search: search || undefined, ordering: "-last_activity_at", limit: PAGE_SIZE, @@ -69,11 +78,14 @@ export function useTaskPages(search: string, archived: boolean) { export function useTasks(search = "") { const session = useAuth((s) => s.session); return useQuery({ - queryKey: search ? [...keys.tasks, "search", search] : keys.tasks, + queryKey: search + ? [...keys.tasks, "created-by", session?.userId, "search", search] + : [...keys.tasks, "created-by", session?.userId], queryFn: async () => visibleTasks( await getClient().getTasks({ basic: true, + createdBy: session?.userId, search: search || undefined, }), ), From e083369e7db4c6683258960b9b994fadd91e9244 Mon Sep 17 00:00:00 2001 From: Dylan Martin Date: Wed, 30 Sep 2026 00:05:42 -0500 Subject: [PATCH 3/3] fix(mobilehog): use Expo fetch for photo uploads Generated-By: PostHog Desktop Task-Id: 3313e91d-d757-49f3-9553-dc14b9d26cc9 --- products/desktop/apps/mobilehog/package.json | 4 +- .../mobilehog/src/lib/attachments.test.ts | 74 +++++++++++++++++++ .../apps/mobilehog/src/lib/attachments.ts | 1 + .../desktop/apps/mobilehog/vitest.config.ts | 15 ++++ products/desktop/pnpm-lock.yaml | 3 + 5 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 products/desktop/apps/mobilehog/src/lib/attachments.test.ts create mode 100644 products/desktop/apps/mobilehog/vitest.config.ts diff --git a/products/desktop/apps/mobilehog/package.json b/products/desktop/apps/mobilehog/package.json index 3f667a55ca8e..fc479875ab34 100644 --- a/products/desktop/apps/mobilehog/package.json +++ b/products/desktop/apps/mobilehog/package.json @@ -9,6 +9,7 @@ "prebuild": "expo prebuild", "prebuild:clean": "expo prebuild --clean", "typecheck": "tsc --noEmit", + "test": "vitest run", "lint": "biome check .", "lint:fix": "biome check --write .", "clean": "node ../../scripts/rimraf.mjs .turbo .expo" @@ -79,7 +80,8 @@ }, "devDependencies": { "@types/react": "^19.2.0", - "typescript": "~5.9.2" + "typescript": "~5.9.2", + "vitest": "^4.1.8" }, "private": true } diff --git a/products/desktop/apps/mobilehog/src/lib/attachments.test.ts b/products/desktop/apps/mobilehog/src/lib/attachments.test.ts new file mode 100644 index 000000000000..c98fb3d06389 --- /dev/null +++ b/products/desktop/apps/mobilehog/src/lib/attachments.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => { + const photoBytes = new Uint8Array([137, 80, 78, 71]); + class MockFile extends Blob { + constructor(_uri: string) { + super([photoBytes], { type: "image/png" }); + } + } + + return { + client: { + finalizeTaskStagedArtifactUploads: vi.fn(), + prepareTaskStagedArtifactUploads: vi.fn(), + }, + expoFetch: vi.fn(), + MockFile, + photoBytes, + }; +}); + +vi.mock("expo/fetch", () => ({ fetch: mocks.expoFetch })); +vi.mock("expo-file-system", () => ({ File: mocks.MockFile })); +vi.mock("expo-image-picker", () => ({ + UIImagePickerPreferredAssetRepresentationMode: { Compatible: "compatible" }, + launchImageLibraryAsync: vi.fn(), +})); +vi.mock("@/lib/client", () => ({ getClient: () => mocks.client })); + +import { uploadStagedPhotos } from "./attachments"; + +beforeEach(() => { + vi.clearAllMocks(); + mocks.client.prepareTaskStagedArtifactUploads.mockResolvedValue([ + { + id: "prepared-id", + presigned_post: { + fields: { key: "artifact-key" }, + url: "https://uploads.example.com", + }, + }, + ]); + mocks.client.finalizeTaskStagedArtifactUploads.mockResolvedValue([ + { id: "artifact-id" }, + ]); +}); + +describe("uploadStagedPhotos", () => { + it("sends the selected photo bytes through Expo fetch", async () => { + let uploadedBytes: Uint8Array | undefined; + mocks.expoFetch.mockImplementation(async (_url, init) => { + const file = (init?.body as FormData).get("file") as Blob; + uploadedBytes = new Uint8Array(await file.arrayBuffer()); + return { ok: true }; + }); + + const result = await uploadStagedPhotos("task-id", [ + { + id: "photo-id", + uri: "file:///photo.png", + name: "photo.png", + mimeType: "image/png", + size: mocks.photoBytes.byteLength, + }, + ]); + + expect(uploadedBytes).toEqual(mocks.photoBytes); + expect(mocks.expoFetch).toHaveBeenCalledWith( + "https://uploads.example.com", + expect.objectContaining({ method: "POST" }), + ); + expect(result).toEqual(["artifact-id"]); + }); +}); diff --git a/products/desktop/apps/mobilehog/src/lib/attachments.ts b/products/desktop/apps/mobilehog/src/lib/attachments.ts index b346473a1284..8eff4314d2e9 100644 --- a/products/desktop/apps/mobilehog/src/lib/attachments.ts +++ b/products/desktop/apps/mobilehog/src/lib/attachments.ts @@ -2,6 +2,7 @@ import type { PreparedTaskArtifactUpload, TaskArtifactUploadRequest, } from "@posthog/api-client/posthog-client"; +import { fetch } from "expo/fetch"; import { File } from "expo-file-system"; import * as ImagePicker from "expo-image-picker"; import { getClient } from "@/lib/client"; diff --git a/products/desktop/apps/mobilehog/vitest.config.ts b/products/desktop/apps/mobilehog/vitest.config.ts new file mode 100644 index 000000000000..efc3e2036956 --- /dev/null +++ b/products/desktop/apps/mobilehog/vitest.config.ts @@ -0,0 +1,15 @@ +import path from "node:path"; +import { defineConfig } from "vitest/config"; +import { trunkTestOptions } from "../../vitest.config.base"; + +export default defineConfig({ + test: { + ...trunkTestOptions, + environment: "node", + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); diff --git a/products/desktop/pnpm-lock.yaml b/products/desktop/pnpm-lock.yaml index 5164d3d04384..a58fbfb2c16d 100644 --- a/products/desktop/pnpm-lock.yaml +++ b/products/desktop/pnpm-lock.yaml @@ -792,6 +792,9 @@ importers: typescript: specifier: ~5.9.2 version: 5.9.3 + vitest: + specifier: ^4.1.8 + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.2.0)(@vitest/ui@4.1.8)(jsdom@26.1.0)(msw@2.12.8(@types/node@25.2.0)(typescript@5.9.3))(vite@7.3.5(@types/node@25.2.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0)) apps/web: dependencies: