diff --git a/client/src/components/Journey/JourneyDetailPageSettingsDialog.test.tsx b/client/src/components/Journey/JourneyDetailPageSettingsDialog.test.tsx
index 365654835..3adb569c9 100644
--- a/client/src/components/Journey/JourneyDetailPageSettingsDialog.test.tsx
+++ b/client/src/components/Journey/JourneyDetailPageSettingsDialog.test.tsx
@@ -98,6 +98,19 @@ describe('JourneySettingsDialog', () => {
await screen.findByRole('button', { name: /create share link/i })
})
+ it('FE-JRN-SETTINGS-2683: lists the people a linked trip lets in under their own heading, and nothing without', () => {
+ const { unmount } = mountDialog()
+ expect(screen.queryByText('Through trips')).not.toBeInTheDocument()
+ unmount()
+
+ mountDialog(buildJourney({
+ trip_access: [{ user_id: 7, role: 'viewer', username: 'lena', avatar: null, trip_id: 5, trip_title: 'Italy Trip' }],
+ }))
+ expect(screen.getByText('Through trips')).toBeInTheDocument()
+ expect(screen.getByText('lena')).toBeInTheDocument()
+ expect(screen.getByText('via Italy Trip')).toBeInTheDocument()
+ })
+
it('FE-JRN-SETTINGS-002: saves the edited title and subtitle', async () => {
const user = userEvent.setup()
const { onSaved } = mountDialog()
diff --git a/client/src/components/Journey/JourneyDetailPageSettingsDialog.tsx b/client/src/components/Journey/JourneyDetailPageSettingsDialog.tsx
index 0439a5f7c..9036172b7 100644
--- a/client/src/components/Journey/JourneyDetailPageSettingsDialog.tsx
+++ b/client/src/components/Journey/JourneyDetailPageSettingsDialog.tsx
@@ -7,7 +7,7 @@ import { DeleteButton, DialogButton, DialogFooter, DialogHeader, DialogSection,
import { AddRowButton, EditorField, INPUT, Segmented } from '../shared/dialogParts'
import JourneyShareSection from './JourneyShareSection'
import { useJourneySettings } from './useJourneySettings'
-import type { JourneyContributor, JourneyDetail } from '../../store/journeyStore'
+import type { JourneyContributor, JourneyDetail, JourneyTripAccess } from '../../store/journeyStore'
import { pickGradient } from '../../pages/journeyDetail/JourneyDetailPage.helpers'
import { AddTripDialog } from './JourneyDetailPageAddTripDialog'
import ToggleSwitch from '../Settings/ToggleSwitch'
@@ -277,6 +277,8 @@ export function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite,
{t('journey.contributors.invite')}
+
+
@@ -324,3 +326,32 @@ export function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite,
>
)
}
+
+/**
+ * Who a trip's linked journal lets in (#2683), under its own heading below the
+ * contributors: shown here, but managed in that trip's settings, so read only.
+ */
+function TripAccessSection({ access, roleLabel }: {
+ access: JourneyTripAccess[] | undefined
+ roleLabel: (role: JourneyContributor['role']) => string
+}) {
+ const { t } = useTranslation()
+ if (!access?.length) return null
+ return (
+
+ {t('journey.contributors.viaTripsHint')}
+
+ {access.map(a => (
+
+
+
+
{a.username}
+
{t('journey.contributors.viaTrip', { trip: a.trip_title })}
+
+
{roleLabel(a.role)}
+
+ ))}
+
+
+ )
+}
diff --git a/client/src/mobile/screens/journey/MJourneySettingsSheet.tsx b/client/src/mobile/screens/journey/MJourneySettingsSheet.tsx
index be3774e48..fe2f56f12 100644
--- a/client/src/mobile/screens/journey/MJourneySettingsSheet.tsx
+++ b/client/src/mobile/screens/journey/MJourneySettingsSheet.tsx
@@ -1,8 +1,5 @@
import { useRef, useState } from 'react'
-import {
- X, Image, Plus, Trash2, UserPlus, Link as LinkIcon,
- List, Grid3x3, MapPin, Route, Archive, ArchiveRestore, Undo2,
-} from 'lucide-react'
+import { X, Image, Plus, Trash2, UserPlus, Link as LinkIcon, List, Grid3x3, MapPin, Route, Archive, ArchiveRestore, Undo2, Link2 } from 'lucide-react'
import MSheet from '../../components/MSheet'
import MIconBtn from '../../components/MIconBtn'
import MToggle from '../../components/MToggle'
@@ -237,6 +234,26 @@ export default function MJourneySettingsSheet({
{t('journey.contributors.invite')}
+ {/* Through a trip's linked journal (#2683): shown, but changed in that trip. */}
+ {!!journey.trip_access?.length && (
+ <>
+
{t('journey.contributors.viaTrips')}
+ {t('journey.contributors.viaTripsHint')}
+ {journey.trip_access.map(a => (
+
+
+
+ {a.username}
+ {t('journey.contributors.viaTrip', { trip: a.trip_title })}
+
+
+ {a.role}
+
+
+ ))}
+ >
+ )}
+
{/* Public share */}
{t('journey.share.publicShare')}
{!shareLink ? (
diff --git a/client/src/store/journeyStore.ts b/client/src/store/journeyStore.ts
index 1faa1b1c2..9b44f78a4 100644
--- a/client/src/store/journeyStore.ts
+++ b/client/src/store/journeyStore.ts
@@ -141,11 +141,23 @@ export interface JourneyContributor {
avatar?: string | null
}
+/** Someone a trip's linked journal lets in, with the trip they come through (#2683). */
+export interface JourneyTripAccess {
+ user_id: number
+ role: 'editor' | 'viewer'
+ username: string
+ avatar?: string | null
+ trip_id: number
+ trip_title: string
+}
+
export interface JourneyDetail extends Journey {
entries: JourneyEntry[]
gallery: GalleryPhoto[]
trips: JourneyTrip[]
contributors: JourneyContributor[]
+ /** Who the trips' journal links let in besides the contributors, managed in those trips. */
+ trip_access?: JourneyTripAccess[]
stats: { entries: number; photos: number; places: number }
hide_skeletons?: boolean
/** How many suggestions were waved away one at a time, so the settings sheet can offer them back. */
diff --git a/client/tests/unit/mobile/journey/MJourneySettingsSheet.test.tsx b/client/tests/unit/mobile/journey/MJourneySettingsSheet.test.tsx
index b76732601..41af7234a 100644
--- a/client/tests/unit/mobile/journey/MJourneySettingsSheet.test.tsx
+++ b/client/tests/unit/mobile/journey/MJourneySettingsSheet.test.tsx
@@ -89,6 +89,17 @@ describe('MJourneySettingsSheet', () => {
expect(screen.getByText('8 places')).toBeInTheDocument();
});
+ it('FE-MOB-JSET-2683: lists the people a linked trip lets in, and nothing without', async () => {
+ const { unmount } = setup();
+ expect(screen.queryByText('Through trips')).not.toBeInTheDocument();
+ unmount();
+
+ setup({ trip_access: [{ user_id: 7, role: 'editor', username: 'lena', avatar: null, trip_id: 3, trip_title: 'Tokyo trip' }] });
+ expect(await screen.findByText('Through trips')).toBeInTheDocument();
+ expect(screen.getByText('lena')).toBeInTheDocument();
+ expect(screen.getByText('via Tokyo trip')).toBeInTheDocument();
+ });
+
it('FE-MOB-JSET-002: shows the empty hint when no trip is linked', async () => {
setup({ trips: [] });
expect(await screen.findByText('No trips linked')).toBeInTheDocument();
diff --git a/server/src/db/repositories/JourneyContributors.repository.ts b/server/src/db/repositories/JourneyContributors.repository.ts
index 11d30a4ee..1942da289 100644
--- a/server/src/db/repositories/JourneyContributors.repository.ts
+++ b/server/src/db/repositories/JourneyContributors.repository.ts
@@ -1,6 +1,12 @@
import type { JourneyContributors } from '../entities/JourneyContributors.entity';
import type { DB } from '../kysely/db';
-import { readLinkHideSkeletons, readLinkRole, type TripJournalLinkKyselyDB } from './TripJournalLinks.repository';
+import {
+ linkReach,
+ readLinkHideSkeletons,
+ readLinkRole,
+ type TripJournalLinkKyselyDB,
+ type TripJournalLinkRole,
+} from './TripJournalLinks.repository';
import { TrekRepository } from './_shared/trek-repository';
/** JG18's contributor-list row (`getJourneyFull`) — the junction plus the joined username/avatar. */
@@ -16,6 +22,16 @@ export interface JourneyContributorListRow {
/** The `journey_contributors`/`users` tables {@link JourneyContributorsRepository.listForJourney} reads. */
type JourneyContributorListKyselyDB = Pick;
+/** Someone a trip's journal link lets in, with the trip they come through (#2683). */
+export interface JourneyTripAccessRow {
+ user_id: number;
+ role: TripJournalLinkRole;
+ username: string;
+ avatar: string | null;
+ trip_id: number;
+ trip_title: string;
+}
+
/** The table the access reads below take a person's own contributor row from. */
type JourneyContributorRowKyselyDB = Pick;
@@ -79,6 +95,24 @@ export class JourneyContributorsRepository extends TrekRepository {
+ const direct = new Set((await this.listForJourney(journeyId)).map((c) => c.user_id));
+ const rows = await linkReach(this.kysely())
+ .innerJoin('users as u', 'u.id', 'm.user_id')
+ .select(['m.user_id', 'm.role', 'u.username', 'u.avatar', 't.id as trip_id', 't.title as trip_title'])
+ .where('l.journey_id', '=', journeyId)
+ .orderBy('u.username', 'asc')
+ .execute();
+ return rows
+ .filter((r) => !direct.has(r.user_id))
+ .map((r) => ({ ...r, role: r.role === 'editor' ? 'editor' : 'viewer' }));
+ }
+
/** JG18 — `getJourneyFull`'s contributor list, joined to `users` for `username`/`avatar` (`avatar_url` is computed in the SERVICE, unchanged). */
async listForJourney(journeyId: number): Promise {
const rows = await this.kysely()
diff --git a/server/src/nest/journey/journey-domain.service.ts b/server/src/nest/journey/journey-domain.service.ts
index d8a3485f0..752dbfe46 100644
--- a/server/src/nest/journey/journey-domain.service.ts
+++ b/server/src/nest/journey/journey-domain.service.ts
@@ -270,12 +270,10 @@ export class JourneyDomainService {
// linked trips (JG17)
const trips = await this.journeyTripsRepo.listForJourney(journeyId);
- // contributors (JG18)
- const contributorsRaw = await this.contributorsRepo.listForJourney(journeyId);
- const contributors = contributorsRaw.map((c) => ({
- ...c,
- avatar_url: avatarUrl(c),
- }));
+ const withAvatar = (rows: R[]) => rows.map((r) => ({ ...r, avatar_url: avatarUrl(r) }));
+ const contributors = withAvatar(await this.contributorsRepo.listForJourney(journeyId)); // JG18
+ // Who a trip's journal link lets in besides them (#2683), shown apart in the settings.
+ const tripAccess = withAvatar(await this.contributorsRepo.listTripAccessForJourney(journeyId));
// stats
const entryCount = entries.filter((e) => e.type === 'entry').length;
@@ -306,6 +304,7 @@ export class JourneyDomainService {
gallery,
trips,
contributors,
+ trip_access: tripAccess,
stats: { entries: entryCount, photos: photoCount, places: places.length },
hide_skeletons: hideSkeletons,
// What the eye toggle cannot say: how much was waved away one at a time, and
diff --git a/server/tests/unit/db/repositories/TripJournalLinks.repository.test.ts b/server/tests/unit/db/repositories/TripJournalLinks.repository.test.ts
index 1c80a2a36..28e01207e 100644
--- a/server/tests/unit/db/repositories/TripJournalLinks.repository.test.ts
+++ b/server/tests/unit/db/repositories/TripJournalLinks.repository.test.ts
@@ -171,4 +171,21 @@ describe('TripJournalLinksRepository', () => {
expect(await links.findForTrip(s.trip.id)).toBeNull();
expect(await links.listMembers(s.trip.id)).toEqual([]);
});
+ it('TJLREPO-008: the journal lists who the links let in, per trip, apart from its contributors', async () => {
+ const s = await scene();
+ await links.upsertLink(s.trip.id, s.journal.id, s.owner.id, 1);
+ await links.replaceMembers(
+ s.trip.id,
+ [
+ { user_id: s.tom.id, role: 'editor' },
+ { user_id: s.lena.id, role: 'viewer' },
+ ],
+ 1,
+ );
+ await addJourneyContributor(t, s.journal.id, s.tom.id, 'viewer');
+
+ const access = await contributors.listTripAccessForJourney(s.journal.id);
+ expect(access.map((a) => [a.username, a.role, a.trip_id])).toEqual([['Lena', 'viewer', s.trip.id]]);
+ expect(access[0]?.trip_title).toBe(s.trip.title);
+ });
});
diff --git a/shared/src/i18n/ar/journey.ts b/shared/src/i18n/ar/journey.ts
index 624d6af8c..069b80e28 100644
--- a/shared/src/i18n/ar/journey.ts
+++ b/shared/src/i18n/ar/journey.ts
@@ -241,6 +241,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': 'عبر الرحلات',
+ 'journey.contributors.viaTripsHint': 'يمكنهم فتح هذه اليوميات لأن رحلة ترتبط بها. غيّر ذلك من إعدادات الرحلة.',
+ 'journey.contributors.viaTrip': 'عبر {trip}',
'journey.share.publicShare': 'Public Share', // en-fallback
'journey.share.createLink': 'Create share link', // en-fallback
'journey.share.linkCreated': 'Share link created', // en-fallback
diff --git a/shared/src/i18n/az/journey.ts b/shared/src/i18n/az/journey.ts
index f792dcdae..f6f855de0 100644
--- a/shared/src/i18n/az/journey.ts
+++ b/shared/src/i18n/az/journey.ts
@@ -206,6 +206,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': '{username} bu səyahət gündəliyindən çıxarılsın?',
'journey.contributors.removed': 'İştirakçı çıxarıldı',
'journey.contributors.removeFailed': 'İştirakçını çıxarmaq mümkün olmadı',
+ 'journey.contributors.viaTrips': 'Səyahətlər vasitəsilə',
+ 'journey.contributors.viaTripsHint':
+ 'Bu gündəliyi aça bilirlər, çünki bir səyahət onu bağlayır. Bunu səyahətin parametrlərində dəyişin.',
+ 'journey.contributors.viaTrip': '{trip} vasitəsilə',
'journey.share.publicShare': 'İctimai paylaşım',
'journey.share.createLink': 'Paylaşım linki yarat',
diff --git a/shared/src/i18n/br/journey.ts b/shared/src/i18n/br/journey.ts
index c6cd13f08..acc2ccd59 100644
--- a/shared/src/i18n/br/journey.ts
+++ b/shared/src/i18n/br/journey.ts
@@ -265,6 +265,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': 'Por meio de viagens',
+ 'journey.contributors.viaTripsHint':
+ 'Podem abrir este diário porque uma viagem o vincula. Altere isso nas configurações da viagem.',
+ 'journey.contributors.viaTrip': 'por {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/ca/journey.ts b/shared/src/i18n/ca/journey.ts
index 1511fc76e..fcf942aa6 100644
--- a/shared/src/i18n/ca/journey.ts
+++ b/shared/src/i18n/ca/journey.ts
@@ -265,6 +265,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': "Vols eliminar {username} d'aquesta travesia?",
'journey.contributors.removed': 'Col·laborador eliminat',
'journey.contributors.removeFailed': "No s'ha pogut eliminar el col·laborador",
+ 'journey.contributors.viaTrips': 'A través de viatges',
+ 'journey.contributors.viaTripsHint':
+ 'Poden obrir aquest diari perquè un viatge el vincula. Canvia-ho a la configuració del viatge.',
+ 'journey.contributors.viaTrip': 'a través de {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/cs/journey.ts b/shared/src/i18n/cs/journey.ts
index 44ec4fcc9..9bce80035 100644
--- a/shared/src/i18n/cs/journey.ts
+++ b/shared/src/i18n/cs/journey.ts
@@ -270,6 +270,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': 'Přes cesty',
+ 'journey.contributors.viaTripsHint':
+ 'Mohou tento deník otevřít, protože ho cesta propojuje. Změňte to v nastavení cesty.',
+ 'journey.contributors.viaTrip': 'přes {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/de/journey.ts b/shared/src/i18n/de/journey.ts
index f0d0047f0..72ad48565 100644
--- a/shared/src/i18n/de/journey.ts
+++ b/shared/src/i18n/de/journey.ts
@@ -180,6 +180,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': '{username} aus dieser Journey entfernen?',
'journey.contributors.removed': 'Mitwirkender entfernt',
'journey.contributors.removeFailed': 'Entfernen fehlgeschlagen',
+ 'journey.contributors.viaTrips': 'Über Reisen',
+ 'journey.contributors.viaTripsHint':
+ 'Sie können dieses Journal öffnen, weil eine Reise es verknüpft. Ändern lässt sich das in den Einstellungen der Reise.',
+ 'journey.contributors.viaTrip': 'über {trip}',
'journey.share.publicShare': 'Öffentlicher Link',
'journey.share.createLink': 'Link erstellen',
'journey.share.linkCreated': 'Link erstellt',
diff --git a/shared/src/i18n/en/journey.ts b/shared/src/i18n/en/journey.ts
index 7584c22bf..5371fefbc 100644
--- a/shared/src/i18n/en/journey.ts
+++ b/shared/src/i18n/en/journey.ts
@@ -188,6 +188,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?',
'journey.contributors.removed': 'Contributor removed',
'journey.contributors.removeFailed': 'Failed to remove contributor',
+ 'journey.contributors.viaTrips': 'Through trips',
+ 'journey.contributors.viaTripsHint':
+ "They can open this journal because a trip links it. Change that in the trip's settings.",
+ 'journey.contributors.viaTrip': 'via {trip}',
'journey.share.publicShare': 'Public Share',
'journey.share.createLink': 'Create share link',
'journey.share.linkCreated': 'Share link created',
diff --git a/shared/src/i18n/es/journey.ts b/shared/src/i18n/es/journey.ts
index 275244765..d3c0370be 100644
--- a/shared/src/i18n/es/journey.ts
+++ b/shared/src/i18n/es/journey.ts
@@ -265,6 +265,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': 'A través de viajes',
+ 'journey.contributors.viaTripsHint':
+ 'Pueden abrir este diario porque un viaje lo vincula. Cámbialo en los ajustes del viaje.',
+ 'journey.contributors.viaTrip': 'a través de {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/et/journey.ts b/shared/src/i18n/et/journey.ts
index ea30fb624..6e27ec081 100644
--- a/shared/src/i18n/et/journey.ts
+++ b/shared/src/i18n/et/journey.ts
@@ -190,6 +190,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Kas eemaldada {username} sellest reisiloost?',
'journey.contributors.removed': 'Kaasautor eemaldatud',
'journey.contributors.removeFailed': 'Kaasautori eemaldamine ebaõnnestus',
+ 'journey.contributors.viaTrips': 'Reiside kaudu',
+ 'journey.contributors.viaTripsHint':
+ 'Nad saavad selle päeviku avada, sest reis seob selle. Muuda seda reisi seadetes.',
+ 'journey.contributors.viaTrip': '{trip} kaudu',
'journey.share.publicShare': 'Avalik jagamine',
'journey.share.createLink': 'Loo jagamislink',
'journey.share.linkCreated': 'Jagamislink loodud',
diff --git a/shared/src/i18n/fr/journey.ts b/shared/src/i18n/fr/journey.ts
index d90c27b14..405f1a2bd 100644
--- a/shared/src/i18n/fr/journey.ts
+++ b/shared/src/i18n/fr/journey.ts
@@ -265,6 +265,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': 'Via des voyages',
+ 'journey.contributors.viaTripsHint':
+ "Ces personnes peuvent ouvrir ce journal parce qu'un voyage le lie. Modifiez-le dans les paramètres du voyage.",
+ 'journey.contributors.viaTrip': 'via {trip}', // same-as-en
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/gr/journey.ts b/shared/src/i18n/gr/journey.ts
index 408625d35..931dff69b 100644
--- a/shared/src/i18n/gr/journey.ts
+++ b/shared/src/i18n/gr/journey.ts
@@ -183,6 +183,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Αφαίρεση {username} από αυτό το ταξίδι;',
'journey.contributors.removed': 'Ο συνεισφέρων αφαιρέθηκε',
'journey.contributors.removeFailed': 'Αποτυχία αφαίρεσης συνεισφέροντα',
+ 'journey.contributors.viaTrips': 'Μέσω ταξιδιών',
+ 'journey.contributors.viaTripsHint':
+ 'Μπορούν να ανοίξουν αυτό το ημερολόγιο επειδή ένα ταξίδι το συνδέει. Αλλάξτε το στις ρυθμίσεις του ταξιδιού.',
+ 'journey.contributors.viaTrip': 'μέσω {trip}',
'journey.share.publicShare': 'Δημόσια Κοινοποίηση',
'journey.share.createLink': 'Δημιουργία συνδέσμου κοινής χρήσης',
'journey.share.linkCreated': 'Ο σύνδεσμος κοινής χρήσης δημιουργήθηκε',
diff --git a/shared/src/i18n/hu/journey.ts b/shared/src/i18n/hu/journey.ts
index 7d7612223..a75296657 100644
--- a/shared/src/i18n/hu/journey.ts
+++ b/shared/src/i18n/hu/journey.ts
@@ -264,6 +264,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': 'Utazásokon át',
+ 'journey.contributors.viaTripsHint':
+ 'Azért nyithatják meg ezt a naplót, mert egy utazás kapcsolja. Ezt az utazás beállításaiban módosíthatod.',
+ 'journey.contributors.viaTrip': '{trip} útján',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/id/journey.ts b/shared/src/i18n/id/journey.ts
index 45b6bdce4..eca375ea2 100644
--- a/shared/src/i18n/id/journey.ts
+++ b/shared/src/i18n/id/journey.ts
@@ -257,6 +257,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': 'Melalui perjalanan',
+ 'journey.contributors.viaTripsHint':
+ 'Mereka bisa membuka jurnal ini karena sebuah perjalanan menautkannya. Ubah di pengaturan perjalanan.',
+ 'journey.contributors.viaTrip': 'melalui {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/it/journey.ts b/shared/src/i18n/it/journey.ts
index ca28cf3d6..ccbbef7a8 100644
--- a/shared/src/i18n/it/journey.ts
+++ b/shared/src/i18n/it/journey.ts
@@ -264,6 +264,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': 'Tramite viaggi',
+ 'journey.contributors.viaTripsHint':
+ 'Possono aprire questo diario perché un viaggio lo collega. Modificalo nelle impostazioni del viaggio.',
+ 'journey.contributors.viaTrip': 'tramite {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/ja/journey.ts b/shared/src/i18n/ja/journey.ts
index 03fb57a7d..3c8403be4 100644
--- a/shared/src/i18n/ja/journey.ts
+++ b/shared/src/i18n/ja/journey.ts
@@ -173,6 +173,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': '{username}をこの日記から削除しますか?',
'journey.contributors.removed': '参加者を削除しました',
'journey.contributors.removeFailed': '削除に失敗しました',
+ 'journey.contributors.viaTrips': '旅行経由',
+ 'journey.contributors.viaTripsHint': '旅行がこのジャーナルをリンクしているため開けます。変更は旅行の設定で行います。',
+ 'journey.contributors.viaTrip': '{trip} 経由',
'journey.share.publicShare': '公開共有',
'journey.share.createLink': '共有リンクを作成',
'journey.share.linkCreated': '共有リンクを作成しました',
diff --git a/shared/src/i18n/ko/journey.ts b/shared/src/i18n/ko/journey.ts
index 92b5e0842..1f60a59fd 100644
--- a/shared/src/i18n/ko/journey.ts
+++ b/shared/src/i18n/ko/journey.ts
@@ -172,6 +172,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': '{username}을(를) 이 Journey에서 제거할까요?',
'journey.contributors.removed': '기여자가 제거되었습니다',
'journey.contributors.removeFailed': '기여자 제거 실패',
+ 'journey.contributors.viaTrips': '여행을 통해',
+ 'journey.contributors.viaTripsHint': '여행이 이 저널을 연결하고 있어 열 수 있습니다. 여행 설정에서 변경하세요.',
+ 'journey.contributors.viaTrip': '{trip} 통해',
'journey.share.publicShare': '공개 공유',
'journey.share.createLink': '공유 링크 만들기',
'journey.share.linkCreated': '공유 링크가 생성되었습니다',
diff --git a/shared/src/i18n/nl/journey.ts b/shared/src/i18n/nl/journey.ts
index cd3075032..8e2b8da48 100644
--- a/shared/src/i18n/nl/journey.ts
+++ b/shared/src/i18n/nl/journey.ts
@@ -266,6 +266,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': 'Via reizen',
+ 'journey.contributors.viaTripsHint':
+ 'Zij kunnen dit dagboek openen omdat een reis het koppelt. Wijzig dat in de instellingen van de reis.',
+ 'journey.contributors.viaTrip': 'via {trip}', // same-as-en
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/pl/journey.ts b/shared/src/i18n/pl/journey.ts
index b276a33a5..f81602e52 100644
--- a/shared/src/i18n/pl/journey.ts
+++ b/shared/src/i18n/pl/journey.ts
@@ -278,6 +278,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': 'Przez podróże',
+ 'journey.contributors.viaTripsHint':
+ 'Mogą otworzyć ten dziennik, bo podróż go łączy. Zmień to w ustawieniach podróży.',
+ 'journey.contributors.viaTrip': 'przez {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/ru/journey.ts b/shared/src/i18n/ru/journey.ts
index 8ad56ec55..9277d3d3c 100644
--- a/shared/src/i18n/ru/journey.ts
+++ b/shared/src/i18n/ru/journey.ts
@@ -278,6 +278,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': 'Через поездки',
+ 'journey.contributors.viaTripsHint':
+ 'Они могут открыть этот дневник, потому что его привязала поездка. Измените это в настройках поездки.',
+ 'journey.contributors.viaTrip': 'через {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/sk/journey.ts b/shared/src/i18n/sk/journey.ts
index 18a9ea216..c808bb3da 100644
--- a/shared/src/i18n/sk/journey.ts
+++ b/shared/src/i18n/sk/journey.ts
@@ -193,6 +193,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Odstrániť {username} z tohto cestovného denníka?',
'journey.contributors.removed': 'Prispievateľ odstránený',
'journey.contributors.removeFailed': 'Odstránenie prispievateľa zlyhalo',
+ 'journey.contributors.viaTrips': 'Cez cesty',
+ 'journey.contributors.viaTripsHint':
+ 'Môžu tento denník otvoriť, pretože ho cesta prepája. Zmeňte to v nastaveniach cesty.',
+ 'journey.contributors.viaTrip': 'cez {trip}',
'journey.share.publicShare': 'Verejné zdieľanie',
'journey.share.createLink': 'Vytvoriť odkaz na zdieľanie',
'journey.share.linkCreated': 'Odkaz na zdieľanie vytvorený',
diff --git a/shared/src/i18n/sv/journey.ts b/shared/src/i18n/sv/journey.ts
index 97b0cb569..296e35494 100644
--- a/shared/src/i18n/sv/journey.ts
+++ b/shared/src/i18n/sv/journey.ts
@@ -181,6 +181,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Ta bort {username} från denna journey?',
'journey.contributors.removed': 'Medverkare borttagen',
'journey.contributors.removeFailed': 'Kunde inte ta bort medverkare',
+ 'journey.contributors.viaTrips': 'Via resor',
+ 'journey.contributors.viaTripsHint':
+ 'De kan öppna dagboken eftersom en resa kopplar den. Ändra det i resans inställningar.',
+ 'journey.contributors.viaTrip': 'via {trip}', // same-as-en
'journey.share.publicShare': 'Offentlig delning',
'journey.share.createLink': 'Skapa en delningslänk',
'journey.share.linkCreated': 'Länk för delning har skapats',
diff --git a/shared/src/i18n/th/journey.ts b/shared/src/i18n/th/journey.ts
index 3ca5a0fb0..f3ed803d9 100644
--- a/shared/src/i18n/th/journey.ts
+++ b/shared/src/i18n/th/journey.ts
@@ -183,6 +183,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'ลบ {username} ออกจากการเดินทางนี้หรือไม่',
'journey.contributors.removed': 'ผู้ร่วมให้ข้อมูลถูกลบออก',
'journey.contributors.removeFailed': 'ไม่สามารถลบผู้ร่วมให้ข้อมูลได้',
+ 'journey.contributors.viaTrips': 'ผ่านทริป',
+ 'journey.contributors.viaTripsHint': 'พวกเขาเปิดบันทึกนี้ได้เพราะทริปหนึ่งเชื่อมไว้ เปลี่ยนได้ในการตั้งค่าของทริป',
+ 'journey.contributors.viaTrip': 'ผ่าน {trip}',
'journey.share.publicShare': 'แบ่งปันสาธารณะ',
'journey.share.createLink': 'สร้างลิงก์แชร์',
'journey.share.linkCreated': 'สร้างลิงก์แชร์แล้ว',
diff --git a/shared/src/i18n/tr/journey.ts b/shared/src/i18n/tr/journey.ts
index dbaf617c0..c01c6a32c 100644
--- a/shared/src/i18n/tr/journey.ts
+++ b/shared/src/i18n/tr/journey.ts
@@ -178,6 +178,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': "{username} bu journey'den kaldırılsın mı?",
'journey.contributors.removed': 'Katkıda bulunan kaldırıldı',
'journey.contributors.removeFailed': 'Katkıda bulunan kaldırılamadı',
+ 'journey.contributors.viaTrips': 'Seyahatler üzerinden',
+ 'journey.contributors.viaTripsHint':
+ 'Bir seyahat bu günlüğü bağladığı için açabiliyorlar. Bunu seyahatin ayarlarından değiştir.',
+ 'journey.contributors.viaTrip': '{trip} üzerinden',
'journey.share.publicShare': 'Herkese açık paylaşım',
'journey.share.createLink': 'Paylaşım bağlantısı oluştur',
'journey.share.linkCreated': 'Paylaşım bağlantısı oluşturuldu',
diff --git a/shared/src/i18n/uk/journey.ts b/shared/src/i18n/uk/journey.ts
index 238a511e7..29f5e0ee2 100644
--- a/shared/src/i18n/uk/journey.ts
+++ b/shared/src/i18n/uk/journey.ts
@@ -187,6 +187,10 @@ const journey: TranslationStrings = {
'journey.contributors.remove': 'Вилучити учасника',
'journey.contributors.removeConfirm': 'Вилучити {username} з цієї подорожі?',
'journey.contributors.removeFailed': 'Не вдалося вилучити учасника',
+ 'journey.contributors.viaTrips': 'Через подорожі',
+ 'journey.contributors.viaTripsHint':
+ 'Вони можуть відкрити цей щоденник, бо його привʼязала подорож. Змініть це в налаштуваннях подорожі.',
+ 'journey.contributors.viaTrip': 'через {trip}',
'journey.contributors.removed': 'Учасника вилучено',
'journey.share.publicShare': 'Публічний доступ',
'journey.share.createLink': 'Створити посилання для спільного доступу',
diff --git a/shared/src/i18n/vi/journey.ts b/shared/src/i18n/vi/journey.ts
index ea2b820f1..47ca318ef 100644
--- a/shared/src/i18n/vi/journey.ts
+++ b/shared/src/i18n/vi/journey.ts
@@ -176,6 +176,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Xóa {username} khỏi hành trình này?',
'journey.contributors.removed': 'Đã xóa người đóng góp',
'journey.contributors.removeFailed': 'Không thể xóa người đóng góp',
+ 'journey.contributors.viaTrips': 'Qua chuyến đi',
+ 'journey.contributors.viaTripsHint':
+ 'Họ có thể mở nhật ký này vì một chuyến đi liên kết nó. Thay đổi trong cài đặt của chuyến đi.',
+ 'journey.contributors.viaTrip': 'qua {trip}',
'journey.share.publicShare': 'Chia sẻ công khai',
'journey.share.createLink': 'Tạo liên kết chia sẻ',
'journey.share.linkCreated': 'Đã tạo liên kết chia sẻ',
diff --git a/shared/src/i18n/zh-TW/journey.ts b/shared/src/i18n/zh-TW/journey.ts
index a0882b74f..c6c8d6a77 100644
--- a/shared/src/i18n/zh-TW/journey.ts
+++ b/shared/src/i18n/zh-TW/journey.ts
@@ -253,6 +253,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
+ 'journey.contributors.viaTrips': '透過旅行',
+ 'journey.contributors.viaTripsHint': '因為某個旅行連結了此日誌,他們可以開啟它。請在該旅行的設定中變更。',
+ 'journey.contributors.viaTrip': '透過 {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
diff --git a/shared/src/i18n/zh/journey.ts b/shared/src/i18n/zh/journey.ts
index 5893a3443..1cdfbaf44 100644
--- a/shared/src/i18n/zh/journey.ts
+++ b/shared/src/i18n/zh/journey.ts
@@ -253,6 +253,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': '要将 {username} 从此旅程中移除吗?',
'journey.contributors.removed': '已移除贡献者',
'journey.contributors.removeFailed': '移除贡献者失败',
+ 'journey.contributors.viaTrips': '通过旅行',
+ 'journey.contributors.viaTripsHint': '因为某个旅行关联了此日志,他们可以打开它。请在该旅行的设置中更改。',
+ 'journey.contributors.viaTrip': '通过 {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback