Creating the add-on Similar Threads

AndyB

Well-known member
Description:

The Similar Threads add-on will show a list of similar threads when a user is creating a new thread.

Key Features:
  • Limit number of results (Admin Control Panel Setting)
  • Exclude common words (Admin Control Panel Setting)
  • Simple one word search
  • Node permissions honored
  • Similar Threads immediately displayed
  • Automatically disabled for displays 800px or less (Responsive design)
  • Fully phrased
How it Works:

When a user types a new thread title, as soon as a non-common word is entered a list of similar threads is displayed.

Example:

pic001.webp
 
This tutorial will show the steps required to create this add-on:
  1. Create the directory structure
  2. Create the New Add-on
  3. Create the similarthreads.js file
  4. Create the Abstract.php file
  5. Create the Model.php file
  6. Create the SimilarThreads.php file
  7. Create the Route Prefix
  8. Create the Template andy_similarthreads
  9. Create the Option Group
  10. Create the Options
  11. Create the Template Modifications
  12. Create the page_container_js_head Template Modification
  13. Create the thread_create #1 Template Modification
  14. Create the thread_create #2 Template Modification
  15. Create the xenforo_data_table.css Template Modification
  16. Create the New Phrase
 
Create the directory structure:
js
--xenforo
----similarthreads.js
library
--Andy
----SimilarThreads
------ControllerPublic
--------Abstract.php
------Model.php
------Route
--------Prefix
----------SimilarThreads.php
 
Create the similarthreads.js file:

js/xenforo/similarthreads.js

Code:
!function($, window, document, _undefined)
{
	XenForo.similarthreadsId = function($form)
	{		
		var typewatch = (function()
		{
			var timer = 0;
			return function(callback, ms)
			{
				clearTimeout (timer);
				timer = setTimeout(callback, ms);
			}  
		})(); 
		$title = $form.find('input[name="title"]');
		$title.keyup(function() 
		{
			typewatch(function () 
			{
				XenForo.ajax(
				$('base').attr('href') + 'similarthreads/',
				$form.serializeArray(),
				function(ajaxData, textStatus)
				{
					if (ajaxData.templateHtml)
					{
						new XenForo.ExtLoader(ajaxData, function()
						{
							$('#similarthreadsId-result').html('<div>' + ajaxData.templateHtml + '</div>');
						});
					}
				});
			}, 500);
		});		
	}	
	XenForo.register('#similarthreadsId', 'XenForo.similarthreadsId');	
}
(jQuery, this, document);
 
Create the Abstract.php file:

library/Andy/SimilarThreads/ControllerPublic/Abstract.php

PHP:
<?php

class Andy_SimilarThreads_ControllerPublic_Abstract extends XenForo_ControllerPublic_Abstract
{
    public function actionIndex()
    {            
        // declare variables
        $searchWord = '';
        $searchWords = array();
        $safeSearchWord = '';       
            
        // get newTitle
        $newTitle = $this->_request->getParam('title');
        
        // put into array
        $newTitle = explode(' ', $newTitle);
        
        // get options from Admin CP -> Options -> Similar Threads -> Common Words    
        $commonWords = XenForo_Application::get('options')->commonWords;
        
        // convert to lowercase
        $commonWords = strtolower($commonWords);    
        
        // put $commonWordsLower into an array
        $commonWords = explode(' ', $commonWords); 
        
        // remove any common words from array
        foreach($newTitle as $var)
        {
            if (!in_array(strtolower($var), $commonWords))
            {
                $searchWords[] = $var;
            }
        }
		
		$count = count($searchWords);
        
        // only continue if we have a search word
        if ($count > 0)
        {
            // get first none common word
            $searchWord = $searchWords[0];
        
            // make safe for database
            $safeSearchWord = addslashes($searchWord);
        }
        
        // run query only if we have a search
        if ($safeSearchWord != '')
        {
            // run query in model    
            $threads = $this->getModelFromCache('Andy_SimilarThreads_Model')->getThreads($safeSearchWord);    
        } 
        else 
        {
            // $viewParams needs to be an array
            $threads = array();                
        }
        
        // prepare $viewParams for template
        $viewParams = array(
            'threads' => $threads,
            'searchWord' => $searchWord
        );
        
        // send to template
        return $this->responseView('Andy_SimilarThreads_ViewPublic_SimilarThreads', 'andy_similarthreads', $viewParams);
    }
}

?>
 
Create the Model.php file:

library/Andy/SimilarThreads/Model.php

PHP:
<?php

class Andy_SimilarThreads_Model extends XenForo_Model
{
	public function getThreads($safeSearchWord)
	{ 		
	
		// declare variable	
		$visitor = XenForo_Visitor::getInstance();			
		$permissionCombinationId = $visitor['permission_combination_id'];							
		
		// get permissions
		$permissions = $this->_getDb()->fetchAll("
			SELECT xf_permission_cache_content.cache_value, xf_permission_cache_content.content_id
			FROM xf_permission_cache_content
			INNER JOIN xf_forum ON xf_forum.node_id = xf_permission_cache_content.content_id
			WHERE xf_permission_cache_content.permission_combination_id = '$permissionCombinationId'
			ORDER BY xf_permission_cache_content.content_id ASC
		");	
		
		foreach ($permissions AS $k => $v)
		{
			$cache_value[] = unserialize($v['cache_value']);
			$content_id[] = $v['content_id'];				
		}
		
		$i = 0;
		
		foreach ($cache_value AS $v)
		{
			$view[] = $v['view'];
			
			if ($v['view'] != 1)
			{
				$forum_id_no_permission[] = $content_id[$i];
			}
			$i++;
		} 
		
		// create whereclause for forums the user cannot view
		if (isset($forum_id_no_permission))
		{
			$whereclause = 'AND xf_thread.node_id <> ' . implode(' AND xf_thread.node_id <> ', $forum_id_no_permission);
		}
		else
		{
			$whereclause = '';
		} 
		
		// get option from Admin CP -> Options -> Similar Threads -> Maximum Results	
		$maxResults = XenForo_Application::get('options')->maxResults;		
		
		// get threads
		return $this->_getDb()->fetchAll("
			SELECT xf_thread.thread_id, xf_thread.title, xf_node.title AS nodetitle
			FROM xf_thread
			INNER JOIN xf_node ON xf_node.node_id = xf_thread.node_id
			WHERE xf_thread.title LIKE '%$safeSearchWord%'
			$whereclause
			ORDER BY xf_thread.thread_id DESC
			LIMIT $maxResults
		");		
	}
}

?>
 
Last edited:
Create the SimilarThreads.php file:

library/Andy/SimilarThreads/Model/Route/Prefix/SimilarThreads.php

PHP:
<?php

class Andy_SimilarThreads_Route_Prefix_SimilarThreads implements XenForo_Route_Interface
{
	public function match($routePath, Zend_Controller_Request_Http $request, XenForo_Router $router)
	{
		return $router->getRouteMatch('Andy_SimilarThreads_ControllerPublic_Abstract', 'Index', $routePath);
	}
}

?>
 
Create the Template andy_similarthreads:

Code:
<xen:if is="{$threads}">

  <div class="sectionMain similarThreads">
  
  <div class="primaryContent">
     {xen:phrase similar_threads_found_with_the_word_in_the_thread_title, 'searchWord={$searchWord}'}
  </div>
  
  <table class="dataTable">
  
  <tr class="dataRow">
  <th>{xen:phrase forum}</th>
  <th>{xen:phrase title}</th>
  </tr>
  
  <xen:foreach loop="$threads" key="$index" value="$thread">
  
  <tr class="dataRow">
  <td>{$thread.nodetitle}</td>
  <td><a href="{xen:link threads, $thread}" title="{$thread.title}" target="_blank">{$thread.title}</a></td>
  </tr>
  
  </xen:foreach>
  
  </table>

  </div>

</xen:if>

<xen:if is="!{$threads}">
</xen:if>
 
Last edited:
Create the Option Group:

This will add an option group to the Admin CP in order to allow configuring the Similar Threads add-on.

Admin CP -> Options -> + Add Option Group

pic004.webp
 
Last edited:
Create the Options:

Create the two options. Refer to the similarthreads.xml file for detailed information on each option.

The final result will look like this:

pic005.webp
 
Create the Template Modifications:

There are four Template Modifications that need to be created.

The final result will look like this:

pic006.webp
 
Create the page_container_js_head Template Modification:

Find:
Code:
<!--XenForo_Require:JS-->

Replace:
Code:
$0
<script src="js/xenforo/similarthreads.js"></script>
 
Create the thread_create #1 Template Modification:

Find:
Code:
data-redirect="on"

Replace:
Code:
$0
id="similarthreadsId" autocomplete="off"
 
Create the thread_create #2 Template Modification:

Find:
Code:
<dl class="ctrlUnit fullWidth">

Replace:
Code:
<dl id="similarthreadsId-result"></dl>$0
 
Create the xenforo_data_table.css Template Modification:

Find:
Code:
/** Data tables **/

Replace:
Code:
$0
.similarThreads {
    margin: 0px 30px 0px 30px;
    padding: 0px 15px 5px 15px;
}

@media (max-width:@maxResponsiveWideWidth){
    .similarthreads {
        display: none;
    }
}
 
Create the New Phrase:

Title
Code:
similar_threads_found_with_the_word_in_the_thread_title

Phrase text
Code:
Similar threads found with the word ({searchWord}) in the thread title:
 
Top Bottom