mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
feat(journey): carry capture metadata into the payload, and keep it behind share_map
The gallery rows now hand the client taken_at, lat and lng, which is what the map and a capture-ordered gallery will read. The coordinates needed a projection before they could ship. A photo's capture location is a place the owner never typed and may not expect to publish, so it follows share_map — the same switch the entry coordinates already follow. Without that, turning the map off and leaving the gallery on would have handed out exactly the locations the owner had just withheld. The capture time is not a location and stays either way; a blog-style view sorts by it.
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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<string, unknown> = { ...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<T>(photos: T[] | undefined | null): T[] {
|
||||
return (photos ?? []).map(p => ({ ...(p as Record<string, unknown>), lat: null, lng: null })) as T[];
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>[];
|
||||
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<string, unknown>).photos as Record<string, unknown>[];
|
||||
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<string, unknown>[];
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user