Store::prune() ); // Cron passes '' as the first arg; keep the int default. add_action( 'init', array( $this, 'schedule' ) ); add_action( 'dashbread_register', array( $this, 'core_types' ) ); // Built-in sources. add_action( 'comment_post', array( $this, 'on_comment' ), 10, 3 ); add_action( 'transition_post_status', array( $this, 'on_publish' ), 10, 3 ); add_action( 'woocommerce_order_status_changed', array( $this, 'on_order_status' ), 10, 3 ); add_action( 'dashbread_notify_batch', array( $this, 'notify_batch' ), 10, 3 ); } public function schedule(): void { if ( ! wp_next_scheduled( 'dashbread_daily' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'dashbread_daily' ); } } public function core_types( Registry $r ): void { $r->register_page( 'notifications', array( 'title' => __( 'Notifications', 'dashbread' ), 'icon' => 'bell', 'position' => 940, 'render' => static function (): void { echo wp_kses( \Dashbread\Shell\Islands::render( 'notifications-list', array( 'perPage' => 20 ) ), \Dashbread\Core\Kses::ui() ); }, ) ); $r->register_notification_type( 'comment_reply', array( 'label' => __( 'Replies to your comments', 'dashbread' ), 'icon' => 'message-square' ) ); $r->register_notification_type( 'new_content', array( 'label' => __( 'New content in the app', 'dashbread' ), 'icon' => 'file-text' ) ); $r->register_notification_type( 'order_status', array( 'label' => __( 'Order updates', 'dashbread' ), 'icon' => 'shopping-bag' ) ); $r->register_notification_type( 'broadcast', array( 'label' => __( 'Announcements', 'dashbread' ), 'icon' => 'megaphone' ) ); } /** * Send a notification. Honors the member's per-type preference (usermeta dashbread_notify_off[]). * * @param array{title?:string,body?:string,url?:string,icon?:string,data?:array} $payload */ public static function notify( int $user_id, string $type, array $payload ): int { $type = sanitize_key( $type ); $off = (array) get_user_meta( $user_id, 'dashbread_notify_off', true ); if ( in_array( $type, $off, true ) ) { return 0; } $types = Registry::instance()->notification_types(); if ( empty( $payload['icon'] ) && isset( $types[ $type ]['icon'] ) ) { $payload['icon'] = (string) $types[ $type ]['icon']; } /** @param array $payload @param int $user_id @param string $type */ $payload = (array) apply_filters( 'dashbread_notify_payload', $payload, $user_id, $type ); $id = Store::insert( $user_id, $type, $payload ); /** Fires after a notification is stored; Pro hooks email delivery here. */ do_action( 'dashbread_notified', $id, $user_id, $type, $payload ); return $id; } /** Notify many users without blocking the request: 200 per cron batch. @param int[] $user_ids */ public static function notify_many( array $user_ids, string $type, array $payload ): void { $user_ids = array_values( array_unique( array_map( 'intval', $user_ids ) ) ); if ( count( $user_ids ) <= 50 ) { foreach ( $user_ids as $uid ) { self::notify( $uid, $type, $payload ); } return; } foreach ( array_chunk( $user_ids, 200 ) as $i => $chunk ) { wp_schedule_single_event( time() + $i * 5, 'dashbread_notify_batch', array( $chunk, $type, $payload ) ); } } public function notify_batch( array $user_ids, string $type, array $payload ): void { foreach ( $user_ids as $uid ) { self::notify( (int) $uid, $type, $payload ); } } /* ---------- built-in sources ---------- */ public function on_comment( int $comment_id, $approved, array $data ): void { if ( 1 !== (int) $approved ) { return; } $comment = get_comment( $comment_id ); if ( ! $comment ) { return; } $post = get_post( (int) $comment->comment_post_ID ); $targets = array(); if ( $comment->comment_parent ) { $parent = get_comment( (int) $comment->comment_parent ); if ( $parent && (int) $parent->user_id && (int) $parent->user_id !== (int) $comment->user_id ) { $targets[] = (int) $parent->user_id; } } if ( $post && (int) $post->post_author && (int) $post->post_author !== (int) $comment->user_id ) { $targets[] = (int) $post->post_author; } foreach ( array_unique( $targets ) as $uid ) { self::notify( $uid, 'comment_reply', array( 'title' => sprintf( /* translators: 1: commenter, 2: post title */ __( '%1$s replied on "%2$s"', 'dashbread' ), $comment->comment_author, $post ? get_the_title( $post ) : '' ), 'body' => wp_trim_words( wp_strip_all_tags( (string) $comment->comment_content ), 20 ), 'url' => get_comment_link( $comment ), ) ); } } /** A post flagged "Show in Dashbread app" going live notifies every member who can see it. */ public function on_publish( string $new, string $old, \WP_Post $post ): void { if ( 'publish' !== $new || 'publish' === $old || ! (bool) get_post_meta( $post->ID, HostedPages::META_IN_APP, true ) ) { return; } $roles = array_filter( (array) get_post_meta( $post->ID, HostedPages::META_ROLES, true ) ); $args = array( 'fields' => 'ID', 'number' => 5000 ); if ( $roles ) { $args['role__in'] = $roles; } $ids = array_map( 'intval', (array) get_users( $args ) ); self::notify_many( array_diff( $ids, array( (int) $post->post_author ) ), 'new_content', array( 'title' => sprintf( /* translators: %s: post title */ __( 'New: %s', 'dashbread' ), get_the_title( $post ) ), 'body' => wp_trim_words( wp_strip_all_tags( get_the_excerpt( $post ) ), 20 ), 'url' => \Dashbread\Core\Settings::app_url( $post->post_name ), ) ); } public function on_order_status( int $order_id, string $from, string $to ): void { if ( ! function_exists( 'wc_get_order' ) ) { return; } $order = wc_get_order( $order_id ); if ( ! $order || ! $order->get_customer_id() ) { return; } self::notify( (int) $order->get_customer_id(), 'order_status', array( 'title' => sprintf( /* translators: 1: order number, 2: status */ __( 'Order #%1$s is now %2$s', 'dashbread' ), $order->get_order_number(), wc_get_order_status_name( $to ) ), 'url' => $order->get_view_order_url(), 'data' => array( 'order_id' => $order_id, 'status' => $to ), ) ); } }