isset( $attributes['lessonPost'] ) ? max( 0, (int) $attributes['lessonPost'] ) : 0 ); } public static function render( array $attributes, string $content = '', ?\WP_Block $block = null ): string { unset( $content ); $item_id = Context::item_id( self::attributes( $attributes ), $block ); LessonExtras::rendered( self::NAME, $item_id ); $user = get_current_user_id(); if ( $item_id <= 0 || $user <= 0 ) { return ''; } $raw = $block instanceof \WP_Block ? (array) ( $block->parsed_block['attrs'] ?? array() ) : $attributes; $panel = ! empty( $raw['panel'] ) || self::panel_by_setting( $raw, $item_id ); return $panel ? self::panel( $item_id, $block ) : self::shell( $item_id, $block ); } private static function panel_by_setting( array $attributes, int $item_id ): bool { return ! isset( $attributes['panel'] ) && 'end' !== \RoosterLMS\Community\CommunitySettings::get( 'qa_place' ) && ! is_admin() && ! wp_is_json_request() && self::own_page( $item_id ); } private static function own_page( int $item_id ): bool { if ( $item_id <= 0 ) { return false; } if ( \RoosterLMS\Routing\RouteContext::item_id() > 0 ) { return \RoosterLMS\Routing\RouteContext::item_id() === $item_id; } $queried = is_singular() ? get_queried_object() : null; return $queried instanceof \WP_Post && Context::is_unit_post( $queried ) && Context::item_id( array( 'lessonPost' => (int) $queried->ID ), null ) === $item_id; } public static function deferred( $deferred, $block = array() ) { if ( ! is_array( $block ) || 'rooster-lms/' . self::NAME !== ( $block['blockName'] ?? '' ) ) { return $deferred; } $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array(); return ! empty( $attributes['panel'] ) || self::panel_by_setting( $attributes, Context::item_id( self::attributes( $attributes ), null ) ) ? false : $deferred; } public static function panel( int $item_id, ?\WP_Block $block ): string { if ( isset( self::$panels[ $item_id ] ) ) { return ''; } self::$panels[ $item_id ] = true; $id = 'rl-qa-panel-' . $item_id; $place = 'corner' === \RoosterLMS\Community\CommunitySettings::get( 'qa_place' ) ? 'corner' : 'side'; return '
' . '' . '' . '
'; } private static function icon(): string { if ( function_exists( 'rooster_icon' ) && \RoosterLMS\UI\Theme::declares( 'C11' ) ) { $icon = (string) rooster_icon( 'circle-question-mark', array( 'width' => 20, 'height' => 20, ) ); if ( '' !== $icon ) { return $icon; } } return ''; } public static function shell( int $item_id, ?\WP_Block $block ): string { Front::enqueue( self::NAME ); $config = Front::config( array( 'item' => $item_id, 'i18n' => self::texts(), ) ); $prefix = 'rl-qa-' . $item_id; return '
(string) wp_json_encode( $config ), 'aria-labelledby' => $prefix . '-h', 'aria-busy' => 'true' ) ) . '>' . '

' . esc_html__( 'Questions and answers', 'rooster-lms' ) . '

' . '

' . esc_html__( 'Loading…', 'rooster-lms' ) . '

' . '
'; } public static function may_read( int $user, Item $item ): bool { if ( ! CourseFacts::is_member( $user, $item->course_id ) ) { return false; } return 1 !== CourseFacts::qa_mode( $item->course_id ) || CourseFacts::is_staff( $user, $item->course_id ); } public static function tab( Item $item, int $user, ?array $page = null ): string { $mode = CourseFacts::qa_mode( $item->course_id ); $staff = CourseFacts::is_staff( $user, $item->course_id ); $page = $page ?? self::page( $item, $user, 1 ); $prefix = 'rl-qa-' . $item->id; $asks = 1 !== $mode && ( $staff || Discussions::LED !== $mode ); $meta = array( 'course' => $item->course_id, 'unit' => $item->unit_id, 'item' => $item->id, 'staff' => $staff, 'held' => ! $staff && in_array( Discussions::hold_mode( $mode ), array( 3, 4 ), true ), ); $tabs = '
' . '' . ( $asks ? '' : '' ) . '
'; $list = '
' . ( 1 === $mode ? Ui::notice( __( 'Q&A is switched off for this course: learners do not see these posts and cannot write.', 'rooster-lms' ), 'warning' ) : '' ) . ( Discussions::LED === $mode && ! $staff ? '

' . esc_html__( 'The course staff start the questions here; you can reply to them.', 'rooster-lms' ) . '

' : '' ) . '
' . $page['html'] . '
'; $ask = ! $asks ? '' : ''; return '' . $tabs . $list . $ask; } public static function page( Item $item, int $user, int $page ): array { $out = Services::discussions()->questions( $user, $item->course_id, $item->unit_id, max( 1, $page ), self::PAGE_SIZE ); $total = (int) $out['total']; $pages = max( 1, (int) ceil( $total / self::PAGE_SIZE ) ); if ( array() === $out['items'] ) { $led = Discussions::LED === CourseFacts::qa_mode( $item->course_id ) && ! CourseFacts::is_staff( $user, $item->course_id ); return array( 'html' => Ui::empty_state( __( 'No questions yet', 'rooster-lms' ), $led ? __( 'The course staff have not started a question about this lesson yet.', 'rooster-lms' ) : __( 'Ask the first question about this lesson.', 'rooster-lms' ) ), 'total' => $total, 'pages' => $pages, ); } $staff = self::staff_ids( $item->course_id, $out['items'] ); $html = '
    '; foreach ( $out['items'] as $q ) { $html .= '
  1. ' . self::post( $q, $user, $staff, $item->course_id ) . '
  2. '; } $html .= '
'; if ( $pages > 1 ) { $page = max( 1, min( $pages, $page ) ); $html .= ''; } return array( 'html' => $html, 'total' => $total, 'pages' => $pages, ); } public static function thread( int $user, int $id ): string { $thread = Services::discussions()->thread( $user, $id ); if ( null === $thread ) { return ''; } $course = (int) $thread['question']['course_id']; $replies = $thread['replies']; usort( $replies, static fn( array $a, array $b ): int => array( $a['is_answer'] ? 0 : 1, $a['created_at'], $a['id'] ) <=> array( $b['is_answer'] ? 0 : 1, $b['created_at'], $b['id'] ) ); $staff = self::staff_ids( $course, array_merge( array( $thread['question'] ), $replies ) ); $html = '
    '; foreach ( $replies as $r ) { $html .= '
  1. ' . self::post( $r, $user, $staff, $course ) . '
  2. '; } $html .= '
'; if ( array() === $replies ) { $html = '

' . esc_html__( 'No replies yet.', 'rooster-lms' ) . '

'; } $mode = CourseFacts::qa_mode( $course ); $is_mgr = CourseFacts::is_staff( $user, $course ); if ( 1 !== $mode && 'visible' === $thread['question']['state'] ) { $held = ! $is_mgr && ! $thread['question']['private'] && in_array( Discussions::hold_mode( $mode ), array( 3, 4 ), true ); $html .= self::form( 'rl-qa-reply-' . (int) $thread['question']['id'], (int) $thread['question']['id'], __( 'Your reply', 'rooster-lms' ), __( 'Post reply', 'rooster-lms' ), $held ); } return $html; } public static function post( array $p, int $user, array $staff, int $course ): string { $question = 0 === (int) $p['parent_id']; $manager = CourseFacts::is_staff( $user, $course ); $badges = ''; if ( isset( $staff[ (int) $p['user_id'] ] ) ) { $badges .= Ui::badge( __( 'Staff', 'rooster-lms' ), 'primary sm' ); } if ( $question && $p['pinned'] ) { $badges .= Ui::badge( __( 'Pinned', 'rooster-lms' ), 'info sm' ); } if ( $question && ! empty( $p['private'] ) ) { $badges .= Ui::badge( __( 'Private', 'rooster-lms' ), 'secondary sm' ); } if ( ! $question && $p['is_answer'] ) { $badges .= Ui::badge( __( 'Answer', 'rooster-lms' ), 'success sm' ); } if ( 'pending' === $p['state'] ) { $badges .= Ui::badge( $p['own'] ? __( 'Waiting for approval', 'rooster-lms' ) : __( 'Held for approval', 'rooster-lms' ), 'warning sm' ); } elseif ( 'hidden' === $p['state'] ) { $badges .= Ui::badge( __( 'Hidden', 'rooster-lms' ), 'secondary sm' ); } $author = '' !== (string) $p['author'] ? (string) $p['author'] : __( 'Former member', 'rooster-lms' ); $id = (int) $p['id']; $actions = ''; if ( $question ) { /* translators: %s: number of replies. */ $label = sprintf( _n( '%s reply', '%s replies', (int) $p['reply_count'], 'rooster-lms' ), Format::number( (int) $p['reply_count'] ) ); $actions .= Ui::button( $label, 'ghost', array( 'size' => 'sm', 'aria-expanded' => 'false', 'aria-controls' => 'rl-qa-thread-' . $id, 'data-rl-qa-replies' => (string) $id, ) ); } $editable = ( $p['own'] && in_array( $p['state'], array( 'visible', 'pending' ), true ) ) || $manager; if ( $editable ) { $actions .= Ui::button( __( 'Edit', 'rooster-lms' ), 'ghost', array( 'size' => 'sm', 'data-rl-qa-edit' => (string) $id ) ); } if ( $p['own'] || $manager ) { $actions .= Ui::button( __( 'Delete', 'rooster-lms' ), 'ghost', array( 'size' => 'sm', 'data-rl-qa-delete' => (string) $id ) ); } if ( $manager ) { if ( 'pending' === $p['state'] || 'hidden' === $p['state'] ) { $actions .= Ui::button( __( 'Approve', 'rooster-lms' ), 'outline', array( 'size' => 'sm', 'data-rl-qa-set' => $id . ':state:visible' ) ); } if ( 'visible' === $p['state'] ) { $actions .= Ui::button( __( 'Hide', 'rooster-lms' ), 'ghost', array( 'size' => 'sm', 'data-rl-qa-set' => $id . ':state:hidden' ) ); } if ( $question ) { $actions .= Ui::button( $p['pinned'] ? __( 'Unpin', 'rooster-lms' ) : __( 'Pin', 'rooster-lms' ), 'ghost', array( 'size' => 'sm', 'data-rl-qa-set' => $id . ':pinned:' . ( $p['pinned'] ? '0' : '1' ) ) ); } else { $actions .= Ui::button( $p['is_answer'] ? __( 'Not the answer', 'rooster-lms' ) : __( 'Mark as the answer', 'rooster-lms' ), 'ghost', array( 'size' => 'sm', 'data-rl-qa-set' => $id . ':is_answer:' . ( $p['is_answer'] ? '0' : '1' ) ) ); } } $time = Format::date( (string) $p['created_at'] ); $body = wp_kses( (string) $p['body'], Discussions::KSES_TAGS ); $html = '
' . '
' . esc_html( $author ) . ' ' . $badges . '
' . '
' . $body . '
' . ( $editable ? '' : '' ) . ( '' !== $actions ? '
' . $actions . '
' : '' ); if ( $question ) { $html .= ''; } return $html . '
'; } private static function form( string $id, int $parent, string $label, string $submit, bool $held, bool $offer = false ): string { return '
' . '

' . '

' . ( $offer ? '

' . Ui::check( '', esc_html__( 'Only the course staff see this question', 'rooster-lms' ) ) . '

' : '' ) . ( $held ? '

' . esc_html__( 'The course staff approve posts before other learners see them.', 'rooster-lms' ) . ( $offer ? ' ' . esc_html__( 'A private question needs no approval.', 'rooster-lms' ) : '' ) . '

' : '' ) . '

' . Ui::button( $submit, 'primary', array( 'type' => 'submit', 'size' => 'sm' ) ) . '

' . '
'; } public static function staff_ids( int $course, array $posts ): array { if ( ! isset( self::$staff[ $course ] ) ) { $ids = array_fill_keys( array_keys( Services::course_staff()->for_course( $course ) ), true ); $home = (int) ( CourseFacts::course( $course )['home_post_id'] ?? 0 ); $auth = $home > 0 ? (int) get_post_field( 'post_author', $home ) : 0; if ( $auth > 0 ) { $ids[ $auth ] = true; } self::$staff[ $course ] = $ids; } $out = self::$staff[ $course ]; foreach ( $posts as $p ) { $u = (int) ( $p['user_id'] ?? 0 ); if ( $u > 0 && ! isset( $out[ $u ] ) && ( Users::can( $u, 'manage_options' ) || Users::can( $u, 'rooster_lms_manage_orgs' ) ) ) { $out[ $u ] = true; } } return $out; } public static function forget(): void { self::$staff = array(); self::$panels = array(); } public static function texts(): array { return array( 'posted' => __( 'Your post is published.', 'rooster-lms' ), 'pending' => __( 'Thank you. Your post is waiting for approval by the course staff.', 'rooster-lms' ), 'private' => __( 'Your question is sent. Only you and the course staff can read it.', 'rooster-lms' ), 'edited' => __( 'Your changes are saved.', 'rooster-lms' ), 'reheld' => __( 'Your changes are saved. The course staff check edited posts again before others see them.', 'rooster-lms' ), 'confirm' => __( 'Delete this post?', 'rooster-lms' ), 'delete' => __( 'Delete', 'rooster-lms' ), 'deleted' => __( 'The post is deleted.', 'rooster-lms' ), 'empty' => __( 'Write something first.', 'rooster-lms' ), 'failed' => __( 'Something went wrong. Please try again.', 'rooster-lms' ), 'save' => __( 'Save', 'rooster-lms' ), 'cancel' => __( 'Cancel', 'rooster-lms' ), 'editHelp' => __( 'The course staff check edited posts again before others see them.', 'rooster-lms' ), 'loading' => __( 'Loading…', 'rooster-lms' ), ); } }