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 '
'
. '
'
. '' . self::icon() . ' ' . esc_html__( 'Questions and answers', 'rooster-lms' ) . ' '
. ' '
. '
'
. '
'
. '' . \RoosterLMS\Elementor\CourseSearch::icon( 'close' ) . '' . esc_html__( 'Close', 'rooster-lms' ) . ' '
. self::shell( $item_id, $block ) . '
';
}
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 = ''
. ''
. esc_html__( 'Questions', 'rooster-lms' ) . ' ' . esc_html( Format::number( $page['total'] ) ) . ' '
. ( $asks ? '' . esc_html__( 'Ask a question', 'rooster-lms' ) . ' ' : '' )
. '
';
$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 ? '' : ''
. self::form( $prefix . '-new', 0, __( 'Your question', 'rooster-lms' ), __( 'Post question', 'rooster-lms' ), ! $staff && ( 3 === $mode || 4 === $mode ), ! $staff && CourseFacts::qa_private( $item->course_id ) ) . '
';
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 .= '' . self::post( $q, $user, $staff, $item->course_id ) . ' ';
}
$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 .= '' . self::post( $r, $user, $staff, $course ) . ' ';
}
$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 = ''
. ''
. '' . $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 '';
}
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' ),
);
}
}