| 1 |
<?php |
| 2 |
// Exit if accessed directly |
| 3 |
if( ! defined( 'ABSPATH' ) ) exit; |
| 4 |
|
| 5 |
$activities = wpfval( WPF()->current_object, 'activities' ) ?: []; |
| 6 |
$args = wpfval( WPF()->current_object, 'args' ) ?: []; |
| 7 |
$activity_type = wpfval( $args, 'activity_type' ) ?: 'all'; |
| 8 |
$period = wpfval( $args, 'period' ) ?: 'week'; |
| 9 |
$items_count = wpfval( WPF()->current_object, 'items_count' ) ?: 0; |
| 10 |
$paged = wpfval( WPF()->current_object, 'paged' ) ?: 1; |
| 11 |
$items_per_page = 20; |
| 12 |
$has_more = ( $paged * $items_per_page ) < $items_count; |
| 13 |
?> |
| 14 |
<div class="wpforo-recent-activity-wrap"> |
| 15 |
<?php do_action( 'wpforo_activity_list_head' ); ?> |
| 16 |
<div class="wpf-head-bar"> |
| 17 |
<div class="wpf-head-top"> |
| 18 |
<h1 id="wpforo-title"><?php wpforo_phrase( 'Recent Activity Timeline' ) ?></h1> |
| 19 |
</div> |
| 20 |
<div class="wpf-head-bottom"> |
| 21 |
<div class="wpf-head-prefix"> |
| 22 |
<form method="get" action="<?php echo esc_url( wpforo_home_url( 'recent-activity' ) ) ?>" class="wpf-recent-activity-filter-form"> |
| 23 |
<select name="type" id="wpf-activity-type-filter" onchange="this.form.submit()"> |
| 24 |
<option value="all" <?php selected( $activity_type, 'all' ) ?>><?php wpforo_phrase( 'All Activities' ) ?></option> |
| 25 |
<option value="topic" <?php selected( $activity_type, 'topic' ) ?>><?php wpforo_phrase( 'Topics' ) ?></option> |
| 26 |
<option value="reply" <?php selected( $activity_type, 'reply' ) ?>><?php wpforo_phrase( 'Replies' ) ?></option> |
| 27 |
<option value="reaction" <?php selected( $activity_type, 'reaction' ) ?>><?php wpforo_phrase( 'Reactions' ) ?></option> |
| 28 |
<option value="favorite" <?php selected( $activity_type, 'favorite' ) ?>><?php wpforo_phrase( 'Favorites' ) ?></option> |
| 29 |
<option value="solved" <?php selected( $activity_type, 'solved' ) ?>><?php wpforo_phrase( 'Solved Topics' ) ?></option> |
| 30 |
<option value="closed" <?php selected( $activity_type, 'closed' ) ?>><?php wpforo_phrase( 'Closed Topics' ) ?></option> |
| 31 |
<option value="answer" <?php selected( $activity_type, 'answer' ) ?>><?php wpforo_phrase( 'Marked as Answer' ) ?></option> |
| 32 |
</select> |
| 33 |
<select name="period" id="wpf-activity-period-filter" onchange="this.form.submit()"> |
| 34 |
<option value="day" <?php selected( $period, 'day' ) ?>><?php wpforo_phrase( 'Last 24 Hours' ) ?></option> |
| 35 |
<option value="week" <?php selected( $period, 'week' ) ?>><?php wpforo_phrase( 'Last Week' ) ?></option> |
| 36 |
<option value="month" <?php selected( $period, 'month' ) ?>><?php wpforo_phrase( 'Last Month' ) ?></option> |
| 37 |
<option value="all" <?php selected( $period, 'all' ) ?>><?php wpforo_phrase( 'All Time' ) ?></option> |
| 38 |
</select> |
| 39 |
</form> |
| 40 |
</div> |
| 41 |
</div> |
| 42 |
</div> |
| 43 |
|
| 44 |
<div class="wpf-recent-activity-content"> |
| 45 |
<?php if( ! empty( $activities ) ) : ?> |
| 46 |
<div class="wpf-timeline" data-page="<?php echo esc_attr( $paged ) ?>" data-type="<?php echo esc_attr( $activity_type ) ?>" data-period="<?php echo esc_attr( $period ) ?>"> |
| 47 |
<div class="wpf-timeline-line"></div> |
| 48 |
<?php |
| 49 |
$can_delete = WPF()->usergroup->can( 'aum' ); |
| 50 |
foreach( $activities as $index => $activity ) : |
| 51 |
// For non-content actions (reactions, favorites, votes, etc.), the actor is in itemid_second |
| 52 |
$actor_actions = [ 'new_like', 'new_dislike', 'new_up_vote', 'new_down_vote', 'new_reaction', 'new_favorite', 'topic_solved', 'topic_closed', 'post_answer' ]; |
| 53 |
$actor_userid = in_array( $activity['type'], $actor_actions ) && ! empty( $activity['itemid_second'] ) |
| 54 |
? $activity['itemid_second'] |
| 55 |
: $activity['userid']; |
| 56 |
$member = wpforo_member( $actor_userid ); |
| 57 |
$icon = wpforo_activity_icon( $activity['type'] ); |
| 58 |
$info_html = wpforo_activity_info_html( $activity, $member ); |
| 59 |
$show_excerpt = in_array( $activity['type'], [ 'wpforo_topic', 'wpforo_post' ] ); |
| 60 |
$excerpt = ''; |
| 61 |
if( $show_excerpt && $activity['itemid'] ) { |
| 62 |
// For wpforo_topic, itemid is topicid - get first post via topic's first_postid |
| 63 |
if( $activity['type'] === 'wpforo_topic' ) { |
| 64 |
$topic_data = wpforo_topic( $activity['itemid'] ); |
| 65 |
if( $topic_data && ! empty( $topic_data['first_postid'] ) ) { |
| 66 |
$first_post = wpforo_post( $topic_data['first_postid'] ); |
| 67 |
if( $first_post && ! empty( $first_post['body'] ) ) { |
| 68 |
$excerpt = wpforo_text( $first_post['body'], 300, false ); |
| 69 |
} |
| 70 |
} |
| 71 |
} else { |
| 72 |
$post = wpforo_post( $activity['itemid'] ); |
| 73 |
if( $post && ! empty( $post['body'] ) ) { |
| 74 |
$excerpt = wpforo_text( $post['body'], 300, false ); |
| 75 |
} |
| 76 |
} |
| 77 |
} |
| 78 |
?> |
| 79 |
<div class="wpf-timeline-item wpf-timeline-<?php echo esc_attr( $activity['type'] ) ?>" data-activity-id="<?php echo esc_attr( $activity['id'] ) ?>" style="--delay: <?php echo $index * 0.03 ?>s"> |
| 80 |
<!-- Left Side: User Info --> |
| 81 |
<div class="wpf-timeline-left"> |
| 82 |
<?php if( WPF()->usergroup->can( 'va' ) && wpforo_setting( 'profiles', 'avatars' ) ) : ?> |
| 83 |
<div class="wpf-timeline-avatar"><?php echo wpforo_user_avatar( $member, 40 ) ?></div> |
| 84 |
<?php endif; ?> |
| 85 |
<div class="wpf-timeline-user"><?php wpforo_member_link( $member ) ?></div> |
| 86 |
<div class="wpf-timeline-date" style="margin-right: -51px; color: #8a969a;"><?php wpforo_date( $activity['date'], 'ago' ) ?> <span class="wpf-timeline-date-stick"> </span></div> |
| 87 |
</div> |
| 88 |
|
| 89 |
<!-- Center: Icon Node --> |
| 90 |
<div class="wpf-timeline-node"> |
| 91 |
<div class="wpf-timeline-icon"><?php echo $icon ?></div> |
| 92 |
</div> |
| 93 |
|
| 94 |
<!-- Right Side: Activity Content --> |
| 95 |
<div class="wpf-timeline-right"> |
| 96 |
<div class="wpf-timeline-card"> |
| 97 |
<?php if( $can_delete ) : ?> |
| 98 |
<button type="button" class="wpf-activity-delete" title="<?php wpforo_phrase( 'Delete Activity', false ) ?>"> |
| 99 |
<i class="fas fa-trash-alt"></i> |
| 100 |
</button> |
| 101 |
<?php endif; ?> |
| 102 |
<!-- Mobile-only header with user and date --> |
| 103 |
<div class="wpf-timeline-card-header"> |
| 104 |
<div class="wpf-timeline-date"><?php wpforo_date( $activity['date'], 'ago' ) ?></div> |
| 105 |
<div class="wpf-timeline-user"><?php wpforo_member_link( $member ) ?></div> |
| 106 |
</div> |
| 107 |
<div class="wpf-timeline-activity"><?php echo $info_html ?></div> |
| 108 |
<?php if( $show_excerpt && $excerpt ) : ?> |
| 109 |
<div class="wpf-timeline-excerpt"><?php echo esc_html( $excerpt ) ?></div> |
| 110 |
<?php endif; ?> |
| 111 |
</div> |
| 112 |
</div> |
| 113 |
</div> |
| 114 |
<?php endforeach; ?> |
| 115 |
</div> |
| 116 |
|
| 117 |
<?php if( $has_more ) : ?> |
| 118 |
<div class="wpf-timeline-load-more-wrap"> |
| 119 |
<button type="button" class="wpf-timeline-load-more wpf-button wpf-button-primary" data-loading="<?php wpforo_phrase( 'Loading...', false ) ?>"> |
| 120 |
<span class="wpf-load-more-text"><?php wpforo_phrase( 'Load More Activities' ) ?></span> |
| 121 |
<span class="wpf-load-more-spinner"></span> |
| 122 |
</button> |
| 123 |
</div> |
| 124 |
<?php endif; ?> |
| 125 |
|
| 126 |
<?php else : ?> |
| 127 |
<div class="wpf-timeline-empty"> |
| 128 |
<div class="wpf-timeline-empty-icon"><i class="far fa-clock"></i></div> |
| 129 |
<p><?php wpforo_phrase( 'No activities found' ) ?></p> |
| 130 |
</div> |
| 131 |
<?php endif; ?> |
| 132 |
</div> |
| 133 |
|
| 134 |
<?php do_action( 'wpforo_activity_list_footer' ); ?> |
| 135 |
</div> |
| 136 |
|
| 137 |
<script> |
| 138 |
(function() { |
| 139 |
document.addEventListener('DOMContentLoaded', function() { |
| 140 |
// Intersection Observer for fade-in animations |
| 141 |
const observer = new IntersectionObserver((entries) => { |
| 142 |
entries.forEach(entry => { |
| 143 |
if (entry.isIntersecting) { |
| 144 |
entry.target.classList.add('wpf-timeline-visible'); |
| 145 |
observer.unobserve(entry.target); |
| 146 |
} |
| 147 |
}); |
| 148 |
}, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }); |
| 149 |
|
| 150 |
document.querySelectorAll('.wpf-timeline-item').forEach(item => { |
| 151 |
observer.observe(item); |
| 152 |
}); |
| 153 |
|
| 154 |
// Load More functionality |
| 155 |
const loadMoreBtn = document.querySelector('.wpf-timeline-load-more'); |
| 156 |
if (loadMoreBtn) { |
| 157 |
loadMoreBtn.addEventListener('click', function() { |
| 158 |
const timeline = document.querySelector('.wpf-timeline'); |
| 159 |
const btn = this; |
| 160 |
const btnText = btn.querySelector('.wpf-load-more-text'); |
| 161 |
const originalText = btnText.textContent; |
| 162 |
|
| 163 |
if (btn.classList.contains('wpf-loading')) return; |
| 164 |
|
| 165 |
btn.classList.add('wpf-loading'); |
| 166 |
btnText.textContent = btn.dataset.loading; |
| 167 |
|
| 168 |
const currentPage = parseInt(timeline.dataset.page) || 1; |
| 169 |
const nextPage = currentPage + 1; |
| 170 |
const type = timeline.dataset.type || 'all'; |
| 171 |
const period = timeline.dataset.period || 'week'; |
| 172 |
|
| 173 |
const formData = new FormData(); |
| 174 |
formData.append('action', 'wpforo_load_more_activities'); |
| 175 |
formData.append('page', nextPage); |
| 176 |
formData.append('type', type); |
| 177 |
formData.append('period', period); |
| 178 |
formData.append('_wpfnonce', (typeof wpforo !== 'undefined' && wpforo.nonces ? wpforo.nonces.wpforo_load_more_activities : '')); |
| 179 |
|
| 180 |
fetch((typeof wpforo !== 'undefined' ? wpforo.ajax_url : '/wp-admin/admin-ajax.php'), { |
| 181 |
method: 'POST', |
| 182 |
body: formData |
| 183 |
}) |
| 184 |
.then(response => response.json()) |
| 185 |
.then(data => { |
| 186 |
btn.classList.remove('wpf-loading'); |
| 187 |
btnText.textContent = originalText; |
| 188 |
|
| 189 |
if (data.success && data.data.html) { |
| 190 |
// Append new items at the end of the timeline |
| 191 |
timeline.insertAdjacentHTML('beforeend', data.data.html); |
| 192 |
timeline.dataset.page = nextPage; |
| 193 |
|
| 194 |
// Observe new items |
| 195 |
timeline.querySelectorAll('.wpf-timeline-item:not(.wpf-timeline-visible)').forEach(item => { |
| 196 |
observer.observe(item); |
| 197 |
}); |
| 198 |
|
| 199 |
if (!data.data.has_more) { |
| 200 |
btn.parentElement.remove(); |
| 201 |
} |
| 202 |
} |
| 203 |
}) |
| 204 |
.catch(error => { |
| 205 |
btn.classList.remove('wpf-loading'); |
| 206 |
btnText.textContent = originalText; |
| 207 |
console.error('Load more error:', error); |
| 208 |
}); |
| 209 |
}); |
| 210 |
} |
| 211 |
|
| 212 |
// Delete activity functionality |
| 213 |
document.addEventListener('click', function(e) { |
| 214 |
const deleteBtn = e.target.closest('.wpf-activity-delete'); |
| 215 |
if (!deleteBtn) return; |
| 216 |
|
| 217 |
e.preventDefault(); |
| 218 |
e.stopPropagation(); |
| 219 |
|
| 220 |
const timelineItem = deleteBtn.closest('.wpf-timeline-item'); |
| 221 |
if (!timelineItem) return; |
| 222 |
|
| 223 |
const activityId = timelineItem.dataset.activityId; |
| 224 |
if (!activityId) return; |
| 225 |
|
| 226 |
if (!confirm('<?php echo esc_js( wpforo_phrase( 'Are you sure you want to delete this activity?', false ) ) ?>')) { |
| 227 |
return; |
| 228 |
} |
| 229 |
|
| 230 |
deleteBtn.classList.add('wpf-deleting'); |
| 231 |
|
| 232 |
const formData = new FormData(); |
| 233 |
formData.append('action', 'wpforo_delete_activity'); |
| 234 |
formData.append('activity_id', activityId); |
| 235 |
formData.append('_wpfnonce', (typeof wpforo !== 'undefined' && wpforo.nonces ? wpforo.nonces.wpforo_delete_activity : '')); |
| 236 |
|
| 237 |
fetch((typeof wpforo !== 'undefined' ? wpforo.ajax_url : '/wp-admin/admin-ajax.php'), { |
| 238 |
method: 'POST', |
| 239 |
body: formData |
| 240 |
}) |
| 241 |
.then(response => response.json()) |
| 242 |
.then(data => { |
| 243 |
if (data.success) { |
| 244 |
timelineItem.style.opacity = '0'; |
| 245 |
timelineItem.style.transform = 'translateX(100px)'; |
| 246 |
setTimeout(() => { |
| 247 |
timelineItem.remove(); |
| 248 |
}, 300); |
| 249 |
} else { |
| 250 |
deleteBtn.classList.remove('wpf-deleting'); |
| 251 |
alert(data.data && data.data.message ? data.data.message : '<?php echo esc_js( wpforo_phrase( 'Failed to delete activity', false ) ) ?>'); |
| 252 |
} |
| 253 |
}) |
| 254 |
.catch(error => { |
| 255 |
deleteBtn.classList.remove('wpf-deleting'); |
| 256 |
console.error('Delete error:', error); |
| 257 |
}); |
| 258 |
}); |
| 259 |
}); |
| 260 |
})(); |
| 261 |
</script> |
| 262 |
|