1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.

Creating the add-on Similar Threads v1.1

Discussion in 'XenForo Development Discussions' started by AndyB, Oct 18, 2013.

  1. AndyB

    AndyB Well-Known Member

    Description:

    The Similar Threads add-on will do the following:
    • Show a list of similar threads when a user is creating a new thread (location below thread title)
    • Show a list of similar threads when a user is viewing a thread (location below quick reply)
    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 similar threads list is displayed
    • When a user is viewing a thread, the similar threads list is displayed below the quick reply
    Example:

    pic001.jpg
     
    Pereira likes this.
  2. AndyB

    AndyB Well-Known Member

    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 Thread.php file
    6. Create the Model.php file
    7. Create the SimilarThreads.php file
    8. Create the Listener.php file
    9. Create the Route Prefix
    10. Create the Code Event Listener (Thread)
    11. Create the Template andy_similarthreads
    12. Create the Option Group
    13. Create the Options
    14. Create the Template Modifications
    15. Create the page_container_js_head Template Modification
    16. Create the thread_create #1 Template Modification
    17. Create the thread_create #2 Template Modification
    18. Create the thread_view Template Modification
    19. Create the xenforo_data_table.css Template Modification
    20. Create the New Phrase
     
  3. AndyB

    AndyB Well-Known Member

    Create the directory structure:

    js
    --xenforo
    ----similarthreads.js
    library
    --Andy
    ----SimilarThreads
    ------ControllerPublic
    --------Abstract.php
    --------Thread.php
    ------Model.php
    ------Route
    --------Prefix
    ----------SimilarThreads.php
     
  4. AndyB

    AndyB Well-Known Member

    Create the New Add-on:

    pic002.jpg
     
  5. AndyB

    AndyB Well-Known Member

    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);
    [/php]
     
  6. AndyB

    AndyB Well-Known Member

    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()
        { 
            
    // get options from Admin CP -> Options -> Similar Threads -> Show on new thread    
            
    $showNew XenForo_Application::get('options')->showNew;    
        
            
    // show similar threads if true
            
    if ($showNew)
            {
                
    // declare variables
                
    $searchWord '';
                
    $searchWords = array();
                
    $safeSearchWord '';   
                
    $threadId '';    
                    
                
    // 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,$threadId);    
                } 
                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);
            }
        }
    }

    ?>
     
  7. AndyB

    AndyB Well-Known Member

    Create the Thread.php file:

    library/Andy/SimilarThreads/ControllerPublic/Thread.php

    PHP:
    <?php

    class Andy_SimilarThreads_ControllerPublic_Thread extends XFCP_Andy_SimilarThreads_ControllerPublic_Thread
    {    
        public function 
    actionIndex()
        {    
            
    // get options from Admin CP -> Options -> Similar Threads -> Show on Thread View    
            
    $showThreadView XenForo_Application::get('options')->showThreadView;    
        
            
    // show similar threads if true
            
    if ($showThreadView)
            {                             
                
    //########################################
                // start default xenforo code
                //########################################        
                
                
    $threadId $this->_input->filterSingle('thread_id'XenForo_Input::UINT);
        
                
    $ftpHelper $this->getHelper('ForumThreadPost');
                list(
    $threadFetchOptions$forumFetchOptions) = $this->_getThreadForumFetchOptions();
                list(
    $thread$forum) = $ftpHelper->assertThreadValidAndViewable($threadId$threadFetchOptions$forumFetchOptions);
        
                
    $visitor XenForo_Visitor::getInstance();
                
    $threadModel $this->_getThreadModel();
                
    $postModel $this->_getPostModel();
        
                if (
    $threadModel->isRedirect($thread))
                {
                    
    $redirect $this->getModelFromCache('XenForo_Model_ThreadRedirect')->getThreadRedirectById($thread['thread_id']);
                    if (!
    $redirect)
                    {
                        return 
    $this->responseNoPermission();
                    }
                    else
                    {
                        return 
    $this->responseRedirect(
                            
    XenForo_ControllerResponse_Redirect::RESOURCE_CANONICAL_PERMANENT,
                            
    $redirect['target_url']
                        );
                    }
                }
        
                
    $page max(1$this->_input->filterSingle('page'XenForo_Input::UINT));
                
    $postsPerPage XenForo_Application::get('options')->messagesPerPage;
        
                
    $this->canonicalizePageNumber($page$postsPerPage$thread['reply_count'] + 1'threads'$thread);
                
    $this->canonicalizeRequestUrl(
                    
    XenForo_Link::buildPublicLink('threads'$thread, array('page' => $page))
                );
        
                
    $postFetchOptions $this->_getPostFetchOptions($thread$forum);
                
    $postFetchOptions += array(
                    
    'perPage' => $postsPerPage,
                    
    'page' => $page
                
    );
        
                
    $posts $postModel->getPostsInThread($threadId$postFetchOptions);
        
                
    // TODO: add a sanity check to ensure we got posts (invalid thread if page 1, invalid page otherwise)
        
                
    $posts $postModel->getAndMergeAttachmentsIntoPosts($posts);
        
                
    $inlineModOptions = array();
                
    $maxPostDate 0;
                
    $firstUnreadPostId 0;
        
                
    $deletedPosts 0;
                
    $moderatedPosts 0;
        
                
    $pagePosition 0;
        
                
    $permissions $visitor->getNodePermissions($thread['node_id']);
                foreach (
    $posts AS &$post)
                {
                    
    $post['position_on_page'] = ++$pagePosition;
        
                    
    $postModOptions $postModel->addInlineModOptionToPost(
                        
    $post$thread$forum$permissions
                    
    );
                    
    $inlineModOptions += $postModOptions;
        
                    
    $post $postModel->preparePost($post$thread$forum$permissions);
        
                    if (
    $post['post_date'] > $maxPostDate)
                    {
                        
    $maxPostDate $post['post_date'];
                    }
        
                    if (
    $post['isDeleted'])
                    {
                        
    $deletedPosts++;
                    }
                    if (
    $post['isModerated'])
                    {
                        
    $moderatedPosts++;
                    }
        
                    if (!
    $firstUnreadPostId && $post['isNew'])
                    {
                        
    $firstUnreadPostId $post['post_id'];
                    }
                }
        
                if (
    $firstUnreadPostId)
                {
                    
    $requestPaths XenForo_Application::get('requestPaths');
                    
    $unreadLink $requestPaths['requestUri'] . '#post-' $firstUnreadPostId;
                }
                else if (
    $thread['isNew'])
                {
                    
    $unreadLink XenForo_Link::buildPublicLink('threads/unread'$thread);
                }
                else
                {
                    
    $unreadLink '';
                }
        
                
    $attachmentHash null;
                if (!empty(
    $thread['draft_extra']))
                {
                    
    $draftExtra = @unserialize($thread['draft_extra']);
                    if (!empty(
    $draftExtra['attachment_hash']))
                    {
                        
    $attachmentHash $draftExtra['attachment_hash'];
                    }
                }
        
                
    $attachmentParams $this->_getForumModel()->getAttachmentParams($forum, array(
                    
    'thread_id' => $thread['thread_id']
                ), 
    nullnull$attachmentHash);
        
                if (
    $thread['discussion_type'] == 'poll')
                {
                    
    $pollModel $this->_getPollModel();
                    
    $poll $pollModel->getPollByContent('thread'$threadId);
                    if (
    $poll)
                    {
                        
    $poll $pollModel->preparePoll($poll$threadModel->canVoteOnPoll($thread$forum));
                        
    $poll['canEdit'] = $threadModel->canEditPoll($thread$forum);
                    }
                }
                else
                {
                    
    $poll false;
                }
        
                
    $threadModel->markThreadRead($thread$forum$maxPostDate);
                
    $threadModel->logThreadView($threadId);
                
                
    //########################################
                // start add-on code
                //########################################    
                
                // declare variables
                
    $searchWord '';
                
    $searchWords = array();
                
    $safeSearchWord ''
                
                
    // get thread title
                
    $parent parent::actionIndex();
                if (
    $parent instanceof XenForo_ControllerResponse_View)
                {
                    
    $threadTitle $parent->params['thread']['title'];
                }                     
            
                
    // put into array
                
    $threadTitle explode(' '$threadTitle);
                
                
    // 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($threadTitle 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,$threadId);    
                } 
                else 
                {
                    
    // $viewParams needs to be an array
                    
    $threads = array();                
                } 
        
                
    //########################################
                // end add-on code
                //########################################    
        
                
    $viewParams $this->_getDefaultViewParams($forum$thread$posts$page, array(
                    
    'deletedPosts' => $deletedPosts,
                    
    'moderatedPosts' => $moderatedPosts,
        
                    
    'inlineModOptions' => $inlineModOptions,
        
                    
    'firstPost' => reset($posts),
                    
    'lastPost' => end($posts),
                    
    'unreadLink' => $unreadLink,
        
                    
    'poll' => $poll,
        
                    
    'attachmentParams' => $attachmentParams,
                    
    'attachmentConstraints' => $this->_getAttachmentModel()->getAttachmentConstraints(),
        
                    
    'showPostedNotice' => $this->_input->filterSingle('posted'XenForo_Input::UINT),
        
                    
    'nodeBreadCrumbs' => $ftpHelper->getNodeBreadCrumbs($forum),
                    
                    
    // add-on variables added here
                    
    'threads' => $threads,
                    
    'searchWord' => $searchWord,
                ));
                    
                return 
    $this->responseView('XenForo_ViewPublic_Thread_View''thread_view'$viewParams);
            }
            else
            {
                
    // run the original actionIndex() function 
                
    return parent::actionIndex();    
            }
        }
    }

    ?>
     
  8. AndyB

    AndyB Well-Known Member

    Create the Model.php file:

    library/Andy/SimilarThreads/Model.php

    PHP:
    <?php

    class Andy_SimilarThreads_Model extends XenForo_Model
    {    
        public function 
    getThreads($safeSearchWord,$threadId)
        {         
            
    // 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 whereclause1 for forums the user cannot view
            
    if (isset($forum_id_no_permission))
            {
                
    $whereclause1 'AND xf_thread.node_id <> ' implode(' AND xf_thread.node_id <> '$forum_id_no_permission);
            }
            else
            {
                
    $whereclause1 '';
            } 
            
            
    // declare variable
            
    $whereclause2 '';
            
            
    // coming from Abstract.php (thread view), don't include the thread we are viewing
            
    if ($threadId != '')
            {
                
    $whereclause2 "AND xf_thread.thread_id <> '$threadId'";
            }
            
            
    // 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_thread.node_id, 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%'
                AND xf_thread.discussion_type <> 'redirect'
                
    $whereclause1
                
    $whereclause2
                ORDER BY xf_thread.thread_id DESC
                LIMIT 
    $maxResults
            "
    );        
        }
    }

    ?>
     
  9. AndyB

    AndyB Well-Known Member

    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($routePathZend_Controller_Request_Http $requestXenForo_Router $router)
        {
            return 
    $router->getRouteMatch('Andy_SimilarThreads_ControllerPublic_Abstract''Index'$routePath);
        }
    }

    ?>
     
  10. AndyB

    AndyB Well-Known Member

    Create the Listener.php file:

    PHP:
    <?php

    class Andy_SimilarThreads_Listener
    {
        public static function 
    Thread($class, array &$extend)
        {
            
    $extend[] = 'Andy_SimilarThreads_ControllerPublic_Thread';
        }
    }

    ?>
     
  11. AndyB

    AndyB Well-Known Member

    Create the Route Prefix:

    pic003.jpg
     
  12. AndyB

    AndyB Well-Known Member

    Create the Code Event Listener (Thread):

    pic004.jpg
     
  13. AndyB

    AndyB Well-Known Member

    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><a href="{xen:link threads, $thread}" title="{$thread.nodetitle}" target="_blank">{$thread.nodetitle}</a></td>
            <td><a href="{xen:link threads, $thread}" title="{$thread.title}" target="_blank">{$thread.title}</a></td>
            </tr>
            
            </xen:foreach>
            
            </table>
    
        </div>
    
    </xen:if>
    
     
  14. AndyB

    AndyB Well-Known Member

    Create the Option Group:

    This will add to the Admin CP to allow configuring the Similar Threads add-on.

    Admin CP -> Options -> + Add Option Group

    pic005.jpg
     
  15. AndyB

    AndyB Well-Known Member

    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:

    pic006.jpg
     
  16. AndyB

    AndyB Well-Known Member

    Create the Template Modifications:

    There are five Template Modifications that need to be created.

    The final result will look like this:

    pic007.jpg
     
  17. AndyB

    AndyB Well-Known Member

    Create the page_container_js_head Template Modification:

    Find:
    Code:
    <!--XenForo_Require:JS-->
    
    Replace:
    Code:
    $0
    <script src="js/xenforo/similarthreads.js"></script>
    
     
  18. AndyB

    AndyB Well-Known Member

    Create the thread_create #1 Template Modification:

    Find:
    Code:
    data-redirect="on"
    
    Replace:
    Code:
    $0
    id="similarthreadsId" autocomplete="off"
    
     
  19. AndyB

    AndyB Well-Known Member

    Create the thread_create #2 Template Modification:

    Find:
    Code:
    <dl class="ctrlUnit fullWidth">
    
    Replace:
    Code:
    <dl id="similarthreadsId-result"></dl>$0
    
     
  20. AndyB

    AndyB Well-Known Member

    Create the thread_view Template Modification:

    Find:
    Code:
    <xen:if is="{$canQuickReply}">
       <xen:include template="quick_reply">
         <xen:set var="$formAction">{xen:link 'threads/add-reply', $thread}</xen:set>
         <xen:set var="$lastDate">{$lastPost.post_date}</xen:set>
         <xen:set var="$lastKnownDate">{$thread.last_post_date}</xen:set>
         <xen:set var="$showMoreOptions">1</xen:set>
       </xen:include>
    </xen:if>
    
    Replace:
    Code:
    $0
    <xen:include template="andy_similarthreads" />
    
     

Share This Page