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