Uczestnik przy dostawcy z wlasnymi pokojami (LiveKit, Jitsi, Daily, Zoom)
nie mial ZADNEJ drogi na sale:
- mail potwierdzajacy prowadzil na /w/{slug}, czyli formularz zapisu,
- strona zgloszenia renderowala przycisk "Dolacz do spotkania" wylacznie
gdy `webinars.join_url` nie bylo puste, a przy LiveKit pokoj jest
wewnetrzny i to pole zostaje NULL,
- mail obiecywal przy tym wprost "link do spotkania znajdziesz na stronie
swojego zgloszenia".
Adres sklada teraz `VideoProviderFactory.meetingUrlFor`, bo tylko backend
zna token zapisu. Tryb NONE oddaje `join_url` wklejony przez organizatora,
kazdy inny dostawca wlasna trase /spotkanie/{token}. Ta sama wartosc zasila
widok zgloszenia i szablony maili, wiec nie ma dwoch miejsc do poprawiania.
Szablon potwierdzenia dostaje przycisk wejscia jako pierwszy, nad odnosnikiem
do strony webinaru, oraz zdanie o tym, ze sala otwiera sie przed startem:
bez niego uczestnik klikajacy tydzien wczesniej widzi odliczanie i nie wie,
czy tak ma byc. Szablon przypomnienia mial obsluge `joinUrl` od poczatku,
wiec naprawia sie sam.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,17 +52,20 @@ public class MailService {
|
||||
private final IcsGenerator icsGenerator;
|
||||
private final ReminderTextBuilder reminderTextBuilder;
|
||||
private final SafeDelivery safeDelivery;
|
||||
private final com.softspm.webinar.video.VideoProviderFactory videoProviderFactory;
|
||||
|
||||
public MailService(SettingsService settings,
|
||||
SpringTemplateEngine mailTemplateEngine,
|
||||
IcsGenerator icsGenerator,
|
||||
ReminderTextBuilder reminderTextBuilder,
|
||||
SafeDelivery safeDelivery) {
|
||||
SafeDelivery safeDelivery,
|
||||
com.softspm.webinar.video.VideoProviderFactory videoProviderFactory) {
|
||||
this.settings = settings;
|
||||
this.mailTemplateEngine = mailTemplateEngine;
|
||||
this.icsGenerator = icsGenerator;
|
||||
this.reminderTextBuilder = reminderTextBuilder;
|
||||
this.safeDelivery = safeDelivery;
|
||||
this.videoProviderFactory = videoProviderFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,11 +237,22 @@ public class MailService {
|
||||
context.setVariable("cancelUrl", publicUrl + "/zapis/" + registration.getConfirmationToken());
|
||||
context.setVariable("brandName", settings.get(SettingKey.APP_BRAND_NAME));
|
||||
|
||||
// Uzywane przez szablon przypomnienia. Pusty joinUrl przekazujemy jako null,
|
||||
// zeby warunek th:if w szablonie zadzialal i pokazal informacje zastepcza.
|
||||
/*
|
||||
* Link do spotkania. Uzywany przez szablon potwierdzenia i przypomnienia.
|
||||
*
|
||||
* Adres sklada `VideoProviderFactory.meetingUrlFor`, bo zalezy od
|
||||
* dostawcy: tryb NONE oddaje `join_url` wklejony przez organizatora,
|
||||
* a kazdy inny wlasna trase `/spotkanie/{token}`. Samo czytanie
|
||||
* `webinar.getJoinUrl()` dawalo przy LiveKit null, wiec uczestnik
|
||||
* dostawal wylacznie odnosnik do strony zapisu, gdzie przycisk wejscia
|
||||
* takze sie nie pokazywal: nie mial ZADNEJ drogi na sale.
|
||||
*
|
||||
* Pusty adres przekazujemy jako null, zeby warunek th:if w szablonie
|
||||
* zadzialal i pokazal informacje zastepcza.
|
||||
*/
|
||||
context.setVariable("countdown", reminderTextBuilder.countdown(webinar, OffsetDateTime.now()));
|
||||
context.setVariable("joinUrl",
|
||||
webinar.getJoinUrl() == null || webinar.getJoinUrl().isBlank() ? null : webinar.getJoinUrl());
|
||||
videoProviderFactory.meetingUrlFor(webinar, registration.getConfirmationToken()));
|
||||
|
||||
String html = mailTemplateEngine.process("mail/" + template + ".html", context);
|
||||
String text = mailTemplateEngine.process("mail/" + template + ".txt", context);
|
||||
|
||||
@@ -58,6 +58,7 @@ public class RegistrationService {
|
||||
private final com.softspm.webinar.contact.InvitationService invitationService;
|
||||
private final com.softspm.webinar.repository.InvitationRepository invitationRepository;
|
||||
private final com.softspm.webinar.repository.CertificateRepository certificateRepository;
|
||||
private final com.softspm.webinar.video.VideoProviderFactory videoProviderFactory;
|
||||
|
||||
public RegistrationService(WebinarRepository webinarRepository,
|
||||
RegistrationRepository registrationRepository,
|
||||
@@ -71,7 +72,8 @@ public class RegistrationService {
|
||||
com.softspm.webinar.contact.ContactService contactService,
|
||||
com.softspm.webinar.contact.InvitationService invitationService,
|
||||
com.softspm.webinar.repository.InvitationRepository invitationRepository,
|
||||
com.softspm.webinar.repository.CertificateRepository certificateRepository) {
|
||||
com.softspm.webinar.repository.CertificateRepository certificateRepository,
|
||||
com.softspm.webinar.video.VideoProviderFactory videoProviderFactory) {
|
||||
this.webinarRepository = webinarRepository;
|
||||
this.registrationRepository = registrationRepository;
|
||||
this.consentRepository = consentRepository;
|
||||
@@ -85,6 +87,7 @@ public class RegistrationService {
|
||||
this.invitationService = invitationService;
|
||||
this.invitationRepository = invitationRepository;
|
||||
this.certificateRepository = certificateRepository;
|
||||
this.videoProviderFactory = videoProviderFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,10 +275,19 @@ public class RegistrationService {
|
||||
Registration registration = requireByToken(token);
|
||||
Webinar webinar = registration.getWebinar();
|
||||
|
||||
// link do spotkania tylko dla zapisu aktywnego
|
||||
/*
|
||||
* Link do spotkania tylko dla zapisu aktywnego.
|
||||
*
|
||||
* Adres sklada `VideoProviderFactory.meetingUrlFor`, bo zalezy od
|
||||
* dostawcy: tryb NONE oddaje `join_url` wklejony przez organizatora,
|
||||
* a kazdy inny wlasna trase `/spotkanie/{token}`. Samo czytanie
|
||||
* `webinar.getJoinUrl()` dawalo przy LiveKit null, wiec przycisk
|
||||
* "Dolacz do spotkania" nie renderowal sie wcale i uczestnik nie mial
|
||||
* jak wejsc na sale.
|
||||
*/
|
||||
String joinUrl = registration.getStatus() == RegistrationStatus.CANCELLED
|
||||
? null
|
||||
: webinar.getJoinUrl();
|
||||
: videoProviderFactory.meetingUrlFor(webinar, registration.getConfirmationToken());
|
||||
|
||||
/*
|
||||
* Nagrania i materialy pokazujemy dopiero po zakonczeniu webinaru
|
||||
|
||||
@@ -120,6 +120,47 @@ public class VideoProviderFactory {
|
||||
return existing != null && !existing.name().equals(forWebinar(webinar).name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adres, pod ktorym uczestnik wchodzi na spotkanie.
|
||||
*
|
||||
* DWA PRZYPADKI, DWA ROZNE ADRESY
|
||||
* ------------------------------------------------------------------
|
||||
* Tryb NONE nie ma wlasnej sali: obraz jest w narzedziu organizatora,
|
||||
* a uczestnik idzie prosto pod `join_url` wklejony w panelu. Kazdy inny
|
||||
* dostawca ma sale U NAS, pod trasa `/spotkanie/{token}`, i wlasnie ten
|
||||
* adres ma dostac uczestnik.
|
||||
*
|
||||
* Wczesniej mail i strona zgloszenia pokazywaly wylacznie `join_url`,
|
||||
* ktory przy LiveKit jest pusty (pokoj jest wewnetrzny). Skutek:
|
||||
* **uczestnik nie mial ZADNEJ drogi na sale**. Mail obiecywal "link do
|
||||
* spotkania znajdziesz na stronie swojego zgloszenia", a tam przycisk
|
||||
* nie renderowal sie wcale, bo warunek sprawdzal puste pole.
|
||||
*
|
||||
* Zwraca null, gdy adresu nie da sie zlozyc: przy pustym `APP_PUBLIC_URL`
|
||||
* powstalby link wzgledny, w kliencie poczty prowadzacy donikad.
|
||||
*/
|
||||
public String meetingUrlFor(Webinar webinar, String registrationToken) {
|
||||
if (webinar == null || registrationToken == null || registrationToken.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dostawca bez wlasnych pokoi: obowiazuje link wklejony przez organizatora.
|
||||
if (!forWebinar(webinar).supportsRooms()) {
|
||||
String joinUrl = webinar.getJoinUrl();
|
||||
return joinUrl == null || joinUrl.isBlank() ? null : joinUrl;
|
||||
}
|
||||
|
||||
String publicUrl = settings.get(SettingKey.APP_PUBLIC_URL);
|
||||
if (publicUrl == null || publicUrl.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
while (publicUrl.endsWith("/")) {
|
||||
publicUrl = publicUrl.substring(0, publicUrl.length() - 1);
|
||||
}
|
||||
// sciezka musi odpowiadac trasie frontendu /spotkanie/:token
|
||||
return publicUrl + "/spotkanie/" + registrationToken;
|
||||
}
|
||||
|
||||
/** Nazwy wszystkich dostepnych dostawcow, do listy wyboru w panelu. */
|
||||
public List<String> availableNames() {
|
||||
return providers.keySet().stream().sorted().toList();
|
||||
|
||||
@@ -65,12 +65,40 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!--
|
||||
Link do sali. Pierwszy, bo to po niego uczestnik wraca do tej
|
||||
wiadomosci w dniu szkolenia. Przycisk w tabeli, bo Outlook
|
||||
ignoruje marginesy na znaczniku a.
|
||||
-->
|
||||
<table th:if="${joinUrl}" role="presentation" cellpadding="0" cellspacing="0" border="0"
|
||||
style="margin:0 0 12px;">
|
||||
<tr>
|
||||
<td style="background-color:#1d4ed8; border-radius:8px;">
|
||||
<a th:href="${joinUrl}"
|
||||
style="display:inline-block; padding:14px 28px; font-size:16px; font-weight:600; color:#ffffff; text-decoration:none;">
|
||||
Dołącz do spotkania
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!--
|
||||
Sala otwiera sie dopiero przed startem. Bez tego zdania uczestnik
|
||||
klikajacy tydzien wczesniej widzi odliczanie i nie wie, czy to
|
||||
tak ma byc, czy cos jest zepsute.
|
||||
-->
|
||||
<p th:if="${joinUrl}"
|
||||
style="margin:0 0 28px; font-size:14px; line-height:1.6; color:#64748b;">
|
||||
Spotkanie otworzy się na krótko przed rozpoczęciem. Jeśli otworzysz
|
||||
link wcześniej, zobaczysz odliczanie do startu.
|
||||
</p>
|
||||
|
||||
<!-- przycisk w tabeli, bo Outlook ignoruje marginesy na znaczniku a -->
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 28px;">
|
||||
<tr>
|
||||
<td style="background-color:#1d4ed8; border-radius:8px;">
|
||||
<td style="background-color:#ffffff; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
<a th:href="${webinarUrl}"
|
||||
style="display:inline-block; padding:14px 28px; font-size:16px; font-weight:600; color:#ffffff; text-decoration:none;">
|
||||
style="display:inline-block; padding:13px 27px; font-size:16px; font-weight:600; color:#1d4ed8; text-decoration:none;">
|
||||
Otwórz stronę webinaru
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -10,7 +10,13 @@ potwierdzamy Twój zapis na webinar: [(${webinarTitle})]
|
||||
Termin: [(${startsAt})]
|
||||
Strefa czasowa: [(${timezone})]
|
||||
|
||||
Strona webinaru:
|
||||
[# th:if="${joinUrl}"]Link do spotkania:
|
||||
[(${joinUrl})]
|
||||
|
||||
Spotkanie otworzy się na krótko przed rozpoczęciem. Jeśli otworzysz
|
||||
link wcześniej, zobaczysz odliczanie do startu.
|
||||
|
||||
[/]Strona webinaru:
|
||||
[(${webinarUrl})]
|
||||
|
||||
Do wiadomości dołączyliśmy plik invite.ics. Otwórz go, aby dodać
|
||||
|
||||
@@ -55,7 +55,9 @@ class MailAttachmentTest {
|
||||
// SafeDelivery z tym samym mockiem ustawien: test sklada wiadomosc,
|
||||
// a nie wysyla jej, wiec straznik nie zmienia tu niczego
|
||||
mailService = new MailService(settings, engine, new IcsGenerator(settings),
|
||||
new ReminderTextBuilder(settings), new SafeDelivery(settings));
|
||||
new ReminderTextBuilder(settings), new SafeDelivery(settings),
|
||||
new com.softspm.webinar.video.VideoProviderFactory(
|
||||
java.util.List.of(new com.softspm.webinar.video.NoneVideoProvider()), settings));
|
||||
}
|
||||
|
||||
private ClassLoaderTemplateResolver resolver(TemplateMode mode, String pattern) {
|
||||
|
||||
@@ -62,7 +62,9 @@ class MailPreviewTest {
|
||||
// SafeDelivery z tym samym mockiem ustawien: test sklada wiadomosc,
|
||||
// a nie wysyla jej, wiec straznik nie zmienia tu niczego
|
||||
mailService = new MailService(settings, engine, new IcsGenerator(settings),
|
||||
new ReminderTextBuilder(settings), new SafeDelivery(settings));
|
||||
new ReminderTextBuilder(settings), new SafeDelivery(settings),
|
||||
new com.softspm.webinar.video.VideoProviderFactory(
|
||||
java.util.List.of(new com.softspm.webinar.video.NoneVideoProvider()), settings));
|
||||
}
|
||||
|
||||
private ClassLoaderTemplateResolver resolver(TemplateMode mode, String pattern) {
|
||||
|
||||
Reference in New Issue
Block a user