PageRenderTime 47ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/controllers/ETConversationsController.class.php

https://github.com/Ramir1/esoTalk
PHP | 364 lines | 203 code | 62 blank | 99 comment | 33 complexity | 71a70e5eecc586ca19b1fe7f4ff95c9b MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. // Copyright 2011 Toby Zerner, Simon Zerner
  3. // This file is part of esoTalk. Please see the included license file for usage information.
  4. if (!defined("IN_ESOTALK")) exit;
  5. /**
  6. * The conversations controller displays a list of conversations, and allows filtering by channels
  7. * and gambits. It also handles marking all conversations as read, and has a method which provides
  8. * auto-refresh results for the conversations view.
  9. *
  10. * @package esoTalk
  11. */
  12. class ETConversationsController extends ETController {
  13. /**
  14. * Display a list of conversations, optionally filtered by channel(s) and a search string.
  15. *
  16. * @return void
  17. */
  18. function index($channelSlug = false)
  19. {
  20. // Add the default gambits to the gambit cloud: gambit text => css class to apply.
  21. $gambits = array(
  22. T("gambit.active last ? hours") => "gambit-activeLastHours",
  23. T("gambit.active last ? days") => "gambit-activeLastDays",
  24. T("gambit.active today") => "gambit-activeToday",
  25. T("gambit.author:").T("gambit.member") => "gambit-author",
  26. T("gambit.contributor:").T("gambit.member") => "gambit-contributor",
  27. T("gambit.dead") => "gambit-dead",
  28. T("gambit.has replies") => "gambit-hasReplies",
  29. T("gambit.has >10 replies") => "gambit-replies",
  30. T("gambit.locked") => "gambit-locked",
  31. T("gambit.more results") => "gambit-more",
  32. T("gambit.order by newest") => "gambit-orderByNewest",
  33. T("gambit.order by replies") => "gambit-orderByReplies",
  34. T("gambit.random") => "gambit-random",
  35. T("gambit.reverse") => "gambit-reverse",
  36. T("gambit.sticky") => "gambit-sticky",
  37. );
  38. // Add some more personal gambits if there is a user logged in.
  39. if (ET::$session->user) {
  40. $gambits += array(
  41. T("gambit.contributor:").T("gambit.myself") => "gambit-contributorMyself",
  42. T("gambit.author:").T("gambit.myself") => "gambit-authorMyself",
  43. T("gambit.draft") => "gambit-draft",
  44. T("gambit.muted") => "gambit-muted",
  45. T("gambit.private") => "gambit-private",
  46. T("gambit.starred") => "gambit-starred",
  47. T("gambit.unread") => "gambit-unread"
  48. );
  49. }
  50. list($channelInfo, $currentChannels, $channelIds, $includeDescendants) = $this->getSelectedChannels($channelSlug);
  51. // Now we need to construct some arrays to determine which channel "tabs" to show in the view.
  52. // $channels is a list of channels with the same parent as the current selected channel(s).
  53. // $path is a breadcrumb trail to the depth of the currently selected channel(s).
  54. $channels = array();
  55. $path = array();
  56. // Work out what channel we will use as the "parent" channel. This will be the last item in $path,
  57. // and its children will be in $channels.
  58. $curChannel = false;
  59. // If channels have been selected, use the first of them.
  60. if (count($currentChannels)) $curChannel = $channelInfo[$currentChannels[0]];
  61. // If the currently selected channel has no children, or if we're not including descendants, use
  62. // its parent as the parent channel.
  63. if (($curChannel and $curChannel["lft"] >= $curChannel["rgt"] - 1) or !$includeDescendants)
  64. $curChannel = @$channelInfo[$curChannel["parentId"]];
  65. // If no channel is selected, make a faux parent channel.
  66. if (!$curChannel) $curChannel = array("lft" => 0, "rgt" => PHP_INT_MAX, "depth" => -1);
  67. // Now, finally, go through all the channels and add ancestors of the "parent" channel to the $path,
  68. // and direct children to the list of $channels. Make sure we don't include any channels which
  69. // the user has unsubscribed to.
  70. foreach ($channelInfo as $channel) {
  71. if ($channel["lft"] > $curChannel["lft"] and $channel["rgt"] < $curChannel["rgt"] and $channel["depth"] == $curChannel["depth"] + 1 and empty($channel["unsubscribed"]))
  72. $channels[] = $channel;
  73. elseif ($channel["lft"] <= $curChannel["lft"] and $channel["rgt"] >= $curChannel["rgt"])
  74. $path[] = $channel;
  75. }
  76. // Store the currently selected channel in the session, so that it can be automatically selected
  77. // if "New conversation" is clicked.
  78. if (!empty($currentChannels)) ET::$session->store("searchChannelId", $currentChannels[0]);
  79. // Get the search string request value.
  80. $searchString = R("search");
  81. // Last, but definitely not least... perform the search!
  82. $search = ET::searchModel();
  83. $conversationIDs = $search->getConversationIDs($channelIds, $searchString, count($currentChannels));
  84. $results = $search->getResults($conversationIDs);
  85. // Were there any errors? Show them as messages.
  86. if ($search->errorCount()) {
  87. $this->messages($search->errors(), "warning dismissable");
  88. }
  89. // Add fulltext keywords to be highlighted. Make sure we keep ones "in quotes" together.
  90. else {
  91. $words = array();
  92. foreach ($search->fulltext as $term) {
  93. if (preg_match_all('/"(.+?)"/', $term, $matches)) {
  94. $words[] = $matches[1];
  95. $term = preg_replace('/".+?"/', '', $term);
  96. }
  97. $words = array_unique(array_merge($words, explode(" ", $term)));
  98. }
  99. ET::$session->store("highlight", $words);
  100. }
  101. // Pass on a bunch of data to the view.
  102. $this->data("results", $results);
  103. $this->data("showViewMoreLink", $search->areMoreResults());
  104. $this->data("channelPath", $path);
  105. $this->data("channelTabs", $channels);
  106. $this->data("currentChannels", $currentChannels);
  107. $this->data("channelInfo", $channelInfo);
  108. $this->data("channelSlug", $channelSlug ? $channelSlug : "all");
  109. $this->data("searchString", $searchString);
  110. $this->data("fulltextString", implode(" ", $search->fulltext));
  111. $this->data("gambits", $gambits);
  112. // Construct a canonical URL and add to the breadcrumb stack.
  113. $slugs = array();
  114. foreach ($currentChannels as $channel) $slugs[] = $channelInfo[$channel]["slug"];
  115. $url = "conversations/".urlencode(($k = implode(" ", $slugs)) ? $k : "all").($searchString ? "?search=".urlencode($searchString) : "");
  116. $this->pushNavigation("conversations", "search", URL($url));
  117. $this->canonicalURL = URL($url, true);
  118. // If we're loading the page in full...
  119. if ($this->responseType === RESPONSE_TYPE_DEFAULT) {
  120. // Update the user's last action.
  121. ET::memberModel()->updateLastAction("search");
  122. // Add a link to the RSS feed in the bar.
  123. // $this->addToMenu("meta", "feed", "<a href='".URL(str_replace("conversations/", "conversations/index.atom/", $url))."' id='feed'>".T("Feed")."</a>");
  124. // Construct a list of keywords to use in the meta tags.
  125. $keywords = array();
  126. foreach ($channelInfo as $c) {
  127. if ($c["depth"] == 0) $keywords[] = strtolower($c["title"]);
  128. }
  129. // Add meta tags to the header.
  130. $this->addToHead("<meta name='keywords' content='".sanitizeHTML(($k = C("esoTalk.meta.keywords")) ? $k : implode(",", $keywords))."'>");
  131. list($lastKeyword) = array_splice($keywords, count($keywords) - 1, 1);
  132. $this->addToHead("<meta name='description' content='".sanitizeHTML(($d = C("esoTalk.meta.description")) ? $d
  133. : sprintf(T("forumDescription"), C("esoTalk.forumTitle"), implode(", ", $keywords), $lastKeyword))."'>");
  134. // If this is not technically the homepage (if it's a search page) the we don't want it to be indexed.
  135. if ($searchString) $this->addToHead("<meta name='robots' content='noindex, noarchive'>");
  136. // Add JavaScript language definitions and variables.
  137. $this->addJSLanguage("Starred", "Unstarred", "gambit.member", "gambit.more results", "Filter conversations", "Jump to last");
  138. $this->addJSVar("searchUpdateInterval", C("esoTalk.search.updateInterval"));
  139. $this->addJSVar("currentSearch", $searchString);
  140. $this->addJSVar("currentChannels", $currentChannels);
  141. $this->addJSFile("js/lib/jquery.cookie.js");
  142. $this->addJSFile("js/autocomplete.js");
  143. $this->addJSFile("js/search.js");
  144. // Add an array of channels in the form slug => id for the JavaScript to use.
  145. $channels = array();
  146. foreach ($channelInfo as $id => $c) $channels[$id] = $c["slug"];
  147. $this->addJSVar("channels", $channels);
  148. // Get a bunch of statistics...
  149. $queries = array(
  150. "post" => ET::SQL()->select("COUNT(*)")->from("post")->get(),
  151. "conversation" => ET::SQL()->select("COUNT(*)")->from("conversation")->get(),
  152. "member" => ET::SQL()->select("COUNT(*)")->from("member")->get()
  153. );
  154. $sql = ET::SQL();
  155. foreach ($queries as $k => $query) $sql->select("($query) AS $k");
  156. $stats = $sql->exec()->firstRow();
  157. // ...and show them in the footer.
  158. foreach ($stats as $k => $v) {
  159. $stat = Ts("statistic.$k", "statistic.$k.plural", number_format($v));
  160. if ($k == "member" and (C("esoTalk.members.visibleToGuests") or ET::$session->user)) $stat = "<a href='".URL("members")."'>$stat</a>";
  161. $this->addToMenu("statistics", "statistic-$k", $stat, array("before" => "statistic-online"));
  162. }
  163. $this->render("conversations/index");
  164. }
  165. // For a view, just render the results.
  166. elseif ($this->responseType === RESPONSE_TYPE_VIEW) {
  167. $this->render("conversations/results");
  168. }
  169. // For ajax, render the results, and also pass along the channels view.
  170. elseif ($this->responseType === RESPONSE_TYPE_AJAX) {
  171. $this->json("channels", $this->getViewContents("channels/tabs", $this->data));
  172. $this->render("conversations/results");
  173. }
  174. // For json, output the results as a json object.
  175. elseif ($this->responseType === RESPONSE_TYPE_JSON) {
  176. $this->json("results", $results);
  177. $this->render();
  178. }
  179. }
  180. /**
  181. * Given the channel slug from a request, work out which channels are selected, whether or not to include
  182. * descendant channels in the results, and construct a full list of channel IDs to consider when getting the
  183. * list a conversations.
  184. *
  185. * @param string $channelSlug The channel slug from the request.
  186. * @return array An array containing:
  187. * 0 => a full list of channel information.
  188. * 1 => the list of currently selected channel IDs.
  189. * 2 => the full list of channel IDs to consider (including descendant channels of selected channels.)
  190. * 3 => whether or not descendant channels are being included.
  191. */
  192. protected function getSelectedChannels($channelSlug = "")
  193. {
  194. // Get a list of all viewable channels.
  195. $channelInfo = ET::channelModel()->get();
  196. // Get a list of the currently selected channels.
  197. $currentChannels = array();
  198. $includeDescendants = true;
  199. if (!empty($channelSlug)) {
  200. $channels = explode(" ", $channelSlug);
  201. // If the first channel is empty (ie. the URL is conversations/+channel-slug), set a flag
  202. // to turn off the inclusion of descendant channels when considering conversations.
  203. if ($channels[0] == "") {
  204. $includeDescendants = false;
  205. array_shift($channels);
  206. }
  207. // Go through the channels and add their IDs to the list of current channels.
  208. foreach ($channels as $channel) {
  209. foreach ($channelInfo as $id => $c) {
  210. if ($c["slug"] == $channel) {
  211. $currentChannels[] = $id;
  212. break;
  213. }
  214. }
  215. }
  216. }
  217. // Get an array of channel IDs to consider when getting the list of conversations.
  218. // If we're not including descendants, this is the same as the list of current channels.
  219. if (!$includeDescendants) {
  220. $channelIds = $currentChannels;
  221. }
  222. // Otherwise, loop through all the channels and add IDs of descendants. Make sure we don't include
  223. // any channels which the user has unsubscribed to.
  224. else {
  225. $channelIds = array();
  226. foreach ($currentChannels as $id) {
  227. $channelIds[] = $id;
  228. $rootUnsubscribed = !empty($channelInfo[$id]["unsubscribed"]);
  229. foreach ($channelInfo as $channel) {
  230. if ($channel["lft"] > $channelInfo[$id]["lft"] and $channel["rgt"] < $channelInfo[$id]["rgt"] and (empty($channel["unsubscribed"]) or $rootUnsubscribed))
  231. $channelIds[] = $channel["channelId"];
  232. }
  233. }
  234. }
  235. // If by now we don't have any channel IDs, we must be viewing "all channels." In this case,
  236. // add all the channels.
  237. if (empty($channelIds)) {
  238. foreach ($channelInfo as $id => $channel) {
  239. if (empty($channel["unsubscribed"])) $channelIds[] = $id;
  240. }
  241. }
  242. return array($channelInfo, $currentChannels, $channelIds, $includeDescendants);
  243. }
  244. /**
  245. * Mark all conversations as read and return to the index page.
  246. *
  247. * @return void
  248. */
  249. public function markAllAsRead()
  250. {
  251. // Update the user's preferences.
  252. ET::$session->setPreferences(array("markedAllConversationsAsRead" => time()));
  253. // For a normal response, redirect to the conversations page.
  254. if ($this->responseType === RESPONSE_TYPE_DEFAULT) $this->redirect(URL("conversations"));
  255. // For an ajax response, just pretend this is a normal search response.
  256. $this->index();
  257. }
  258. /**
  259. * Return updated HTML for each row in the conversations table, and indicate if there are new results for the
  260. * specified channel and search query.
  261. *
  262. * @param string $channelSlug The channel slug.
  263. * @param string $query The search query.
  264. * @return void
  265. */
  266. public function update($channelSlug = "", $query = "")
  267. {
  268. // This must be done as an AJAX request.
  269. $this->responseType = RESPONSE_TYPE_AJAX;
  270. list($channelInfo, $currentChannels, $channelIds, $includeDescendants) = $this->getSelectedChannels($channelSlug);
  271. // Work out which conversations we need to get details for (according to the input value.)
  272. $conversationIds = explode(",", R("conversationIds"));
  273. // Make sure they are all integers.
  274. foreach ($conversationIds as $k => $v) {
  275. if (!($conversationIds[$k] = (int)$v)) unset($conversationIds[$k]);
  276. }
  277. if (!count($conversationIds)) return;
  278. $conversationIds = array_slice((array)$conversationIds, 0, 20);
  279. // Get the full result data for these conversations, and construct an array of rendered conversation rows.
  280. $results = ET::searchModel()->getResults($conversationIds, true);
  281. $rows = array();
  282. foreach ($results as $conversation) {
  283. $rows[$conversation["conversationId"]] = $this->getViewContents("conversations/conversation", array("conversation" => $conversation, "channelInfo" => $channelInfo));
  284. }
  285. // Add that to the response.
  286. $this->json("conversations", $rows);
  287. // Now we need to work out if there are any new results for this channel/search query.
  288. // If the "random" gambit is in the search string, then don't go any further (because the results will
  289. // obviously differ!)
  290. $terms = $query ? explode("+", strtolower(str_replace("-", "+!", trim($query, " +-")))) : array();
  291. foreach ($terms as $v) {
  292. if (trim($v) == T("gambit.random")) return;
  293. }
  294. // Get a list of conversation IDs for the channel/query.
  295. $newConversationIds = ET::searchModel()->getConversationIDs($channelIds, $query, count($currentChannels));
  296. $newConversationIds = array_slice((array)$newConversationIds, 0, 20);
  297. // Get the difference of the two sets of conversationId's.
  298. $diff = array_diff((array)$newConversationIds, (array)$conversationIds);
  299. if (count($diff)) $this->message(sprintf(T("message.newSearchResults"), "javascript:ETSearch.showNewActivity();void(0)"), array("id" => "newSearchResults"));
  300. $this->render();
  301. }
  302. }