diff --git a/server/src/nest/journey/journey-domain.service.ts b/server/src/nest/journey/journey-domain.service.ts index 3f4967580..6b7f74bdb 100644 --- a/server/src/nest/journey/journey-domain.service.ts +++ b/server/src/nest/journey/journey-domain.service.ts @@ -14,7 +14,7 @@ import { TrekPhotosRepository } from '../photos/trek-photos.repository'; const JP_SELECT = ` gp.id, jep.entry_id, gp.photo_id, gp.caption, jep.sort_order, gp.shared, gp.created_at, tp.provider, tp.asset_id, tp.owner_id, tp.file_path, tp.thumbnail_path, tp.width, tp.height, - tp.media_type, tp.duration_ms + tp.media_type, tp.duration_ms, tp.taken_at, tp.lat, tp.lng `; const JP_JOIN = `journey_entry_photos jep @@ -24,7 +24,7 @@ const JP_JOIN = `journey_entry_photos jep const GALLERY_SELECT = ` gp.id, gp.journey_id, gp.photo_id, gp.caption, gp.shared, gp.sort_order, gp.created_at, tp.provider, tp.asset_id, tp.owner_id, tp.file_path, tp.thumbnail_path, tp.width, tp.height, - tp.media_type, tp.duration_ms + tp.media_type, tp.duration_ms, tp.taken_at, tp.lat, tp.lng `; const GALLERY_JOIN = 'journey_photos gp JOIN trek_photos tp ON tp.id = gp.photo_id'; diff --git a/server/src/nest/journey/journey-share.service.ts b/server/src/nest/journey/journey-share.service.ts index 2aefb766d..c75519626 100644 --- a/server/src/nest/journey/journey-share.service.ts +++ b/server/src/nest/journey/journey-share.service.ts @@ -130,7 +130,7 @@ export class JourneyShareService { const photos = this.db.prepare(` SELECT gp.id, jep.entry_id, gp.photo_id, gp.caption, jep.sort_order, gp.shared, gp.created_at, tkp.provider, tkp.asset_id, tkp.owner_id, tkp.file_path, tkp.thumbnail_path, tkp.width, tkp.height, - tkp.media_type, tkp.duration_ms + tkp.media_type, tkp.duration_ms, tkp.taken_at, tkp.lat, tkp.lng FROM journey_entry_photos jep JOIN journey_photos gp ON gp.id = jep.journey_photo_id JOIN trek_photos tkp ON tkp.id = gp.photo_id @@ -146,7 +146,7 @@ export class JourneyShareService { const gallery = this.db.prepare(` SELECT gp.id, gp.journey_id, gp.photo_id, gp.caption, gp.shared, gp.sort_order, gp.created_at, tkp.provider, tkp.asset_id, tkp.owner_id, tkp.file_path, tkp.thumbnail_path, tkp.width, tkp.height, - tkp.media_type, tkp.duration_ms + tkp.media_type, tkp.duration_ms, tkp.taken_at, tkp.lat, tkp.lng FROM journey_photos gp JOIN trek_photos tkp ON tkp.id = gp.photo_id WHERE gp.journey_id = ? @@ -183,6 +183,7 @@ export class JourneyShareService { const projected: Record = { ...e }; if (!shareMap) { projected.location_lat = null; projected.location_lng = null; } if (!shareGallery) projected.photos = []; + else if (!shareMap) projected.photos = stripPhotoGps(e.photos); return projected; }); } else if (shareMap) { @@ -208,7 +209,11 @@ export class JourneyShareService { status: journey.status, }, entries: publicEntries, - gallery: shareGallery ? gallery : [], + // A photo now carries the coordinates it was taken at, which is a location the + // owner never typed and may not expect to publish. It follows share_map, the + // same switch the entry coordinates follow — otherwise a gallery-only share + // would hand out places the map was deliberately turned off for. + gallery: shareGallery ? (shareMap ? gallery : stripPhotoGps(gallery)) : [], stats, permissions: { share_timeline: shareTimeline, @@ -218,3 +223,8 @@ export class JourneyShareService { }; } } + +/** Drop capture coordinates from a photo list, keeping everything else. */ +function stripPhotoGps(photos: T[] | undefined | null): T[] { + return (photos ?? []).map(p => ({ ...(p as Record), lat: null, lng: null })) as T[]; +} diff --git a/server/tests/unit/nest/journey-share.service.test.ts b/server/tests/unit/nest/journey-share.service.test.ts index 0f48104a0..005f946ef 100644 --- a/server/tests/unit/nest/journey-share.service.test.ts +++ b/server/tests/unit/nest/journey-share.service.test.ts @@ -495,6 +495,55 @@ describe('getPublicJourney', () => { expect(e.story).toBeUndefined(); // narrative withheld }); + // #1614 — a photo now carries the coordinates it was taken at. That is a place + // the owner never typed, so it has to follow the same switch the entry + // coordinates follow rather than riding in on the gallery flag. + it('JOURNEY-SHARE-025: withholds photo capture coordinates when the map flag is off', () => { + const { user } = createUser(testDb); + const journey = createJourney(testDb, user.id); + const entry = createJourneyEntry(testDb, journey.id, user.id, { + type: 'entry', title: 'Day 1', story: 'notes', entry_date: '2026-05-01', + }); + const trekId = insertJourneyPhoto(entry.id, { ownerId: user.id }); + testDb.prepare('UPDATE trek_photos SET lat = ?, lng = ?, taken_at = ? WHERE id = ?') + .run(48.8584, 2.2945, '2026-05-01T10:00:00Z', trekId); + + const { token } = svc.createOrUpdateJourneyShareLink(journey.id, user.id, { + share_timeline: true, share_gallery: true, share_map: false, + }); + + const result = svc.getPublicJourney(token)!; + const gallery = result.gallery as Record[]; + expect(gallery).toHaveLength(1); + expect(gallery[0].lat).toBeNull(); + expect(gallery[0].lng).toBeNull(); + // The capture time is not a location and stays — it is what a blog view sorts by. + expect(gallery[0].taken_at).toBe('2026-05-01T10:00:00Z'); + + const inline = (result.entries[0] as Record).photos as Record[]; + expect(inline[0].lat).toBeNull(); + expect(inline[0].lng).toBeNull(); + }); + + it('JOURNEY-SHARE-026: hands out photo coordinates once the map is shared', () => { + const { user } = createUser(testDb); + const journey = createJourney(testDb, user.id); + const entry = createJourneyEntry(testDb, journey.id, user.id, { + type: 'entry', title: 'Day 1', entry_date: '2026-05-01', + }); + const trekId = insertJourneyPhoto(entry.id, { ownerId: user.id }); + testDb.prepare('UPDATE trek_photos SET lat = ?, lng = ? WHERE id = ?') + .run(48.8584, 2.2945, trekId); + + const { token } = svc.createOrUpdateJourneyShareLink(journey.id, user.id, { + share_timeline: true, share_gallery: true, share_map: true, + }); + + const gallery = svc.getPublicJourney(token)!.gallery as Record[]; + expect(gallery[0].lat).toBe(48.8584); + expect(gallery[0].lng).toBe(2.2945); + }); + it('JOURNEY-SHARE-024: strips inline entry photos (and their asset metadata) when the gallery is off', () => { const { user } = createUser(testDb); const journey = createJourney(testDb, user.id);