Per-user intro versioning + admin pending-delivery view
Replaces "clear intro after first send", which dropped the message for any subscriber still queued on a slower cadence. - Each non-empty admin save bumps a per-locale version counter on the tree. The dispatcher includes the intro only for recipients whose last-seen version is behind, then advances their watermark after a successful send. Webtrees users get a per-user watermark; external addresses share one tree-level watermark. Re-saving the same text is a no-op. - The preferences page now shows delivery progress per locale: "Delivered to X of Y subscriber(s)" plus a collapsible Pending list with name + email of each subscriber who hasn't received the current intro yet, and a single "External recipients (N)" row when the external watermark is behind. - README rewritten to reflect every feature shipped since the initial commit (BockenTheme skin, embedded avatars, relationship labels, kin-distance filter, per-user cadence, bilingual subject prefix, locale-aware subject date, SiteUser as From, three subscriber sources, Markdown intro with personalisation tokens).
This commit is contained in:
@@ -185,9 +185,14 @@ final class NewsletterDispatchService
|
||||
|
||||
// One-shot intro paragraph (admin-supplied). Empty
|
||||
// string is "no intro" — the view simply omits the
|
||||
// block. Cleared from preferences after the run if
|
||||
// at least one recipient was successfully reached.
|
||||
$intro = Configuration::introForLocale($module, $tree, $lang);
|
||||
// block. We do NOT clear it after sending; instead
|
||||
// we version-stamp it and track each recipient's
|
||||
// last-seen version, so subscribers on slower
|
||||
// cadences still get the message exactly once.
|
||||
$intro = Configuration::introForLocale($module, $tree, $lang);
|
||||
$intro_version = Configuration::introVersion($module, $tree, $lang);
|
||||
$external_seen = Configuration::externalIntroVersion($module, $tree, $lang);
|
||||
$external_served_this_run = false;
|
||||
|
||||
// Render the email body per recipient — the relationship
|
||||
// labels are personalised relative to whichever individual
|
||||
@@ -221,6 +226,21 @@ final class NewsletterDispatchService
|
||||
}
|
||||
}
|
||||
|
||||
// Decide whether to attach the intro for *this*
|
||||
// recipient: only if it's non-empty AND this
|
||||
// recipient hasn't yet received the current
|
||||
// version. Webtrees users have their own
|
||||
// watermark; external addresses share a single
|
||||
// tree-level one.
|
||||
$recipient_seen = $recipient instanceof User
|
||||
? Configuration::userIntroVersion($tree, $recipient, $lang)
|
||||
: $external_seen;
|
||||
$show_intro = $intro !== '' && $intro_version > $recipient_seen;
|
||||
|
||||
$personalised_intro = $show_intro
|
||||
? $this->renderIntroTemplate($intro, $recipient)
|
||||
: '';
|
||||
|
||||
$html = view($module->name() . '::email', [
|
||||
'tree' => $tree,
|
||||
'birthdays' => $birthdays,
|
||||
@@ -233,8 +253,8 @@ final class NewsletterDispatchService
|
||||
'relationships' => $relationships,
|
||||
'detailed_xrefs' => $detailed_set,
|
||||
'account_url' => $account_url,
|
||||
'intro' => $this->renderIntroTemplate($intro, $recipient),
|
||||
'intro_author' => $intro_author,
|
||||
'intro' => $personalised_intro,
|
||||
'intro_author' => $show_intro ? $intro_author : null,
|
||||
]);
|
||||
|
||||
$text = $this->htmlToText($html);
|
||||
@@ -247,6 +267,20 @@ final class NewsletterDispatchService
|
||||
// added addresses always fire on every run.
|
||||
if ($recipient instanceof User) {
|
||||
Configuration::setUserLastSentAt($tree, $recipient, $now);
|
||||
|
||||
// Mark this user as up-to-date on the
|
||||
// intro so we don't re-deliver it next
|
||||
// time their cadence comes round.
|
||||
if ($show_intro) {
|
||||
Configuration::setUserIntroVersion($tree, $recipient, $lang, $intro_version);
|
||||
}
|
||||
} elseif ($show_intro) {
|
||||
// External recipient — bump the single
|
||||
// tree-level "externals served"
|
||||
// watermark after the loop so all
|
||||
// externals in this run see the same
|
||||
// intro before we move it forward.
|
||||
$external_served_this_run = true;
|
||||
}
|
||||
} else {
|
||||
$failures++;
|
||||
@@ -256,6 +290,10 @@ final class NewsletterDispatchService
|
||||
Log::addErrorLog('Newsletter send to ' . $recipient->email() . ' failed: ' . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($external_served_this_run) {
|
||||
Configuration::setExternalIntroVersion($module, $tree, $lang, $intro_version);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Always restore the original locale, even if a render or
|
||||
@@ -269,14 +307,6 @@ final class NewsletterDispatchService
|
||||
Configuration::setLastSentAt($tree, $now);
|
||||
}
|
||||
|
||||
// One-shot intro: clear only once at least one recipient was
|
||||
// actually reached. A run that produced nothing but failures
|
||||
// (transport down, all addresses bouncing) preserves the
|
||||
// admin's intro for the next attempt.
|
||||
if ($sent > 0) {
|
||||
Configuration::clearIntros($module, $tree);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'Tree "%s": sent to %d recipient(s), %d failure(s).',
|
||||
$tree->name(),
|
||||
|
||||
Reference in New Issue
Block a user