diff --git a/README.md b/README.md index f583d4a..1d15a95 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # phpBB---PM-Name-Suggestions A phpBB extension that suggests usernames when selecting a recipient of a private message. + +### How to install +Copy the /pcgf/ folder into your phpBB extension folder /ext/ \ No newline at end of file diff --git a/pcgf/pmnamesuggestions/composer.json b/pcgf/pmnamesuggestions/composer.json new file mode 100644 index 0000000..a7709ae --- /dev/null +++ b/pcgf/pmnamesuggestions/composer.json @@ -0,0 +1,32 @@ +{ + "name": "pcgf/pmnamesuggestions", + "type": "phpbb-extension", + "description": "An extension that suggests usernames when selecting a recipient of a private message", + "homepage": "https://github.com/MarkusWME/phpBB---PM-Name-Suggestions", + "version": "1.0.0", + "time": "2016-04-18", + "keywords": ["pm", "private message", "recipient", "username", "suggestion"], + "license": "GPL-2.0", + "authors": [ + { + "name": "MarkusWME", + "homepage": "https://github.com/MarkusWME", + "email": "markuswme@pcgamingfreaks.at", + "role": "Lead Developer" + } + ], + "require": { + "php": ">=5.3.3" + }, + "extra": { + "display-name": "PM Name Suggestions", + "soft-require": { + "phpbb/phpbb": ">=3.1.*" + }, + "version-check": { + "host": "api.pcgf.at", + "directory": "/phpbb", + "filename": "pmnamesuggestions.json" + } + } +} \ No newline at end of file diff --git a/pcgf/pmnamesuggestions/config/routing.yml b/pcgf/pmnamesuggestions/config/routing.yml new file mode 100644 index 0000000..a1e1fa2 --- /dev/null +++ b/pcgf/pmnamesuggestions/config/routing.yml @@ -0,0 +1,3 @@ +pcgf_pmnamesuggestions_controller: + path: /pcgf/pmnamesuggestions + defaults: { _controller: pcgf.pmnamesuggestions.controller:getNameSuggestions } \ No newline at end of file diff --git a/pcgf/pmnamesuggestions/config/services.yml b/pcgf/pmnamesuggestions/config/services.yml new file mode 100644 index 0000000..8c939fc --- /dev/null +++ b/pcgf/pmnamesuggestions/config/services.yml @@ -0,0 +1,14 @@ +services: + pcgf.pmnamesuggestions.listener: + class: pcgf\pmnamesuggestions\event\listener + arguments: + - @template + tags: + - { name: event.listener } + pcgf.pmnamesuggestions.controller: + class: pcgf\pmnamesuggestions\controller\controller + arguments: + - @request + - @dbal.conn + - @auth + - @user \ No newline at end of file diff --git a/pcgf/pmnamesuggestions/controller/controller.php b/pcgf/pmnamesuggestions/controller/controller.php new file mode 100644 index 0000000..dbee55b --- /dev/null +++ b/pcgf/pmnamesuggestions/controller/controller.php @@ -0,0 +1,115 @@ + + * @copyright 2016 MarkusWME + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 + */ + +namespace pcgf\pmnamesuggestions\controller; + +use Symfony\Component\HttpFoundation\Response; + +/** @version 1.0.0 */ +class controller +{ + /** @const Max user count */ + const MAX_USER_COUNT = 5; + + /** @var \phpbb\request\request $request Request object */ + protected $request; + + /** @var \phpbb\db\driver\factory $db Database object */ + protected $db; + + /** @var \phpbb\auth\auth $auth Authenticator object */ + protected $auth; + + /** @var \phpbb\user $user User object */ + protected $user; + + /** + * Constructor + * + * @access public + * @since 1.0.0 + * @param \phpbb\request\request\ $request Request object + * @param \phpbb\db\driver\factory $db Database object + * @param \phpbb\auth\auth $auth Authenticator object + * @param \phpbb\user $user User object + * @return \pcgf\pmnamesuggestions\controller\controller The controller object of the extension + */ + public function __construct(\phpbb\request\request $request, \phpbb\db\driver\factory $db, \phpbb\auth\auth $auth, \phpbb\user $user) + { + $this->request = $request; + $this->db = $db; + $this->auth = $auth; + $this->user = $user; + } + + /** + * Function to get names matching a given username + * + * @access public + * @since 1.0.0 + * @return null + */ + public function getNameSuggestions() + { + $response = new Response(); + $response->headers->set('Content-Type', 'text/html'); + $response->setContent(''); + // Only allow JSON requests + if ($this->request->is_ajax()) + { + // Search if a name is given + $search = utf8_strtolower($this->request->variable('search', '')); + if (strlen($search) > 0) + { + $users = ''; + $user_count = 0; + $search = $this->db->sql_escape($search); + $query = 'SELECT * + FROM ' . USERS_TABLE . ' + WHERE ' . $this->db->sql_in_set('user_type', array(USER_NORMAL, USER_FOUNDER)) . ' + AND LOWER(username) LIKE "%' . $search . '%" + ORDER BY CASE WHEN LOWER(username) LIKE "' . $search . '%" THEN 0 ELSE 1 END, username'; + $result = $this->db->sql_query($query); + $phpbb_root_path = defined('PHPBB_ROOT_PATH') ? PHPBB_ROOT_PATH : './'; + $default_avatar_url = $phpbb_root_path . '/styles/' . $this->user->style['style_path'] . '/theme/images/no_avatar.gif'; + if (!file_exists($default_avatar_url)) + { + $default_avatar_url = $phpbb_root_path . '/ext/pcgf/pmnamesuggestions/styles/all/theme/images/no_avatar.gif'; + } + // Get all users with pm read permission + while ($user = $this->db->sql_fetchrow($result)) + { + $this->auth->acl($user); + if ($this->auth->acl_get('u_readpm') > 0) + { + // Add the user to the user list + $avatar_image = phpbb_get_user_avatar($user); + if ($avatar_image == '') + { + $avatar_image = ''; + } + $users .= '' . $avatar_image . $user['username'] . ''; + $user_count++; + if ($user_count == self::MAX_USER_COUNT) + { + break; + } + } + } + $this->db->sql_freeresult($result); + $response->setStatusCode(200); + $response->setContent(''); + } + } + else + { + $response->setStatusCode(400); + } + return $response; + } +} diff --git a/pcgf/pmnamesuggestions/event/listener.php b/pcgf/pmnamesuggestions/event/listener.php new file mode 100644 index 0000000..9ad53ea --- /dev/null +++ b/pcgf/pmnamesuggestions/event/listener.php @@ -0,0 +1,57 @@ + + * @copyright 2016 MarkusWME + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 + */ + +namespace pcgf\pmnamesuggestions\event; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** @version 1.0.0 */ +class listener implements EventSubscriberInterface +{ + /** @var \phpbb\template\template $template Template object */ + protected $template; + + /** + * Constructor + * + * @access public + * @since 1.0.0 + * @param \phpbb\template\template $template Template object + * @return \pcgf\pmnamesuggestions\event\listener The listener object of the extension + */ + public function __construct(\phpbb\template\template $template) + { + $this->template = $template; + } + + /** + * Function that returns the subscribed events + * + * @access public + * @since 1.0.0 + * @return mixed[] The subscribed event list + */ + static public function getSubscribedEvents() + { + return array( + 'core.ucp_pm_compose_modify_data' => 'add_pmnamesuggestion_css', + ); + } + + /** + * Function that set's the template variable to load the css file when writing a pm + * + * @access public + * @since 1.0.0 + * @return null + */ + public function add_pmnamesuggestion_css($event) + { + $this->template->assign_var('PM_NAME_SUGGESTIONS', true); + } +} diff --git a/pcgf/pmnamesuggestions/styles/Mobbern3.1/theme/namesuggestions.css b/pcgf/pmnamesuggestions/styles/Mobbern3.1/theme/namesuggestions.css new file mode 100644 index 0000000..3949b6e --- /dev/null +++ b/pcgf/pmnamesuggestions/styles/Mobbern3.1/theme/namesuggestions.css @@ -0,0 +1,40 @@ +#pcgf_pmnamesuggestionlist.pcgf-hidden { + display: none; +} + +#pcgf_pmnamesuggestionlist { + background-color: rgb(255, 255, 255); + border: 1px solid rgb(160, 160, 160); + margin-top: 5px; + position: absolute; + width: auto; + z-index: 10; +} + +#pcgf_pmnamesuggestionlist > ul { + display: inline-block; + list-style: none; + margin-bottom: 0px; +} + +#pcgf_pmnamesuggestionlist > ul > li { + cursor: pointer; + padding: 10px; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +#pcgf_pmnamesuggestionlist > ul > li.selected { + background-color: rgb(200, 200, 200) !important; +} + +#pcgf_pmnamesuggestionlist > ul > li > img { + display: inline-block; + height: 20px; + margin-right: 10px; + vertical-align: middle; + width: 20px; +} \ No newline at end of file diff --git a/pcgf/pmnamesuggestions/styles/all/template/event/overall_header_head_append.html b/pcgf/pmnamesuggestions/styles/all/template/event/overall_header_head_append.html new file mode 100644 index 0000000..be6b6b9 --- /dev/null +++ b/pcgf/pmnamesuggestions/styles/all/template/event/overall_header_head_append.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/pcgf/pmnamesuggestions/styles/all/template/event/posting_pm_header_find_username_after.html b/pcgf/pmnamesuggestions/styles/all/template/event/posting_pm_header_find_username_after.html new file mode 100644 index 0000000..1721f67 --- /dev/null +++ b/pcgf/pmnamesuggestions/styles/all/template/event/posting_pm_header_find_username_after.html @@ -0,0 +1,2 @@ +
+ \ No newline at end of file diff --git a/pcgf/pmnamesuggestions/styles/all/template/js/namesuggestions.js b/pcgf/pmnamesuggestions/styles/all/template/js/namesuggestions.js new file mode 100644 index 0000000..3faa1d5 --- /dev/null +++ b/pcgf/pmnamesuggestions/styles/all/template/js/namesuggestions.js @@ -0,0 +1,188 @@ +var pcgfUserList = $('#username_list'); +var pcgfSuggestionList = $('#pcgf_pmnamesuggestionlist'); +var pcgfLastSearchValue = ''; +var pcgfResultCount = 0; +var pcgfLastSelectedIndex = -1; +var pcgfKeyCatched = 0; + +function hideSuggestions() { + // Hide the suggestion list + pcgfSuggestionList.css('display', 'none'); + pcgfSuggestionList.find('ul > li:nth-child(' + pcgfLastSelectedIndex + ')').removeClass('selected'); + pcgfLastSelectedIndex = -1; +} + +function setSuggestionPosition() { + // Position the suggestion list under the current line of the user list + var userListPosition = pcgfUserList.position(); + var currentLine = pcgfUserList.val().substr(0, pcgfUserList.prop('selectionStart')).split('\n').length; + pcgfSuggestionList.css({display: 'inline-block', left: userListPosition.left, top: userListPosition.top + (currentLine * parseInt(pcgfUserList.css('line-height'))) + 5}); +} + +function setPMName(name) { + // Get the name without the image + name = name.substr(name.indexOf('>') + 1); + // Replace the current selected line with the new suggested name + var currentPosition = pcgfUserList.prop('selectionStart'); + var searchValue = pcgfUserList.val(); + var startIndex = searchValue.lastIndexOf('\n', currentPosition - 1) + 1; + var endIndex = searchValue.indexOf('\n', currentPosition); + if (endIndex < 0) { + endIndex = searchValue.length; + } + pcgfUserList.val(searchValue.substr(0, startIndex) + name + '\n' + searchValue.substr(endIndex)); + pcgfUserList.prop('selectionStart', startIndex + name.length + 1); + pcgfUserList.prop('selectionEnd', startIndex + name.length + 1); + hideSuggestions(); +} + +$(window).resize(function(e) { + // Refresh the position of the suggestion list when the screen resizes + setSuggestionPosition(); +}); + +pcgfUserList.on('click', function() { + // Refresh the list when something has been clicked inside the textarea + pcgfUserList.trigger('keyup'); +}); + +pcgfUserList.on('focusin', function() { + // Show the suggestion list when the textarea get's focused + pcgfUserList.trigger('keyup'); +}); + +pcgfUserList.on('focusout', function(e) { + // Hide the suggestion list when nothing is selected + if (pcgfLastSelectedIndex > 0) { + e.preventDefault(); + e.stopPropagation(); + return; + } + hideSuggestions(); +}); + +pcgfUserList.on('keydown', function(e) { + if (pcgfSuggestionList.css('display') !== 'none') + { + pcgfKeyCatched++; + if (e.which === 13) { + // Enter selects the current name and replaces the current line of the textarea + pcgfSuggestionList.trigger('click'); + pcgfKeyCatched = 13; + } else if (e.which === 27) { + // Escape closes the current suggestion list + hideSuggestions(); + } else if (e.which === 38) { + // Up arrow selects the previous entry of the list + if (pcgfKeyCatched % 3 === 1) { + if (pcgfLastSelectedIndex <= 1) { + pcgfLastSelectedIndex = pcgfResultCount; + pcgfSuggestionList.find('ul > li:nth-child(' + pcgfResultCount + ')').addClass('selected'); + if (pcgfResultCount > 1) { + pcgfSuggestionList.find('ul > li:nth-child(1)').removeClass('selected'); + } + } else { + pcgfSuggestionList.find('ul > li:nth-child(' + pcgfLastSelectedIndex + ')').removeClass('selected'); + pcgfLastSelectedIndex--; + pcgfSuggestionList.find('ul > li:nth-child(' + pcgfLastSelectedIndex + ')').addClass('selected'); + } + } + } else if (e.which === 40) { + // Down arrow selects the next entry of the list + if (pcgfKeyCatched % 3 === 1) { + if (pcgfLastSelectedIndex < 0 || pcgfLastSelectedIndex >= pcgfResultCount) { + pcgfLastSelectedIndex = 1; + pcgfSuggestionList.find('ul > li:nth-child(1)').addClass('selected'); + if (pcgfResultCount > 1) { + pcgfSuggestionList.find('ul > li:nth-child(' + pcgfResultCount + ')').removeClass('selected'); + } + } else { + pcgfSuggestionList.find('ul > li:nth-child(' + pcgfLastSelectedIndex + ')').removeClass('selected'); + pcgfLastSelectedIndex++; + pcgfSuggestionList.find('ul > li:nth-child(' + pcgfLastSelectedIndex + ')').addClass('selected'); + } + } + } else { + pcgfKeyCatched = 0; + } + if (pcgfKeyCatched > 0) { + e.preventDefault(); + e.stopPropagation(); + return; + } + } +}); + +pcgfUserList.on('keyup', function(e) { + if (pcgfKeyCatched > 0) { + pcgfKeyCatched = 0; + e.preventDefault(); + e.stopPropagation(); + return; + } + // Any other key will lead to a refresh of the list + var currentPosition = pcgfUserList.prop('selectionStart'); + var searchValue = pcgfUserList.val(); + var startIndex = searchValue.lastIndexOf('\n', currentPosition - 1) + 1; + var endIndex = searchValue.indexOf('\n', currentPosition); + if (endIndex < 0) { + endIndex = searchValue.length; + } + hideSuggestions(); + searchValue = searchValue.substr(startIndex, endIndex - startIndex); + if (searchValue !== pcgfLastSearchValue) { + pcgfLastSearchValue = searchValue; + $.ajax({url: './app.php/pcgf/pmnamesuggestions', type: 'POST', data: {search: searchValue}, success: function(result) { + if (searchValue === pcgfLastSearchValue) { + if (result === '' || result === '') { + // Make the suggestion list invisible if no match could be found + pcgfLastSearchValue = ''; + } else { + // If name is already entered don't show it + var singleMatch = null; + var matches = []; + var regex = /(.*?)<\/li>/gi; + while ((singleMatch = regex.exec(result))) { + matches.push(singleMatch[1]); + } + if (matches.length === 1 && matches[0] === searchValue) { + pcgfLastSearchValue = ''; + return; + } + // Show the result list and refresh it's position + pcgfSuggestionList.html(result); + setSuggestionPosition(); + pcgfResultCount = pcgfSuggestionList.find('ul > li').length; + } + } + }}); + } else if (pcgfLastSearchValue !== '' && pcgfSuggestionList.html() !== '') { + setSuggestionPosition(); + } +}); + +pcgfSuggestionList.on('mousemove', 'ul > li', function() { + // Select the list element where the cursor is above it + if (pcgfLastSelectedIndex > 0) { + pcgfSuggestionList.find('ul > li:nth-child(' + pcgfLastSelectedIndex + ')').removeClass('selected'); + } + pcgfLastSelectedIndex = $(this).index() + 1; + pcgfSuggestionList.find('ul > li:nth-child(' + pcgfLastSelectedIndex + ')').addClass('selected'); + +}); + +pcgfSuggestionList.on('mouseleave', function() { + // Unselect the last selection of the suggestion + if (pcgfLastSelectedIndex > 0) { + pcgfSuggestionList.find('ul > li:nth-child(' + pcgfLastSelectedIndex + ')').removeClass('selected'); + } + pcgfLastSelectedIndex = -1; +}); + +pcgfSuggestionList.on('click', function() { + // Select the name + if (pcgfLastSelectedIndex > 0) { + setPMName(pcgfSuggestionList.find('ul > li:nth-child(' + pcgfLastSelectedIndex + ')').html()); + pcgfUserList.focus(); + } +}); \ No newline at end of file diff --git a/pcgf/pmnamesuggestions/styles/all/theme/images/no-avatar.gif b/pcgf/pmnamesuggestions/styles/all/theme/images/no-avatar.gif new file mode 100644 index 0000000..a04ace0 Binary files /dev/null and b/pcgf/pmnamesuggestions/styles/all/theme/images/no-avatar.gif differ diff --git a/pcgf/pmnamesuggestions/styles/all/theme/namesuggestions.css b/pcgf/pmnamesuggestions/styles/all/theme/namesuggestions.css new file mode 100644 index 0000000..c2c89d4 --- /dev/null +++ b/pcgf/pmnamesuggestions/styles/all/theme/namesuggestions.css @@ -0,0 +1,37 @@ +#pcgf_pmnamesuggestionlist.hidden { + display: none; +} + +#pcgf_pmnamesuggestionlist { + background-color: rgb(255, 255, 255); + border: 1px solid rgb(50, 50, 50); + position: absolute; + width: auto; +} + +#pcgf_pmnamesuggestionlist > ul { + display: inline-block; + list-style: none; +} + +#pcgf_pmnamesuggestionlist > ul > li { + cursor: pointer; + padding: 10px; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +#pcgf_pmnamesuggestionlist > ul > li.selected { + background-color: rgb(180, 180, 180) !important; +} + +#pcgf_pmnamesuggestionlist > ul > li > img { + display: inline-block; + height: 20px; + margin-right: 10px; + vertical-align: middle; + width: 20px; +} \ No newline at end of file