[
'icon' => 'heart',
'priority' => 'low',
'requires_action' => false,
'audience' => 'content_owner',
'email_digest' => true
],
'artist_approved' => [
'icon' => 'check',
'priority' => 'high',
'requires_action' => false,
'audience' => 'single',
'email_digest' => true
],
'artist_joined' => [
'icon' => 'artist',
'priority' => 'low',
'requires_action' => false,
'email_digest' => false,
],
'artist_rejected' => [
'icon' => 'x',
'priority' => 'high',
'requires_action' => false,
'audience' => 'single',
'email_digest' => true
],
'artist_invitation' => [
'icon' => 'invite',
'priority' => 'high',
'requires_action' => true,
'audience' => 'single',
'email_digest' => true
],
'artist_request' => [
'icon' => 'artist',
'priority' => 'high',
'requires_action' => true,
'audience' => 'single',
'email_digest' => true,
],
'shop_accepted' => [
'icon' => 'shop',
'priority' => 'high',
'requires_action' => false,
'audience' => 'single',
'email_digest' => true,
],
'shop_rejected' => [
'icon' => 'shop',
'priority' => 'high',
'requires_action' => false,
'audience' => 'single',
'email_digest' => true,
],
'new_term' => [
'icon' => 'style',
'priority' => 'medium',
'requires_action' => true,
'audience' => 'artists',
'email_digest' => false
],
'term_approved' => [
'icon' => 'check',
'priority' => 'high',
'requires_action' => false,
'audience' => 'single',
'email_digest' => true
],
'term_rejected' => [
'icon' => 'x',
'priority' => 'medium',
'requires_action' => false,
'audience' => 'single',
'email_digest' => true
],
'list_shared' => [
'icon' => 'share',
'priority' => 'medium',
'requires_action' => false,
'audience' => 'single',
'email_digest' => true
],
'list_share_accepted' => [
'icon' => 'check',
'priority' => 'low',
'requires_action' => false,
'audience' => 'single',
'email_digest' => true
],
'list_share_revoked' => [
'icon' => 'close',
'priority' => 'medium',
'requires_action' => false,
'audience' => 'single',
'email_digest' => true
],
'system_message' => [
'icon' => 'info',
'priority' => 'high',
'requires_action' => false,
'audience' => 'all',
'email_digest' => false
]
];
/**
* Constructor
*/
public function __construct()
{
$this->userCache = Cache::for('userNotifications', WEEK_IN_SECONDS);
$this->contentCache = Cache::for('contentNotifications', WEEK_IN_SECONDS)->connect('post', true)->connect('taxonomy', true);
$this->artistsCache = Cache::for('artist', WEEK_IN_SECONDS)->connect('post');
$this->favouritesCache = Cache::for('favouritedUsers', WEEK_IN_SECONDS)->connect('favourites');
$this->followerCache = Cache::for('totalFollowers', WEEK_IN_SECONDS)->connect('favourites');
// Add filter for bulk operation handling
add_filter(BASE . 'handle_bulk_operation', [ $this, 'processOperation' ], 10, 3);
add_action(BASE . 'cleanup_notifications', [$this, 'cleanupOldNotifications']);
// Track content creation for notifications
add_action('save_post', [ $this, 'trackContentCreation' ], 10, 3);
add_action('saved_term', [ $this, 'track_shop_creation' ], 10, 3);
// Generate content summaries when users log in
add_action('wp_login', [ $this, 'generateUserContentNotifications' ], 10, 2);
// Register digest cron jobs
$this->registerCron();
}
/**
* Registers the digest cron jobs
* @return void
*/
protected function registerCron():void
{
add_action(BASE . 'notification_digest_daily', [ $this, 'runDailyDigests' ]);
add_action(BASE . 'notification_digest_weekly', [ $this, 'runWeeklyDigests' ]);
add_action(BASE . 'notification_digest_monthly', [ $this, 'runMonthlyDigests' ]);
}
/************************************************************
* Basic Notification Methods
************************************************************/
/**
* @return array
*/
public function getNotificationTypes():array
{
return $this->notification_types;
}
/**
* Add a new notification
*
* @param int|array $user_ids Recipient user ID(s)
* @param string $type Notification type
* @param int|null $action_user_id User who performed the action (optional)
* @param string $message Notification message (optional)
* @param int|null $target_id Related target ID (optional)
* @param string|null $target_type Related target type (optional)
* @param array|null $context Additional contextual data (optional)
*
* @return bool|WP_Error Success or error
*/
public function addNotification(
mixed $user_ids,
string $type,
int|null $action_user_id = null,
string $message = '',
int|null $target_id = null,
string|null $target_type = null,
array|null $context = null
):bool|WP_Error {
// Validate notification type
if (!isset($this->notification_types[$type])) {
$this->logError("Invalid notification type: $type", [
'user_ids' => is_array($user_ids) ? implode(',', $user_ids) : $user_ids,
'message' => $message
], 'warning');
return new WP_Error('invalid_type', 'Invalid notification type');
}
// Handle single user or array of users
$user_ids = is_array($user_ids) ? $user_ids : [$user_ids];
if (empty($user_ids)) {
return false;
}
// Queue the bulk notification operation
$queue = JVB()->queue();
$queue->queueOperation(
'addNotification',
$action_user_id,
[
'user_ids' => $user_ids,
'type' => $type,
'action_user_id' => $action_user_id,
'message' => $message,
'target_id' => $target_id,
'target_type' => $target_type,
'context' => $context
],
[
'count' => count($user_ids),
'priority' => 'normal',
'chunk_key' => 'user_ids',
'chunk_size'=> 50,
'operation_id' => 'notifications' . uniqid()
]
);
return true;
}
/**
* Process notification operation
*
* @param int|array $user_ids Recipient user ID(s)
* @param string $type Notification type
* @param int|null $action_user_id User who performed the action (optional)
* @param string $message Notification message (optional)
* @param int|null $target_id Related target ID (optional)
* @param string|null $target_type Related target type (optional)
* @param array|null $context Additional contextual data (optional)
*
* @return bool|WP_Error Success or error
*/
public function processNotification(
int|array $user_ids,
string $type,
int|null $action_user_id = null,
string $message = '',
int|null $target_id = null,
string|null $target_type = null,
array|null $context = null
):bool|WP_Error {
$config = $this->notification_types[$type];
$notifications = [];
$errors = [];
$user_ids = is_array($user_ids) ? $user_ids : [$user_ids];
global $wpdb;
foreach ($user_ids as $user_id) {
// Skip invalid users
if (!$user_id || $user_id <= 0) {
$errors[] = "Invalid user ID: $user_id";
continue;
}
// Skip sending notifications to self if action_user_id == user_id
if ($action_user_id && $action_user_id == $user_id && !($config['notify_self'] ?? false)) {
continue;
}
try {
// Prepare context data
$context_json = null;
if (!empty($context)) {
$context_json = json_encode($context);
}
// Insert new notification
$result = $wpdb->insert(
$wpdb->prefix . $this->table,
[
'owner_id' => $user_id,
'action_user_id' => $action_user_id,
'type' => $type,
'status' => 'unread',
'message' => $message,
'priority' => $config['priority'] ?? 'normal',
'target_id' => $target_id,
'target_type' => $target_type,
'context' => $context_json,
'requires_action' => !empty($config['requires_action']) ? 1 : 0,
'created_at' => current_time('mysql'),
'updated_at' => current_time('mysql')
]
);
if ($result) {
$notification_id = $wpdb->insert_id;
$notifications[] = $notification_id;
// Clear cache for this user
$this->clearNotificationCache($user_id);
} else {
$errors[] = "Database error for user $user_id: " . $wpdb->last_error;
$this->logError("Failed to insert notification", [
'user_id' => $user_id,
'type' => $type,
'db_error' => $wpdb->last_error
]);
}
} catch (Exception $e) {
$errors[] = "Exception for user $user_id: " . $e->getMessage();
$this->logError("Error adding notification", [
'user_id' => $user_id,
'type' => $type,
'message' => $message,
'error' => $e->getMessage()
]);
}
}
// Return results
if (!empty($notifications) && !empty($errors)) {
$this->logError("Some notifications failed", [
'successful' => count($notifications),
'errors' => $errors
], 'warning');
}
if (empty($notifications) && !empty($errors)) {
return new WP_Error('notification_failed', 'All notifications failed', [
'errors' => $errors
]);
}
return !empty($notifications);
}
/**
* Wrapper for notifying verified artists
*
* @param string $type Notification type
* @param int|null $action_user_id User who performed the action (optional)
* @param string $message Notification message (optional)
* @param int|null $target_id Related target ID (optional)
* @param string|null $target_type Related target type (optional)
* @param array|null $context Additional contextual data (optional)
*
* @return bool|WP_Error Success or error
*/
public function notifyVerifiedArtists(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error
{
$artists = $this->getVerified('artist');
return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
}
/**
* Wrapper for notifying verified partners
*
* @param string $type Notification type
* @param int|null $action_user_id User who performed the action (optional)
* @param string $message Notification message (optional)
* @param int|null $target_id Related target ID (optional)
* @param string|null $target_type Related target type (optional)
* @param array|null $context Additional contextual data (optional)
*
* @return bool|WP_Error Success or error
*/
public function notifyVerifiedPartners(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error
{
$artists = $this->getVerified('partner');
return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
}
/**
* Wrapper for notifying verified partners
*
* @param string $type Notification type
* @param int|null $action_user_id User who performed the action (optional)
* @param string $message Notification message (optional)
* @param int|null $target_id Related target ID (optional)
* @param string|null $target_type Related target type (optional)
* @param array|null $context Additional contextual data (optional)
*
* @return bool|WP_Error Success or error
*/
public function notifyEnthusiasts(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error
{
$artists = $this->getUserIDs('enthusiast');
return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
}
/**
* Wrapper for notifying verified partners
*
* @param string $type Notification type
* @param int|null $action_user_id User who performed the action (optional)
* @param string $message Notification message (optional)
* @param int|null $target_id Related target ID (optional)
* @param string|null $target_type Related target type (optional)
* @param array|null $context Additional contextual data (optional)
*
* @return bool|WP_Error Success or error
*/
public function notifyEveryone(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error
{
$artists = $this->getUserIDs(array_keys(JVB_USER));
return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
}
/************************************************************
* Content Notification Methods
************************************************************/
/**
* Track content creation or updates for notification purposes
*
* @param int $post_id Post ID
* @param WP_Post $post Post object
* @param bool $update Whether this is an update
* @return void
*/
public function trackContentCreation(int $post_id, WP_POST $post, bool $update):void
{
// SAFETY: Skip attachments and other non-content post types
if (in_array($post->post_type, jvbIgnoredPostTypes())) {
return;
}
// Skip if not a published post
if ($post->post_status !== 'publish') {
return;
}
// Check if this is a relevant content type
$content_types = jvbBasedFeedContent();
if (!in_array($post->post_type, $content_types)) {
return;
}
// Check if the artist has any followers before tracking
$follower_count = $this->getFollowerCount($post->post_author);
if ($follower_count === 0) {
return; // No need to track if nobody follows this artist
}
// Get the clean content type for storing
$content_type = str_replace(BASE, '', $post->post_type);
// Update the artist's content notification records
$this->updateContentNotificationRecord($post->post_author, $content_type, $post_id, $update);
}
/**
* Update content notification record for an artist
*
* @param int $artist_id Artist user ID
* @param string $content_type Content type (tattoo, artwork, etc)
* @param int $content_id Content post ID
* @param bool $is_update Whether this is an update to existing content
*
* @return bool Success or failure
*/
protected function updateContentNotificationRecord(int $artist_id, string $content_type, int $content_id, bool $is_update = false):bool
{
global $wpdb;
$table = $wpdb->prefix . $this->contentTable;
// Start transaction
$wpdb->query('START TRANSACTION');
$success = false;
try {
// Get today's date
$today = date('Y-m-d');
$frequency_updates = [];
// Update records for each frequency
foreach (['daily', 'weekly', 'monthly'] as $frequency) {
// Find or create a record for this artist, date and frequency
$record = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$table}
WHERE user_id = %d AND date = %s AND frequency = %s",
$artist_id,
$today,
$frequency
));
if ($record) {
$frequency_updates[$frequency] = $this->updateExistingContentRecord(
$record,
$content_type,
$content_id,
$is_update
);
} else {
$frequency_updates[$frequency] = $this->createNewContentRecord(
$artist_id,
$frequency,
$content_type,
$content_id,
$is_update
);
}
}
// Check if all updates were successful
$success = !in_array(false, $frequency_updates, true);
if ($success) {
$wpdb->query('COMMIT');
return true;
} else {
// If any update failed, roll back
$wpdb->query('ROLLBACK');
$this->logError("Failed to update content notification records", [
'artist_id' => $artist_id,
'content_type' => $content_type,
'content_id' => $content_id,
'updates' => $frequency_updates
]);
return false;
}
} catch (Exception $e) {
$wpdb->query('ROLLBACK');
$this->logError("Exception in content notification update", [
'artist_id' => $artist_id,
'content_type' => $content_type,
'content_id' => $content_id,
'error' => $e->getMessage()
]);
return false;
}
}
/**
* Update an existing content notification record
*
* @param object $record Existing database record
* @param string $content_type Content type
* @param int $content_id Content ID
* @param bool $is_update Whether this is an update
*
* @return bool Success or failure
*/
protected function updateExistingContentRecord(object $record, string $content_type, int $content_id, bool $is_update):bool
{
global $wpdb;
$table = $wpdb->prefix . $this->contentTable;
// Decode existing JSON data
$new_items = json_decode($record->new_items, true) ?: [];
$updated_items = json_decode($record->updated_items, true) ?: [];
// Initialize arrays if not present
if (!isset($new_items[BASE.$content_type])) {
$new_items[BASE.$content_type] = [];
}
if (!isset($updated_items[BASE.$content_type])) {
$updated_items[BASE.$content_type] = [];
}
// Add ID to appropriate array
if ($is_update) {
// Add to updated array if not already there and not in new items
if (!in_array($content_id, $updated_items[BASE.$content_type]) &&
!in_array($content_id, $new_items[BASE.$content_type])) {
$updated_items[BASE.$content_type][] = $content_id;
}
} else {
// Add to new array if not already there
if (!in_array($content_id, $new_items[BASE.$content_type])) {
$new_items[BASE.$content_type][] = $content_id;
}
}
// Prepare update data
$update_data = [
'new_items' => json_encode($new_items),
'updated_items' => json_encode($updated_items),
'updated_at' => current_time('mysql')
];
// Update appropriate count column
$count_column = "{$content_type}_count";
if (property_exists($record, $count_column)) {
$update_data[ $count_column ] = $record->$count_column + ( $is_update ? 0 : 1 );
}
// Update total count
$update_data['total_items'] = $record->total_items + ( $is_update ? 0 : 1 );
// Update the record
return $wpdb->update(
$table,
$update_data,
[ 'id' => $record->id ]
) !== false;
}
/**
* Create a new content notification record
*
* @param int $artist_id Artist user ID
* @param string $frequency Frequency (daily, weekly, monthly)
* @param string $content_type Content type
* @param int $content_id Content ID
* @param bool $is_update Whether this is an update
*
* @return bool Success or failure
*/
protected function createNewContentRecord(int $artist_id, string $frequency, string $content_type, int $content_id, bool $is_update):bool
{
global $wpdb;
$table = $wpdb->prefix . $this->contentTable;
// Initialize arrays for new and updated items
$new_items = [];
$updated_items = [];
if ($is_update) {
$updated_items[BASE.$content_type] = [ $content_id ];
} else {
$new_items[BASE.$content_type] = [ $content_id ];
}
// Prepare insert data
$insert_data = [
'user_id' => $artist_id,
'date' => date('Y-m-d'),
'frequency' => $frequency,
'total_items' => 1,
'new_items' => json_encode($new_items),
'updated_items' => json_encode($updated_items),
'created_at' => current_time('mysql'),
'updated_at' => current_time('mysql')
];
// Set count column
$insert_data["{$content_type}_count"] = $is_update ? 0 : 1;
// Insert the record
return $wpdb->insert($table, $insert_data) !== false;
}
/**
* Generate content notifications for a user upon login
*
* @param string $username Username
* @param WP_User $user User object
* @return void
*/
public function generateUserContentNotifications(string $username, WP_User $user):void
{
$this->createContentSeenRecords($user->ID);
}
/**
* Create content seen records for a user's followed artists
*
* @param int $user_id User ID
*
* @return int Number of notifications created
*/
protected function createContentSeenRecords(int $user_id):int
{
global $wpdb;
try {
// Get followed artists
$followed_artists = $this->getFollowedArtists($user_id);
if (empty($followed_artists)) {
return 0;
}
// Get the last time notifications were checked
$last_check = get_user_meta($user_id, BASE . 'last_content_check', true);
$since_date = $last_check ? date('Y-m-d', strtotime($last_check)) : date('Y-m-d', strtotime('-7 days'));
// Create placeholders for SQL query
$placeholders = implode(',', array_fill(0, count($followed_artists), '%d'));
// Get content notifications since last check
$content_records = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}{$this->contentTable}
WHERE user_id IN ($placeholders)
AND date >= %s
AND total_items > 0",
array_merge($followed_artists, [ $since_date ])
)
);
if (empty($content_records)) {
return 0;
}
// Add user seen records for each content notification
$count = 0;
$user_seen_table = $wpdb->prefix . $this->seenTable;
foreach ($content_records as $record) {
// Check if record already exists
$exists = $wpdb->get_var(
$wpdb->prepare(
"SELECT id FROM {$user_seen_table}
WHERE user_id = %d AND content_notification_id = %d",
$user_id,
$record->id
)
);
if (!$exists) {
// Create new seen record
$wpdb->insert(
$user_seen_table,
[
'user_id' => $user_id,
'content_notification_id' => $record->id,
'status' => 'unread',
'created_at' => current_time('mysql')
]
);
$count ++;
}
}
// Update last check time
update_user_meta($user_id, BASE . 'last_content_check', current_time('mysql'));
// Clear cache
$this->clearNotificationCache($user_id);
return $count;
} catch (Exception $e) {
$this->logError("Error creating content seen records: " . $e->getMessage(), [
'user_id' => $user_id
]);
return 0;
}
}
/************************************************************
* Notification Digest Methods
************************************************************/
/**
* Process daily notification digests
*/
public function runDailyDigests():void
{
$this->campaign = 'daily_digest_' . date('Y-m-d');
$this->processDigest('daily');
}
/**
* Process weekly notification digests
*/
public function runWeeklyDigests():void
{
$this->campaign = 'weekly_digest_' . date('Y-m-d');
$this->processDigest('weekly');
}
/**
* Process monthly notification digests
*/
public function runMonthlyDigests():void
{
$this->campaign = 'monthly_digest_' . date('Y-m-d');
$this->processDigest('monthly');
}
/**
* Process digests for a specific frequency
*
* @param string $frequency Digest frequency (daily, weekly, monthly)
*
* @return bool Success or failure
*/
protected function processDigest(string $frequency):bool
{
// Get users with this digest frequency preference
$users = $this->getUsersForDigest($frequency);
if (empty($users)) {
return true; // No users to process
}
// Queue digest generation
$queue = JVB()->queue();
$queue->queueOperation(
'email_notification_digest',
0, // System user
[
'frequency' => $frequency,
'users' => $users
],
[
'count' => count($users),
'chunk_key' => 'users',
'chunk_size' => 20,
'priority' => 'normal',
'operation_id' => 'notification_digest_' . date('Y_m_d')
]
);
return true;
}
/**
* Get users who have subscribed to a specific digest frequency
*
* @param string $frequency Digest frequency
*
* @return array Array of user IDs
*/
protected function getUsersForDigest(string $frequency):array
{
global $wpdb;
$table = $wpdb->prefix . $this->preferencesTable;
// Get users with this frequency setting
$users = $wpdb->get_col(
$wpdb->prepare(
"SELECT DISTINCT user_id FROM $table
WHERE frequency = %s",
$frequency
)
);
return $users ?: [];
}
/**
* Generate and send a digest email for a user
*
* @param int $user_id User ID
* @param string $frequency Digest frequency
*
* @return bool Success status
*/
public function generateUserDigest(int $user_id, string $frequency):bool
{
try {
$user = get_userdata($user_id);
if (!$user || !is_email($user->user_email)) {
return false;
}
// Get date range based on frequency
$today = date('Y-m-d');
$since_date = $this->getSinceDate($frequency, $today);
// Get regular notifications
$notifications = $this->getDigestNotifications($user_id);
// Get content updates from followed artists
$content_updates = $this->getContentUpdatesForDigest($user_id, $since_date);
if (empty($notifications) && empty($content_updates)) {
return true; // Nothing to send
}
// Generate and send email
$sent = $this->sendDigestEmail($user, $frequency, $notifications, $content_updates);
if ($sent) {
// Update preferences last_sent timestamp
$this->updateDigestTimestamps($user_id, $frequency);
return true;
}
return false;
} catch (Exception $e) {
$this->logError("Error generating digest for user $user_id: " . $e->getMessage());
return false;
}
}
/**
* Get notifications for a digest
*
* @param int $user_id User ID
*
* @return array Notification objects
*/
protected function getDigestNotifications(int $user_id):array
{
global $wpdb;
// Get unread, non-emailed notifications
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}{$this->table}
WHERE user_id = %d
AND status = 'unread'
AND emailed_at IS NULL
ORDER BY priority DESC, created_at DESC",
$user_id
)
);
}
/**
* Get content updates for digest
*
* @param int $user_id User ID
* @param string $since_date Date string (YYYY-MM-DD)
*
* @return array Content update records
*/
protected function getContentUpdatesForDigest(int $user_id, string $since_date):array
{
// Get followed artists
$followed_artists = $this->getFollowedArtists($user_id);
if (empty($followed_artists)) {
return [];
}
global $wpdb;
// Create placeholders for SQL IN clause
$placeholders = implode(',', array_fill(0, count($followed_artists), '%d'));
// Get content records since the date
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}{$this->contentTable}
WHERE user_id IN ($placeholders)
AND date >= %s
AND total_items > 0
ORDER BY date DESC",
array_merge($followed_artists, [ $since_date ])
)
);
}
/**
* Get since date for a frequency
*
* @param string $frequency Digest frequency
* @param string $today Today's date
*
* @return string Date string (YYYY-MM-DD)
*/
protected function getSinceDate(string $frequency, string $today):string
{
switch ($frequency) {
case 'daily':
return date('Y-m-d', strtotime('-1 day', strtotime($today)));
case 'weekly':
return date('Y-m-d', strtotime('-1 week', strtotime($today)));
case 'monthly':
return date('Y-m-d', strtotime('-1 month', strtotime($today)));
default:
return '';
}
}
/**
* Send a digest email
*
* @param WP_User $user User object
* @param string $frequency Digest frequency
* @param array $notifications Regular notifications
* @param array $content_updates Content update records
*
* @return bool Whether the email was sent
*/
protected function sendDigestEmail(WP_User $user, string $frequency, array $notifications, array $content_updates):bool
{
// Generate subject based on frequency
$subjects = [
'daily' => [
"[edmonton.ink] Your " . date('l') . " Ink Update ♡",
"[edmonton.ink] Fresh Ink Alert - Your " . date('l') . " Digest",
"[edmonton.ink] What You Missed in Edmonton's Tattoo Scene Today"
],
'weekly' => [
"[edmonton.ink] Your Weekly Roundup from edmonton.ink ♡",
"[edmonton.ink] This Week in Edmonton's Tattoo Scene",
"[edmonton.ink] Weekly Ink Update - Fresh From the Scene"
],
'monthly' => [
"[edmonton.ink] Monthly Ink Roundup ♡",
"[edmonton.ink] Your Monthly Scene Report: " . date('F') . " Edition",
"[edmonton.ink] " . date('F') . " in Edmonton Tattoos"
]
];
// Randomly select a subject for variety
$subject_options = $subjects[ $frequency ] ?? [ "Your edmonton.ink Update" ];
$subject = $subject_options[ array_rand($subject_options) ];
// Generate email content
$content = $this->generateDigestContent($user, $frequency, $notifications, $content_updates);
// Add tracking pixel
$tracking_code = sprintf(
'',
site_url(),
base64_encode($user->ID),
$frequency
);
$content .= $tracking_code;
// Set header based on frequency
$header = match ($frequency) {
'daily' => "TODAY'S INK DROP",
'weekly' => "THIS WEEK'S SCENE REPORT",
'monthly' => "MONTHLY INK ROUNDUP",
default => "YOUR INK UPDATES",
};
// Send the email
return JVB()->email()->sendEmail($user->user_email, $subject, $content, $header);
}
/**
* Generate HTML content for digest email
*
* @param WP_User $user User object
* @param string $frequency Digest frequency
* @param array $notifications Regular notifications
* @param array $content_updates Content update records
*
* @return string HTML email content
*/
protected function generateDigestContent(WP_User $user, string $frequency, array $notifications, array $content_updates):string
{
$content = sprintf('
Hey %s,
', $user->first_name ?: $user->display_name); // Intro text based on frequency switch ($frequency) { case 'daily': $content .= 'Here\'s what happened in Edmonton\'s tattoo scene today:
'; break; case 'weekly': $content .= 'Here\'s what you missed in Edmonton\'s tattoo scene this week:
'; break; case 'monthly': $content .= sprintf('Here\'s your monthly roundup of what happened in %s in Edmonton\'s tattoo scene:
', date('F')); break; } // Process artist content updates - the most visually interesting part $content .= $this->generateContentUpdatesSection($content_updates); // Process regular notifications if (!empty($notifications)) { $content .= $this->generateNotificationsSection($notifications); } // Add footer content $content .= JVB()->email()->divider(); $settings_url = add_query_arg([ 'utm_source' => 'email', 'utm_medium' => 'digest', 'utm_campaign' => $this->campaign ], site_url('/dash/settings/')); $content .= sprintf( 'You\'re receiving this %s digest because you follow artists on edmonton.ink. You can %s at any time.
', $frequency, 'adjust your notification settings' ); return $content; } /** * Generate HTML section for content updates * * @param array $content_updates Content update records * * @return string HTML content * TODO: This needs some work */ protected function generateContentUpdatesSection(array $content_updates):string { if (empty($content_updates)) { return ''; } $content = ''; $cache = Cache::for('digest_content', HOUR_IN_SECONDS * 6); // Cache for 6 hours // Group updates by artist $updates_by_artist = []; foreach ($content_updates as $update) { if (!isset($updates_by_artist[$update->user_id])) { $updates_by_artist[ $update->user_id ] = []; } $updates_by_artist[ $update->user_id ][] = $update; } // Process each artist's updates foreach ($updates_by_artist as $artist_id => $updates) { $artist_data = $this->getArtistData($artist_id); if (!$artist_data) { continue; } // Combine all content updates from this artist $combined_new_items = []; foreach ($updates as $update) { $new_items = json_decode($update->new_items, true) ?: []; foreach ($new_items as $type => $ids) { if (!isset($combined_new_items[ $type ])) { $combined_new_items[ $type ] = []; } $combined_new_items[ $type ] = array_merge($combined_new_items[ $type ], $ids); // Remove duplicates $combined_new_items[ $type ] = array_unique($combined_new_items[ $type ]); } } // Skip if no content to show if (empty($combined_new_items)) { continue; } // Add artist header $content .= sprintf( '