Jake Vanderwerf
2026-03-03 772462eeca3002a1d52508aeba485aab2b4742ad
inc/managers/NotificationManager.php
@@ -2,6 +2,7 @@
namespace JVBase\managers;
use JVBase\JVB;
use JVBase\registrar\Registrar;
use WP_Error;
use Exception;
use WP_Post;
@@ -20,7 +21,11 @@
 */
class NotificationManager
{
    protected object $cache;
    protected Cache $userCache; //the individual notifications
    protected Cache $contentCache;  //the 'shared' notifications on new content that has been created
   protected Cache $artistsCache;
   protected Cache $favouritesCache;
   protected Cache $followerCache;
    protected string $campaign;
   protected string $table = BASE.'notifications';
   protected string $contentTable = BASE.'notifications_content';
@@ -139,7 +144,11 @@
     */
    public function __construct()
    {
        $this->cache = CacheManager::for('notifications', WEEK_IN_SECONDS);
        $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);
@@ -364,7 +373,7 @@
     */
    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->getVerifiedArtists();
        $artists = $this->getVerified('artist');
        return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
    }
    /**
@@ -381,7 +390,7 @@
     */
    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->getVerifiedPartners();
        $artists = $this->getVerified('partner');
        return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
    }
    /**
@@ -398,7 +407,7 @@
     */
    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->getEnthusiasts();
        $artists = $this->getUserIDs('enthusiast');
        return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
    }
    /**
@@ -415,7 +424,7 @@
     */
    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->getEveryone();
        $artists = $this->getUserIDs(Registrar::getRegistered('user'));
        return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
    }
@@ -445,7 +454,7 @@
        }
        // Check if this is a relevant content type
        $content_types = jvbBasedFeedContent();
        $content_types = array_map(function($type) {return jvbCheckBase($type->getSlug()); }, Registrar::getFeatured('show_feed', 'post'));
        if (!in_array($post->post_type, $content_types)) {
            return;
        }
@@ -1030,7 +1039,7 @@
        };
        // Send the email
        return jvbMail($user->user_email, $subject, $content, $header);
        return JVB()->email()->sendEmail($user->user_email, $subject, $content, $header);
    }
    /**
@@ -1043,46 +1052,47 @@
     *
     * @return string HTML email content
     */
    protected function generateDigestContent(WP_User $user, string $frequency, array $notifications, array $content_updates):string
    {
        $content = sprintf('<p>Hey %s,</p>', $user->first_name ?: $user->display_name);
   protected function generateDigestContent(WP_User $user, string $frequency, array $notifications, array $content_updates):string
   {
      $content = sprintf('<p>Hey %s,</p>', $user->first_name ?: $user->display_name);
        // Intro text based on frequency
        switch ($frequency) {
            case 'daily':
                $content .= '<p>Here\'s what happened in Edmonton\'s tattoo scene today:</p>';
                break;
            case 'weekly':
                $content .= '<p>Here\'s what you missed in Edmonton\'s tattoo scene this week:</p>';
                break;
            case 'monthly':
                $content .= sprintf('<p>Here\'s your monthly roundup of what happened in %s in Edmonton\'s tattoo scene:</p>', date('F'));
                break;
        }
      // Intro text based on frequency
      switch ($frequency) {
         case 'daily':
            $content .= '<p>Here\'s what happened in Edmonton\'s tattoo scene today:</p>';
            break;
         case 'weekly':
            $content .= '<p>Here\'s what you missed in Edmonton\'s tattoo scene this week:</p>';
            break;
         case 'monthly':
            $content .= sprintf('<p>Here\'s your monthly roundup of what happened in %s in Edmonton\'s tattoo scene:</p>', date('F'));
            break;
      }
        // Process artist content updates - the most visually interesting part
        $content .= $this->generateContentUpdatesSection($content_updates);
      // Process artist content updates - the most visually interesting part
      $content .= $this->generateContentUpdatesSection($content_updates);
        // Process regular notifications
        if (!empty($notifications)) {
            $content .= $this->generateNotificationsSection($notifications);
        }
      // Process regular notifications
      if (!empty($notifications)) {
         $content .= $this->generateNotificationsSection($notifications);
      }
        // Add footer content
        $content .= '<div class="divider"></div>';
        $content .= sprintf(
            '<p>You\'re receiving this %s digest because you follow artists on edmonton.ink. ' .
            'You can <a href="%s" class="text-link">adjust your notification settings</a> at any time.</p>',
            $frequency,
            esc_url(add_query_arg([
                'utm_source'   => 'email',
                'utm_medium'   => 'digest',
                'utm_campaign' => $this->campaign
            ], site_url('/dash/settings/')))
        );
      // 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/'));
        return $content;
    }
      $content .= sprintf(
         '<p>You\'re receiving this %s digest because you follow artists on edmonton.ink. You can %s at any time.</p>',
         $frequency,
         '<a href="' . esc_url($settings_url) . '">adjust your notification settings</a>'
      );
      return $content;
   }
    /**
     * Generate HTML section for content updates
@@ -1099,7 +1109,7 @@
        }
        $content = '';
        $cache   = CacheManager::for('digest_content', HOUR_IN_SECONDS * 6); // Cache for 6 hours
        $cache   = Cache::for('digest_content', HOUR_IN_SECONDS * 6); // Cache for 6 hours
        // Group updates by artist
        $updates_by_artist = [];
@@ -1234,46 +1244,39 @@
     *
     * @return string HTML content
     */
    protected function generateNotificationsSection(array $notifications):string
    {
        if (empty($notifications)) {
            return '';
        }
   protected function generateNotificationsSection(array $notifications):string
   {
      if (empty($notifications)) {
         return '';
      }
        $content = '<h3>Other Updates</h3>';
        $content .= '<ul style="padding-left: 20px;">';
      $items = [];
        // Group notifications by type
        $by_type = [];
        foreach ($notifications as $notification) {
            if (!isset($by_type[$notification->type])) {
                $by_type[ $notification->type ] = [];
            }
            $by_type[ $notification->type ][] = $notification;
        }
      // Group notifications by type
      $by_type = [];
      foreach ($notifications as $notification) {
         if (!isset($by_type[$notification->type])) {
            $by_type[$notification->type] = [];
         }
         $by_type[$notification->type][] = $notification;
      }
        // Process each type
        foreach ($by_type as $type => $type_notifications) {
            $config = $this->notification_types[ $type ] ?? [];
            $icon   = $config['icon'] ?? 'info';
      // Process each type
      foreach ($by_type as $type => $type_notifications) {
         foreach ($type_notifications as $notification) {
            $message = $notification->message;
            if (empty($message)) {
               $message = $this->generateNotificationMessage($notification);
            }
            foreach ($type_notifications as $notification) {
                $message = $notification->message;
                if (empty($message)) {
                    $message = $this->generateNotificationMessage($notification);
                }
            if (!empty($message)) {
               $items[] = ['label' => '', 'value' => $message];
            }
         }
      }
                if (!empty($message)) {
                    $content .= sprintf('<li>%s</li>', $message);
                }
            }
        }
        $content .= '</ul>';
        $content .= '<div class="divider"></div>';
        return $content;
    }
      return JVB()->email()->table($items, 'Other Updates');
   }
    /**
     * Generate a message for a notification when none is provided
@@ -1525,19 +1528,16 @@
    protected function getArtistData(int $user_id):array|false
    {
        // Try to get from cache
        $cache_key = "artist_data_{$user_id}";
        $cached = $this->cache->get($cache_key);
      $artist_id = get_user_meta($user_id, BASE . 'link', true);
      if (!$artist_id || $artist_id === '') {
         return false;
      }
        $cached = $this->artistsCache->get($artist_id);
        if ($cached !== false) {
            return $cached;
        }
        // Get artist post ID from user meta
        $artist_id = get_user_meta($user_id, BASE . 'link', true);
        if (!$artist_id) {
            return false;
        }
        // Get basic artist data
        $artist_post = get_post($artist_id);
        if (!$artist_post) {
@@ -1554,7 +1554,7 @@
        ];
        // Cache the result
        $this->cache->set($cache_key, $data, null,'artists');
        $this->artistsCache->set($artist_id, $data);
        return $data;
    }
@@ -1566,19 +1566,25 @@
     */
    protected function getFollowedArtists(int $user_id):array
    {
        global $wpdb;
        $favourites_table = $wpdb->prefix . BASE . 'favourites';
      return $this->favouritesCache->remember(
         $user_id,
         function() use ($user_id) {
            global $wpdb;
            $favourites_table = $wpdb->prefix . BASE . 'favourites';
        // Get artists this user has favourited
        return $wpdb->get_col($wpdb->prepare(
            "SELECT f.target_id
         FROM {$favourites_table} f
         JOIN {$wpdb->posts} p ON f.target_id = p.ID
         WHERE f.user_id = %d
         AND f.type = 'artist'
         AND p.post_status = 'publish'",
            $user_id
        ));
            // Get artists this user has favourited
            return $wpdb->get_col($wpdb->prepare(
            "SELECT f.target_id
                FROM {$favourites_table} f
                JOIN {$wpdb->posts} p ON f.target_id = p.ID
                WHERE f.user_id = %d
                AND f.type = 'artist'
                AND p.post_status = 'publish'",
               $user_id
            ));
         }
      );
    }
    /**
@@ -1588,15 +1594,21 @@
     */
    protected function getFollowerCount(int $artist_id):int
    {
        global $wpdb;
        $favourites_table = $wpdb->prefix . BASE . 'favourites';
      return $this->followerCache->remember(
         $artist_id,
         function() use ($artist_id) {
            global $wpdb;
            $favourites_table = $wpdb->prefix . BASE . 'favourites';
        return $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT user_id)
         FROM {$favourites_table}
         WHERE target_id = %d AND type = 'artist'",
            $artist_id
        ));
            return $wpdb->get_var($wpdb->prepare(
               "SELECT COUNT(DISTINCT user_id)
                FROM {$favourites_table}
                WHERE target_id = %d AND type = 'artist'",
               $artist_id
            ));
         }
      );
    }
    /**
@@ -1604,27 +1616,11 @@
     *
     * @return string
     */
    protected function pluralize(string $word):string
    protected function pluralize(string $content):string
    {
        $irregular = [
            'tattoo' => 'tattoos',
            'piercing' => 'piercings',
            'artwork' => 'artwork',
            'news' => 'news',
            'offer' => 'offers',
            'event' => 'events'
        ];
        if (isset($irregular[$word])) {
            return $irregular[$word];
        }
        // Simple pluralization rules
        if (str_ends_with($word, 'y')) {
            return substr($word, 0, -1) . 'ies';
        }
        return $word . 's';
      $registrar = Registrar::getInstance($content);
      return ($registrar) ? $registrar->getPlural()
         : str_replace('_', ' ', $content.'s');
    }
    /**
@@ -1634,9 +1630,8 @@
     */
    protected function clearNotificationCache(int $user_id):void
    {
        $this->cache->delete("user_{$user_id}_notifications_", 'notifications_' . $user_id);
        $this->cache->delete("user_{$user_id}_content_notifications_", 'notifications_' . $user_id);
      $this->userCache->forget($user_id);
      $this->contentCache->forget($user_id);
    }
    /**
@@ -1703,78 +1698,56 @@
    /**
     * @return array
     */
    protected function getVerifiedArtists():array
    protected function getVerified(string|array $userRoles):array
    {
        $artists = $this->cache->get('verified_artists');
        if ($artists) {
            return $artists;
        }
      $userRoles = $this->checkRoles($userRoles);
        $artists = get_users([
            'role'          => BASE.'artist',
            'capability'    => 'skip_moderation',
            'fields'        => 'ID'
        ]);
        $this->cache->set('verified_artists', $artists);
        return $artists;
      if (empty($userRoles)) {
         return [];
      }
      $cache = Cache::for('verifiedUsers', DAY_IN_SECONDS)->connect('user',true);
      return $cache->remember(
         'verified',
         function() use ($userRoles) {
            return get_users([
               'role'          => $userRoles,
               'capability'    => 'skip_moderation',
               'fields'        => 'ID'
            ]);
         }
      );
    }
    /**
     * @return array
     */
    protected function getVerifiedPartners():array
    {
        $partners = $this->cache->get('verified_partners');
        if ($partners) {
            return $partners;
        }
   protected function getUserIDs(array|string $roles):array
   {
      $roles = $this->checkRoles($roles);
      if (empty($roles)) {
         return [];
      }
      $cache = Cache::for('everyone', DAY_IN_SECONDS)->connect('user', true);
      return $cache->remember(
         $cache->generateKey($roles),
         function() use ($roles) {
            return get_users([
               'role'   => $roles,
               'fields' => 'ID'
            ]);
         }
      );
   }
        $partners = get_users([
            'role'          => BASE.'partner',
            'capability'    => 'skip_moderation',
            'fields'        => 'ID'
        ]);
   protected function checkRoles(string|array $roles):array
   {
      if (!is_array($roles)) {
         $roles = explode(',',$roles);
      }
        $this->cache->set('verified_partners', $partners);
        return $partners;
    }
    /**
     * @return array
     */
    protected function getEnthusiasts():array
    {
        $enthusiasts = $this->cache->get('enthusiasts');
        if ($enthusiasts) {
            return $enthusiasts;
        }
        $enthusiasts = get_users([
            'role'          => BASE.'enthusiast',
            'fields'        => 'ID'
        ]);
        $this->cache->set('enthusiasts', $enthusiasts);
        return $enthusiasts;
    }
    /**
     * @return array
     */
    protected function getEveryone():array
    {
        $users = $this->cache->get('users');
        if ($users) {
            return $users;
        }
        $users = get_users([
            'role__in' => [BASE.'artist', BASE.'enthusiast', BASE.'partner'],
            'fields'    => 'ID'
        ]);
        $this->cache->set('users', $users);
        return $users;
    }
      return array_map(function ($r) {
         return jvbCheckBase(trim($r));
      }, array_filter($roles, function ($r) {
         return Registrar::getInstance(trim($r))!== false;
      }));
   }
    /**
     * @param int $userID
@@ -1783,13 +1756,12 @@
     */
    protected function checkUser(int $userID):bool
    {
        $checked = $this->cache->get($userID, 'checked_users');
        if ($checked) {
            return $checked;
        }
        $test = (bool)get_userdata($userID);
        $this->cache->set($userID, $test, null, 'checked_users');
        return $test;
      $cache = Cache::for('checked_users', DAY_IN_SECONDS)->connect('user', true);
      return $cache->remember(
         $userID,
         function() use ($userID) {
            return (bool)get_userdata($userID)?:null;
         }
      );
    }
}