Forum
The great place to discuss topics with other users
How to Mix Forum Threads and Replies into the Main News Feed in Sngine
'Hi everyone,
I’d like to share a complete approach for integrating forum activity directly into the main news feed in Sngine.
The goal is simple:
- when a user creates a new forum thread, it appears in the feed
- when a user replies to a forum thread, it appears in the feed
- forum items are mixed with normal posts by date
- reply items can show a short preview excerpt
- thread/reply items redirect directly to the correct discussion or reply
- no likes/comments/shares are added to forum feed cards, since they are not useful here
This solution uses:
- a dedicated SQL table:
forum_timeline - two small additions in
includes/ajax/forums/thread.phpandincludes/ajax/forums/reply.php - one block in
index.phpto merge forum activity with feed posts - one modification in
__feeds_post.tplto render forum items inside the normal feed loop - optional CSS/JS for styling and to avoid feed staging conflicts
1) SQL table
Run this once in your database:
CREATE TABLE IF NOT EXISTS forum_timeline (
id INT AUTO_INCREMENT PRIMARY KEY,
thread_id INT,
reply_id INT DEFAULT NULL,
user_id INT,
action_type ENUM('thread','reply'),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX(thread_id),
INDEX(created_at)
);
2) includes/ajax/forums/thread.php
This keeps the original thread creation behavior and adds a timeline insert.
<?php
/**
* ajax -> forums -> thread
*
* @package Sngine
* @author Zamblek
*/
// fetch bootstrap
require('../../../bootstrap.php');
// check AJAX Request
is_ajax();
// user access
user_access(true);
/**
* IMPORTANT
* Releases the session lock to avoid AJAX blocking
* during forum thread creation / editing.
*/
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
// check demo account
if ($user->_data['user_demo']) {
modal("ERROR", __("Demo Restriction"), __("You can't do this with demo account"));
}
try {
// initialize return array
$return = [];
switch ($_GET['do']) {
case 'create':
// validate input
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
_error(400);
}
// create thread
$thread_id = $user->post_forum_thread($_GET['id'], $_POST['title'], $_POST['text']);
/**
* Add to forum timeline
*/
$thread_id_int = (int) $thread_id;
$user_id = (int) $user->_data['user_id'];
$db->query("
INSERT INTO forum_timeline (thread_id, user_id, action_type)
VALUES ({$thread_id_int}, {$user_id}, 'thread')
");
// return
$return['path'] = $system['system_url'] . '/forums/thread/' . $thread_id_int . '/' . get_url_text($_POST['title']);
$return['callback'] = '
$(".modal").modal("hide");
$(".modal-backdrop").remove();
$("body").removeClass("modal-open").css("padding-right", "");
setTimeout(function() {
window.location.href = response.path;
}, 150);
';
break;
case 'edit':
// validate input
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
_error(400);
}
// edit thread
$user->edit_forum_thread($_GET['id'], $_POST['title'], $_POST['text']);
// return
$return['path'] = $system['system_url'] . '/forums/thread/' . (int) $_GET['id'] . '/' . get_url_text($_POST['title']);
$return['callback'] = '
$(".modal").modal("hide");
$(".modal-backdrop").remove();
$("body").removeClass("modal-open").css("padding-right", "");
setTimeout(function() {
window.location.href = response.path;
}, 150);
';
break;
default:
_error(400);
break;
}
return_json($return);
} catch (Exception $e) {
return_json([
'error' => true,
'message' => $e->getMessage()
]);
}
3) includes/ajax/forums/reply.php
This keeps the original reply behavior and adds a timeline insert.
<?php
/**
* ajax -> forums -> reply
*
* @package Sngine
* @author Zamblek
*/
// fetch bootstrap
require('../../../bootstrap.php');
// check AJAX Request
is_ajax();
// user access
user_access(true);
/**
* IMPORTANT
* Releases the session lock to avoid AJAX blocking
*/
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
// check demo account
if ($user->_data['user_demo']) {
modal("ERROR", __("Demo Restriction"), __("You can't do this with demo account"));
}
try {
// initialize return
$return = [];
switch ($_GET['do']) {
case 'create':
// validate input
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
_error(400);
}
// create reply
$reply = $user->post_forum_reply($_GET['id'], $_POST['text']);
/**
* Add to forum timeline
*/
$thread_id = (int) $reply['thread']['thread_id'];
$reply_id = (int) $reply['reply_id'];
$user_id = (int) $user->_data['user_id'];
$db->query("
INSERT INTO forum_timeline (thread_id, reply_id, user_id, action_type)
VALUES ({$thread_id}, {$reply_id}, {$user_id}, 'reply')
");
// return
$return['path'] = $system['system_url'] . '/forums/thread/' . $thread_id . '/' . $reply['thread']['title_url'] . "#reply-" . $reply_id;
$return['callback'] = '
$(".modal").modal("hide");
$(".modal-backdrop").remove();
$("body").removeClass("modal-open").css("padding-right", "");
setTimeout(function() {
window.location.href = response.path;
}, 150);
';
break;
case 'edit':
// validate input
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
_error(400);
}
// edit reply
$reply = $user->edit_forum_reply($_GET['id'], $_POST['text']);
$return['path'] = $system['system_url'] . '/forums/thread/' . $reply['thread']['thread_id'] . '/' . $reply['thread']['title_url'] . "#reply-" . $reply['reply_id'];
$return['callback'] = '
$(".modal").modal("hide");
$(".modal-backdrop").remove();
$("body").removeClass("modal-open").css("padding-right", "");
setTimeout(function() {
window.location.href = response.path;
}, 150);
';
break;
default:
_error(400);
break;
}
return_json($return);
} catch (Exception $e) {
return_json([
'error' => true,
'message' => $e->getMessage()
]);
}
4) index.php
The idea here is:
- fetch normal feed posts as usual
- fetch forum timeline items separately
- convert forum items into post-like arrays
- merge both arrays
- sort by
time - assign the merged result back to
$posts
Add this near the top of try {:
$forum_timeline_items = [];
$show_forum_timeline = false;
Enable it on the homepage only:
if ($_GET['view'] == "") {
$show_forum_timeline = true;
}
And inside case '':
$show_forum_timeline = true;
Then, before:
// get trending hashtags
add this full block:
/* forum timeline items for homepage only */
if ($show_forum_timeline) {
$get_forum_timeline = $db->query("
SELECT
ft.id,
ft.thread_id,
ft.reply_id,
ft.user_id,
ft.action_type,
ft.created_at,
t.*,
lr.last_reply_id,
fr.text AS reply_text,
COUNT(fr_all.reply_id) AS replies_count,
u.user_name,
u.user_firstname,
u.user_lastname,
u.user_picture
FROM forum_timeline AS ft
LEFT JOIN forums_threads AS t ON t.thread_id = ft.thread_id
LEFT JOIN forums_replies fr ON fr.reply_id = ft.reply_id
LEFT JOIN (
SELECT thread_id, MAX(reply_id) AS last_reply_id
FROM forums_replies
GROUP BY thread_id
) AS lr ON lr.thread_id = ft.thread_id
LEFT JOIN forums_replies fr_all ON fr_all.thread_id = ft.thread_id
LEFT JOIN users AS u ON u.user_id = ft.user_id
GROUP BY ft.id
ORDER BY ft.created_at DESC
LIMIT 5
");
if ($get_forum_timeline && $get_forum_timeline->num_rows > 0) {
while ($row = $get_forum_timeline->fetch_assoc()) {
/* detect real thread title field */
$thread_title = '';
if (!empty($row['title'])) {
$thread_title = $row['title'];
} elseif (!empty($row['thread_title'])) {
$thread_title = $row['thread_title'];
} elseif (!empty($row['subject'])) {
$thread_title = $row['subject'];
} elseif (!empty($row['name'])) {
$thread_title = $row['name'];
}
if (!$row['thread_id'] || !$thread_title) {
continue;
}
/* clean reply excerpt */
$excerpt = '';
if (!empty($row['reply_text'])) {
$excerpt = html_entity_decode($row['reply_text'], ENT_QUOTES, 'UTF-8');
$excerpt = preg_replace('/<br\s*\/?>/i', ' ', $excerpt);
$excerpt = strip_tags($excerpt);
$excerpt = preg_replace('/\s+/', ' ', $excerpt);
$excerpt = trim($excerpt);
if ($excerpt !== '') {
$full_excerpt = $excerpt;
if (function_exists('mb_substr')) {
$excerpt = mb_substr($excerpt, 0, 120, 'UTF-8');
if (mb_strlen($full_excerpt, 'UTF-8') > 120) {
$excerpt .= '...';
}
} else {
$excerpt = substr($excerpt, 0, 120);
if (strlen($full_excerpt) > 120) {
$excerpt .= '...';
}
}
}
}
$forum_timeline_items[] = [
'post_id' => 'forum_' . (int) $row['id'],
'post_type' => 'forum',
'time' => $row['created_at'],
'author_id' => (int) $row['user_id'],
'post_author_name' => trim($row['user_firstname'] . ' ' . $row['user_lastname']),
'post_author_url' => $system['system_url'] . '/' . $row['user_name'],
'post_author_picture' => get_picture($row['user_picture'], 'user'),
'forum_data' => [
'thread_id' => (int) $row['thread_id'],
'reply_id' => !empty($row['reply_id']) ? (int) $row['reply_id'] : null,
'last_reply_id' => !empty($row['last_reply_id']) ? (int) $row['last_reply_id'] : null,
'replies_count' => (int) $row['replies_count'],
'title' => $thread_title,
'title_url' => get_url_text($thread_title),
'action_type' => $row['action_type'],
'excerpt' => ($row['action_type'] === 'reply') ? $excerpt : ''
]
];
}
}
if (!empty($forum_timeline_items) && !empty($posts) && is_array($posts)) {
$posts = array_merge($posts, $forum_timeline_items);
usort($posts, function ($a, $b) {
return strtotime($b['time']) - strtotime($a['time']);
});
$smarty->assign('posts', $posts);
} elseif (!empty($forum_timeline_items) && empty($posts)) {
$posts = $forum_timeline_items;
$smarty->assign('posts', $posts);
}
}
5) __feeds_post.tpl
To make forum items behave like feed items, render them through the same loop.
Replace the file with this structure:
{if $post['post_type'] == "forum"}
{if !$standalone}<li>{/if}
<div class="post forum-timeline-card" data-id="{$post['post_id']}">
<div class="post-body">
<div class="post-header">
<div class="post-avatar">
<a class="post-avatar-picture" href="{$post['post_author_url']}" style="background-image:url({$post['post_author_picture']});"></a>
</div>
<div class="post-meta">
<span class="post-author">
<a href="{$post['post_author_url']}">{$post['post_author_name']}</a>
</span>
<span class="post-title forum-timeline-action">
{if $post['forum_data']['action_type'] == "thread"}
created a new forum discussion
{else}
replied to a discussion
{/if}
</span>
<div class="post-time">
<span class="js_moment" data-time="{$post['time']}">{$post['time']}</span>
</div>
</div>
</div>
<div class="forum-timeline-content">
<div class="forum-timeline-badge">
<i class="fa fa-comments"></i>
</div>
<div class="forum-timeline-main">
<div class="forum-timeline-label">
{if $post['forum_data']['action_type'] == "thread"}
New thread
{else}
Active discussion
{/if}
</div>
<h4 class="forum-timeline-title">
<a href="{if $post['forum_data']['action_type'] == 'reply' && $post['forum_data']['reply_id']}{$system['system_url']}/forums/thread/{$post['forum_data']['thread_id']}/{$post['forum_data']['title_url']}#reply-{$post['forum_data']['reply_id']}{elseif $post['forum_data']['action_type'] == 'thread' && $post['forum_data']['last_reply_id']}{$system['system_url']}/forums/thread/{$post['forum_data']['thread_id']}/{$post['forum_data']['title_url']}#reply-{$post['forum_data']['last_reply_id']}{else}{$system['system_url']}/forums/thread/{$post['forum_data']['thread_id']}/{$post['forum_data']['title_url']}{/if}">
{$post['forum_data']['title']}
</a>
</h4>
<div class="forum-timeline-stats">
<i class="fa fa-comments"></i>
{$post['forum_data']['replies_count']} {if $post['forum_data']['replies_count'] > 1}replies{else}reply{/if}
</div>
{if $post['forum_data']['excerpt']}
<div class="forum-timeline-excerpt">
{$post['forum_data']['excerpt']}
</div>
{/if}
<div class="forum-timeline-button-wrap">
<a href="{if $post['forum_data']['action_type'] == 'reply' && $post['forum_data']['reply_id']}{$system['system_url']}/forums/thread/{$post['forum_data']['thread_id']}/{$post['forum_data']['title_url']}#reply-{$post['forum_data']['reply_id']}{elseif $post['forum_data']['action_type'] == 'thread' && $post['forum_data']['last_reply_id']}{$system['system_url']}/forums/thread/{$post['forum_data']['thread_id']}/{$post['forum_data']['title_url']}#reply-{$post['forum_data']['last_reply_id']}{else}{$system['system_url']}/forums/thread/{$post['forum_data']['thread_id']}/{$post['forum_data']['title_url']}{/if}" class="btn btn-primary btn-sm forum-timeline-btn">
{if $post['forum_data']['action_type'] == "thread"}
View discussion
{else}
View reply
{/if}
</a>
</div>
</div>
</div>
</div>
</div>
{if !$standalone}</li>{/if}
{else}
{if !$standalone}<li>{/if}
<!-- original Sngine post content stays here exactly as before -->
...
{if !$standalone}</li>{/if}
{/if}
In practice, keep your original normal post code inside the {else} block unchanged.
6) Remove the old separate forum block
If you previously had something like:
{if $forum_timeline_items}
...
{/if}
above the posts stream, remove it completely.
That old block makes forum items stay pinned above the feed instead of being mixed with posts.
7) CSS (custom.css)
.forum-timeline-wrapper {
max-width: 700px;
margin: 0 auto 20px auto;
}
.forum-timeline-stream {
list-style: none;
margin: 0;
padding: 0;
}
.forum-timeline-stream > li {
margin-bottom: 15px;
}
.forum-timeline-card {
border: 1px solid #e9ecef;
border-radius: 14px;
overflow: hidden;
background: #fff;
box-shadow: 0 4px 14px rgba(0,0,0,0.05);
}
.forum-timeline-card .post-body {
padding: 15px;
}
.forum-timeline-action {
display: block;
color: #666;
font-size: 14px;
margin-top: 2px;
}
.forum-timeline-content {
margin-top: 12px;
display: flex;
gap: 12px;
background: #f8fbff;
border: 1px solid #e6f0ff;
border-radius: 12px;
padding: 15px;
}
.forum-timeline-badge {
width: 45px;
height: 45px;
min-width: 45px;
border-radius: 10px;
background: #eaf3ff;
color: #1877f2;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.forum-timeline-main {
flex: 1;
}
.forum-timeline-label {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
color: #1877f2;
margin-bottom: 5px;
}
.forum-timeline-title {
margin: 0 0 10px 0;
font-size: 18px;
font-weight: 700;
}
.forum-timeline-title a {
color: #222;
text-decoration: none;
}
.forum-timeline-title a:hover {
color: #1877f2;
}
.forum-timeline-excerpt {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.forum-timeline-button-wrap {
text-align: center;
}
.forum-timeline-btn {
display: inline-block;
min-width: 220px;
border-radius: 50px;
font-weight: 600;
padding: 8px 20px;
}
.forum-timeline-stats {
font-size: 13px;
color: #666;
margin-bottom: 10px;
}
.forum-timeline-stats i {
margin-right: 5px;
color: #1877f2;
}
@media (max-width: 768px) {
.forum-timeline-wrapper {
max-width: 100%;
}
.forum-timeline-content {
flex-direction: column;
}
.forum-timeline-btn {
width: 100%;
}
}
8) Optional JS to disable feed staging conflicts
document.addEventListener("DOMContentLoaded", function () {
const btn = document.querySelector('.js_view-staging-posts');
if (btn) {
btn.remove();
}
const staging = document.querySelector('.js_posts_stream_staging');
if (staging) {
staging.innerHTML = '';
}
window.disable_posts_staging = true;
});
Final result
With this setup:
- forum threads and replies are stored in
forum_timeline - forum activity is converted into feed-compatible items
- forum items are merged with normal posts
- everything is sorted by timestamp
- forum cards display inside the same feed loop as regular posts
That gives a much more natural feed experience than showing forum activity in a static block above the timeline.
Hope this helps other Sngine developers working on deeper forum/feed integration.
