Fóruns

The great place to discuss topics with other users

How to Mix Forum Threads and Replies into the Main News Feed in Sngine

'
Join the Conversation Resposta ao Post
Vince33 Vince33
Moderator
Joined: 2025-05-22 18:50:59
2026-04-12 14:22:59

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.php and includes/ajax/forums/reply.php
  • one block in index.php to merge forum activity with feed posts
  • one modification in __feeds_post.tpl to 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.

Nife Mhi
Member
Joined: 2025-12-13 22:39:24
2026-04-13 20:54:07

Thank you 🙏🌹❤🥰

Jane Marcia
Admin
Joined: 2025-05-17 02:14:16
2026-04-14 05:34:59

o screenshot? :( 

Thank you for sharing :)

Vince33 Vince33
Moderator
Joined: 2025-05-22 18:50:59
2026-04-14 14:33:35

Jane Marcia
Admin
Joined: 2025-05-17 02:14:16
2026-04-14 23:55:46

Very nice. Look similar to mine but mine doesn't show the answers because I don't want them replying on feed. But I didn't have any extra database table though... Just code changes. 

ScriptsTribe https://scriptstribe.com