v3.0.19 Bug Fixes (#992)

* fix(mcp): replace relative oauth constent redirect by absolute redirect derived from APP_URL (#987)

* feat(journey): convert HEIC/HEIF uploads to JPEG for cross-platform compatibility

HEIC is an Apple-only format not recognised as an image by many browsers
and platforms. heic-to (lazy-loaded) now converts HEIC/HEIF files to JPEG
before upload in both the gallery and entry editor photo pickers.
Embedded metadata (EXIF, GPS) may be lost during conversion — documented
in the Journey Journal wiki page.

* fix(journey): skip heic-to import for non-HEIC files to avoid test env failures

* fix(notifications): prevent double-escaping HTML in password reset emails

buildPasswordResetHtml passed a pre-built HTML block to buildEmailHtml,
which then escaped it again — rendering raw tags as plain text in the email.
This commit is contained in:
Julien G.
2026-05-13 10:13:17 +02:00
committed by GitHub
parent 7f87dc1ce1
commit da3cba2de3
7 changed files with 36 additions and 6 deletions
+7
View File
@@ -11,6 +11,7 @@
"@react-pdf/renderer": "^4.3.2", "@react-pdf/renderer": "^4.3.2",
"axios": "^1.6.7", "axios": "^1.6.7",
"dexie": "^4.4.2", "dexie": "^4.4.2",
"heic-to": "^1.4.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-react": "^0.344.0", "lucide-react": "^0.344.0",
"mapbox-gl": "^3.22.0", "mapbox-gl": "^3.22.0",
@@ -5827,6 +5828,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/heic-to": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/heic-to/-/heic-to-1.4.2.tgz",
"integrity": "sha512-y69thwxfNcEm2Vk8lbOD/cMabnvMJyOREfJYiCHcXCDqlfcPyJoBhyRc8+iDe1B95LRfpbTOpzxzY1xbRkdwBA==",
"license": "LGPL-3.0"
},
"node_modules/hsl-to-hex": { "node_modules/hsl-to-hex": {
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
+1
View File
@@ -18,6 +18,7 @@
"@react-pdf/renderer": "^4.3.2", "@react-pdf/renderer": "^4.3.2",
"axios": "^1.6.7", "axios": "^1.6.7",
"dexie": "^4.4.2", "dexie": "^4.4.2",
"heic-to": "^1.4.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-react": "^0.344.0", "lucide-react": "^0.344.0",
"mapbox-gl": "^3.22.0", "mapbox-gl": "^3.22.0",
+5 -2
View File
@@ -1,5 +1,6 @@
import { useEffect, useState, useRef, useCallback, useMemo } from 'react' import { useEffect, useState, useRef, useCallback, useMemo } from 'react'
import { formatLocationName } from '../utils/formatters' import { formatLocationName } from '../utils/formatters'
import { normalizeImageFiles } from '../utils/convertHeic'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { useParams, useNavigate } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { useJourneyStore } from '../store/journeyStore' import { useJourneyStore } from '../store/journeyStore'
@@ -1027,8 +1028,9 @@ function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick,
if (!files?.length) return if (!files?.length) return
setGalleryUploading(true) setGalleryUploading(true)
try { try {
const normalized = await normalizeImageFiles(files)
const formData = new FormData() const formData = new FormData()
for (const f of files) formData.append('photos', f) for (const f of normalized) formData.append('photos', f)
await journeyApi.uploadGalleryPhotos(journeyId, formData) await journeyApi.uploadGalleryPhotos(journeyId, formData)
toast.success(t('journey.photosUploaded', { count: files.length })) toast.success(t('journey.photosUploaded', { count: files.length }))
onRefresh() onRefresh()
@@ -2265,7 +2267,8 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
if (!files?.length) return if (!files?.length) return
// Queue files locally until Save so cancel/close actually discards. This // Queue files locally until Save so cancel/close actually discards. This
// keeps photo behavior consistent with text fields — no silent persistence. // keeps photo behavior consistent with text fields — no silent persistence.
setPendingFiles(prev => [...prev, ...Array.from(files)]) const normalized = await normalizeImageFiles(files)
setPendingFiles(prev => [...prev, ...normalized])
} }
return ( return (
+17
View File
@@ -0,0 +1,17 @@
function looksLikeHeic(file: File): boolean {
const ext = file.name.split('.').pop()?.toLowerCase() ?? ''
return ext === 'heic' || ext === 'heif' || file.type === 'image/heic' || file.type === 'image/heif'
}
export async function normalizeImageFile(file: File): Promise<File> {
if (!looksLikeHeic(file)) return file
const { isHeic, heicTo } = await import('heic-to')
if (!(await isHeic(file))) return file
const blob = await heicTo({ blob: file, type: 'image/jpeg', quality: 0.92 })
const jpegName = file.name.replace(/\.(heic|heif)$/i, '.jpg')
return new File([blob], jpegName, { type: 'image/jpeg' })
}
export async function normalizeImageFiles(files: FileList | File[]): Promise<File[]> {
return Promise.all(Array.from(files).map(normalizeImageFile))
}
+2 -1
View File
@@ -147,7 +147,8 @@ export const trekOAuthProvider: OAuthServerProvider = {
if (params.state) qs.set('state', params.state); if (params.state) qs.set('state', params.state);
if (params.resource) qs.set('resource', params.resource.href); if (params.resource) qs.set('resource', params.resource.href);
res.redirect(302, `/oauth/consent?${qs.toString()}`); const base = getMcpSafeUrl().replace(/\/+$/, '');
res.redirect(302, `${base}/oauth/consent?${qs.toString()}`);
}, },
// Not called because skipLocalPkceValidation = true. // Not called because skipLocalPkceValidation = true.
+3 -3
View File
@@ -316,12 +316,12 @@ export function getEventText(lang: string, event: NotifEventType, params: Record
// ── Email HTML builder ───────────────────────────────────────────────────── // ── Email HTML builder ─────────────────────────────────────────────────────
export function buildEmailHtml(subject: string, body: string, lang: string, navigateTarget?: string): string { export function buildEmailHtml(subject: string, body: string, lang: string, navigateTarget?: string, rawBody = false): string {
const s = I18N[lang] || I18N.en; const s = I18N[lang] || I18N.en;
const appUrl = getAppUrl(); const appUrl = getAppUrl();
const ctaHref = escapeHtml(navigateTarget ? `${appUrl}${navigateTarget}` : (appUrl || '')); const ctaHref = escapeHtml(navigateTarget ? `${appUrl}${navigateTarget}` : (appUrl || ''));
const safeSubject = escapeHtml(subject); const safeSubject = escapeHtml(subject);
const safeBody = escapeHtml(body); const safeBody = rawBody ? body : escapeHtml(body);
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html> <html>
@@ -396,7 +396,7 @@ function buildPasswordResetHtml(subject: string, strings: PasswordResetStrings,
<p style="margin:0 0 10px 0; font-size:13px; color:#6B7280;">${safeExpiry}</p> <p style="margin:0 0 10px 0; font-size:13px; color:#6B7280;">${safeExpiry}</p>
<p style="margin:0; font-size:13px; color:#6B7280;">${safeIgnore}</p> <p style="margin:0; font-size:13px; color:#6B7280;">${safeIgnore}</p>
`; `;
return buildEmailHtml(subject, block, lang); return buildEmailHtml(subject, block, lang, undefined, true);
} }
/** /**
+1
View File
@@ -37,6 +37,7 @@ Each entry corresponds to a day in your journey. The entry editor provides:
- **Weather** — choose one of six values: Sunny, Partly cloudy, Cloudy, Rainy, Stormy, Cold. - **Weather** — choose one of six values: Sunny, Partly cloudy, Cloudy, Rainy, Stormy, Cold.
- **Photos** — attach photos to the entry. The first photo becomes the card thumbnail in list views. - **Photos** — attach photos to the entry. The first photo becomes the card thumbnail in list views.
> **Note on HEIC files:** HEIC is an Apple-only format that many browsers and platforms do not recognise as an image. To ensure broad compatibility, HEIC/HEIF files are automatically converted to JPEG before upload. This conversion may result in the loss of embedded metadata (EXIF data such as GPS coordinates, camera information, etc.).
- **Pros / Cons** — optional verdict cards. Add items to a **Pros** list (thumbs-up) or a **Cons** list (thumbs-down) to summarise what you loved or what could have been better. These are stored in the `pros_cons.pros` and `pros_cons.cons` arrays on the entry. - **Pros / Cons** — optional verdict cards. Add items to a **Pros** list (thumbs-up) or a **Cons** list (thumbs-down) to summarise what you loved or what could have been better. These are stored in the `pros_cons.pros` and `pros_cons.cons` arrays on the entry.
- **Tags** — free-form labels (e.g. "hidden gem", "best meal"). - **Tags** — free-form labels (e.g. "hidden gem", "best meal").
- **Location** — pin the entry to a map location. - **Location** — pin the entry to a map location.