--- acp/main_info.php @@ -1,9 +1,9 @@ '\canidev\reactions\acp\main_module', 'title' => 'ACP_REACTIONS', - 'version' => '1.0.3', + 'version' => '1.0.4', 'modes' => [ 'config' => ['title' => 'ACP_REACTIONS_CONFIG', 'auth' => 'ext_canidev/reactions && acl_a_board', 'cat' => ['ACP_REACTIONS']], 'manage' => ['title' => 'ACP_REACTIONS_MANAGE', 'auth' => 'ext_canidev/reactions && acl_a_board', 'cat' => ['ACP_REACTIONS']], --- acp/main_module.php @@ -1,9 +1,9 @@ get('canidev.reactions.controller.admin.' . $mode); $controller --- adm/style/acp_reactions_manage.html @@ -23,11 +23,11 @@ {{ lang('REACTION_ADD') }} -
+

{{ lang('NO_REACTIONS') }}

-
+
{% for reaction in reactions %}
@@ -40,6 +40,7 @@
+ {{ reaction.S_HIDDEN_FIELDS }} --- adm/style/event/acp_overall_footer_after.html @@ -1,3 +1,3 @@ -{% if REACTIONS_IN_ADMIN and CANIDEV_CORE_STARTED %} +{% if IN_EXT_REACTIONS and CANIDEV_CORE_STARTED %} {% include '@canidev_core/event/acp_overall_footer_after.html' %} {% endif %} \ No hay ningún carácter de nueva línea al final del fichero --- adm/style/event/acp_overall_header_head_append.html @@ -1,3 +1,3 @@ -{% if REACTIONS_IN_ADMIN and CANIDEV_CORE_STARTED %} +{% if IN_EXT_REACTIONS and CANIDEV_CORE_STARTED %} {% include '@canidev_core/event/overall_header_head_append.html' %} {% endif %} \ No hay ningún carácter de nueva línea al final del fichero --- adm/style/reactions-acp.css @@ -1,7 +1,7 @@ /* cBB Reactions StyleSheet -------------------------------------------------------------- Style: ACP - Copyright (c) 2024 CaniDev ( https://www.canidev.com ) + Copyright (c) 2025 CaniDev ( https://www.canidev.com ) -------------------------------------------------------------- */ @@ -26,6 +26,10 @@ white-space: nowrap; } +.acp-reactions-table .column-actions a.active { + color: #BC2A4D; +} + .reaction-score-selector { list-style-type: none; } --- composer.json @@ -3,7 +3,8 @@ "type": "phpbb-extension", "description": "", "homepage": "https://www.canidev.com", - "version": "1.0.3", + "version": "1.0.4", + "time": "2025-04-01", "keywords": ["phpbb", "extension", "canidev", "reactions"], "license": "CreativeCommons Attribution-NonCommercial v4.0", "authors": [ --- config/services.yml @@ -36,7 +36,9 @@ canidev.reactions.manager: class: canidev\reactions\libraries\reaction_manager arguments: + - '@auth' - '@cache.driver' + - '@config' - '@service_container' - '@dbal.conn' - '@dispatcher' --- controller/admin_config.php @@ -1,9 +1,9 @@ '', 'reactions_allow_myself' => ['lang' => 'REACTIONS_ALLOW_MYSELF', 'validate' => 'bool', 'type' => 'radio:yes_no'], 'reactions_allow_change' => ['lang' => 'REACTIONS_ALLOW_CHANGE', 'validate' => 'bool', 'type' => 'radio:yes_no'], + 'reactions_anonymous' => ['lang' => 'REACTIONS_ANONYMOUS', 'validate' => 'bool', 'type' => 'radio:yes_no'], 'reactions_force_reply' => ['lang' => 'REACTIONS_FORCE_REPLY', 'validate' => 'bool', 'type' => 'radio:yes_no'], 'reactions_force_attach' => ['lang' => 'REACTIONS_FORCE_ATTACH', 'validate' => 'bool', 'type' => 'radio:yes_no'], 'reactions_score_on_profile' => ['lang' => 'REACTIONS_SCORE_ON_PROFILE', 'validate' => 'bool', 'type' => 'radio:yes_no'], - 'reactions_zones' => ['lang' => 'REACTIONS_ZONES', 'type' => 'custom', 'function' => ['tools', 'make_select'], 'params' => [$zones_options, '{KEY}', '{CONFIG_VALUE}']], - 'reactions_button_position' => ['lang' => 'REACTIONS_BUTTON_POSITION', 'type' => 'custom', 'function' => ['tools', 'make_select'], 'params' => [$position_options, '{KEY}', '{CONFIG_VALUE}']], - 'reactions_list_order' => ['lang' => 'REACTIONS_LIST_ORDER', 'type' => 'custom', 'function' => ['tools', 'make_select'], 'params' => [$order_options, '{KEY}', '{CONFIG_VALUE}']], + 'reactions_zones' => ['lang' => 'REACTIONS_ZONES', 'type' => 'select', 'function' => ['tools', 'make_select'], 'params' => [$zones_options, false, '{CONFIG_VALUE}']], + 'reactions_forums' => ['lang' => 'REACTIONS_FORUMS', 'type' => 'custom', 'function' => ['tools', 'forums_select'], 'params' => ['forums', '{CONFIG_VALUE}', 1]], + 'reactions_button_position' => ['lang' => 'REACTIONS_BUTTON_POSITION', 'type' => 'select', 'function' => ['tools', 'make_select'], 'params' => [$position_options, false, '{CONFIG_VALUE}']], + 'reactions_list_order' => ['lang' => 'REACTIONS_LIST_ORDER', 'type' => 'select', 'function' => ['tools', 'make_select'], 'params' => [$order_options, false, '{CONFIG_VALUE}']], ]; $cfg_array = $this->config; @@ -78,6 +80,7 @@ if($submit) { $cfg_array = $this->request->variable('config', ['' => ''], true); + $cfg_array['reactions_forums'] = implode(',', $this->request->variable('forums', [0])); if($cfg_array['reactions_zones'] == constants::ZONE_ONLY_REPLIES) { @@ -93,7 +96,7 @@ /** * @event reactions.acp_config_before * @var array display_vars Array of config values to display and process - * @var boolean submit Do we display the form or process the submission + * @var bool submit Do we display the form or process the submission * @var array cfg_array Array with data * @var array error Array with data errors * @since 1.0.0 @@ -105,7 +108,7 @@ validate_config_vars($display_vars, $cfg_array, $error); // Do not write values if there is an error - if(sizeof($error)) + if(count($error)) { $submit = false; } @@ -141,11 +144,10 @@ trigger_error($this->language->lang('CONFIG_UPDATED') . adm_back_link($this->form_action)); } - $this->template->assign_vars(array( - 'REACTIONS_IN_ADMIN' => true, - 'S_ERROR' => (sizeof($error)) ? true : false, - 'ERROR_MSG' => implode('
', $error), - )); + $this->template->assign_vars([ + 'S_ERROR' => count($error) > 0, + 'ERROR_MSG' => implode('
', $error), + ]); $this->display_vars($display_vars); } --- controller/admin_manage.php @@ -1,9 +1,9 @@ dispatcher->trigger_event('reactions.acp_manage_edit_before', compact($vars))); // Do not write values if there is an error - if(sizeof($error)) + if(count($error)) { $submit = false; } @@ -177,7 +177,7 @@ $this->template->assign_vars([ 'IN_REACTION' => true, - 'S_ERROR' => sizeof($error) ? implode('
', $error) : '', + 'S_ERROR' => count($error) ? implode('
', $error) : '', 'S_HIDDEN_FIELDS' => build_hidden_fields([ 'action' => $action, 'id' => $reaction_id, @@ -274,6 +274,18 @@ $this->json->send(); break; + + case 'set_default': + if($this->config['reactions_default'] == $reaction_id) + { + $reaction_id = 0; + } + + $this->config->set('reactions_default', $reaction_id); + $this->json->send([ + 'reactionID' => $reaction_id + ]); + break; } $sql = 'SELECT * @@ -302,6 +314,7 @@ $this->template->assign_block_vars('reactions', [ 'ID' => $row['reaction_id'], + 'IS_DEFAULT' => ($row['reaction_id'] == $this->config['reactions_default']), 'IS_ENABLED' => (bool)$row['reaction_enabled'], 'S_HIDDEN_FIELDS' => build_hidden_fields([ 'item[]' => $row['reaction_id'], @@ -310,6 +323,7 @@ 'S_SCORE' => $score_label, 'S_TITLE' => $title, 'S_TITLE_RAW' => $this->language->lang($row['reaction_title']), + 'U_DEFAULT' => $this->form_action . '&action=set_default&id=' . $row['reaction_id'], 'U_DELETE' => $this->form_action . '&action=delete&id=' . $row['reaction_id'], 'U_EDIT' => $this->form_action . '&action=edit&id=' . $row['reaction_id'], 'U_STATUS' => $this->form_action . '&action=change_status&id=' . $row['reaction_id'], @@ -318,8 +332,7 @@ $this->db->sql_freeresult($result); $this->template->assign_vars([ - 'REACTIONS_IN_ADMIN' => true, - 'U_REACTION_ADD' => $this->form_action . '&action=add', + 'U_REACTION_ADD' => $this->form_action . '&action=add', ]); // Assets --- controller/main.php @@ -1,14 +1,15 @@ request->variable('post', 0); $user_id = $this->request->variable('user', 0); $score_amount = 0; + $score_row = []; if(!(($user_id == $this->user->data['user_id'] && $this->auth->acl_get('u_reactions')) || $this->auth->acl_get('m_reactions'))) @@ -116,9 +118,10 @@ // Get post data $sql = 'SELECT p.post_id, p.topic_id, p.poster_id, p.post_text, p.bbcode_uid, p.bbcode_bitfield, - t.topic_first_post_id + t.topic_first_post_id, u.user_reaction_score AS current_score FROM ' . POSTS_TABLE . ' p LEFT JOIN ' . TOPICS_TABLE . ' t ON(t.topic_id = p.topic_id) + LEFT JOIN ' . USERS_TABLE . ' u on(u.user_id = p.poster_id) WHERE p.post_id = ' . $post_id; $result = $this->db->sql_query($sql); $post_data = $this->db->sql_fetchrow($result); @@ -204,10 +207,10 @@ } } - $this->template->assign_vars([ + $score_row += [ 'REACTION_HAS_MINE' => true, 'REACTION_LAUNCHER_ICON' => $reaction->get_launcher_html(), - ]); + ]; // Add the new score to post/topic $score_amount += $reaction->get_score(); @@ -224,9 +227,9 @@ AND user_id = ' . $user_id; $this->db->sql_query($sql); - $this->template->assign_vars([ - 'REACTION_LAUNCHER_ICON' => $this->reactions->get(false)->get_launcher_html(), - ]); + $score_row += [ + 'REACTION_LAUNCHER_ICON' => $this->reactions->get_launcher_html(false), + ]; $this->response->add([ 'reactionID' => $reaction_data['reaction_id'], @@ -238,6 +241,15 @@ $this->notifications->delete_notification('canidev.reactions.notification.post', $post_id, $post_data['topic_id'], $user_id); } + // Update user score + if(!is_null($post_data['current_score'])) + { + $sql = 'UPDATE ' . USERS_TABLE . ' + SET user_reaction_score = user_reaction_score + ' . $score_amount . ' + WHERE user_id = ' . (int)$post_data['poster_id']; + $this->db->sql_query($sql); + } + // Update post score $sql = 'UPDATE ' . POSTS_TABLE . ' SET post_reaction_score = post_reaction_score + ' . $score_amount . ' @@ -249,7 +261,7 @@ { $sql = 'UPDATE ' . TOPICS_TABLE . ' SET topic_reaction_score = topic_reaction_score + ' . $score_amount . ' - WHERE topic_id = ' . $post_data['topic_id']; + WHERE topic_id = ' . (int)$post_data['topic_id']; $this->db->sql_query($sql); } @@ -257,18 +269,22 @@ * @event reactions.add_remove_after * @var string mode add_reaction / remove_reaction * @var int post_id Post ID - * @var int user_id User ID - * @var int reaction_id Reaction ID (only for "add_reaction" mode) + * @var int user_id User ID + * @var int reaction_id Reaction ID (only for "add_reaction" mode) * @since 1.0.0 */ $vars = ['mode', 'post_id', 'user_id', 'reaction_id']; extract($this->dispatcher->trigger_event('reactions.add_remove_after', compact($vars))); - $this->template->assign_vars([ - 'CAN_USE_REACTIONS' => $this->auth->acl_get('u_reactions'), - 'POST_ID' => $post_id, - 'U_REACTION' => $this->reactions->get_user_link($post_id, $user_id), - ]); + $score_row += [ + 'CAN_USE_REACTIONS' => $this->auth->acl_get('u_reactions'), + 'POST_ID' => $post_id, + 'REACTION_LIST_ATTR' => Dom::arrayToAttr([ + 'data-post-id' => $post_id, + 'data-title' => $this->language->lang('REACTIONS'), + ]), + 'U_REACTION' => $this->reactions->get_user_link($post_id, $user_id), + ]; // Put reactions on template foreach($this->reactions->get_all() as $reaction) @@ -284,13 +300,16 @@ if($this->auth->acl_get('u_reactions_view')) { $reaction_list = $this->reactions->get_posts_reactions($post_id); - $this->template->assign_vars($this->reactions->parse_post_score_list($reaction_list[$post_id])); + + $score_row = array_merge($score_row, $this->reactions->parse_post_score_list($reaction_list[$post_id])); } - $this->template->set_filenames(array( + $this->template->assign_block_vars('score_rows', $score_row); + + $this->template->set_filenames([ 'launcher' => '@canidev_reactions/launcher_button.html', 'score_list' => '@canidev_reactions/score_list.html', - )); + ]); $this->response->add([ 'postID' => $post_id, @@ -399,7 +418,7 @@ foreach($tabs as $reaction_id => $data) { - $item_count = sizeof($data['items']); + $item_count = count($data['items']); if(!$item_count) { @@ -418,7 +437,7 @@ } $this->template->set_filenames([ - 'dialog' => '@canidev_reactions/reactions_list.html' + 'dialog' => $this->reactions->can_view_usernames() ? '@canidev_reactions/reactions_list.html' : '@canidev_reactions/reactions_list_simple.html' ]); $this->response->addHtml($this->template->assign_display('dialog')); --- event/listener.php @@ -1,14 +1,15 @@ 'add_permissions', 'core.memberlist_view_profile' => 'memberlist_view_profile', 'core.modify_posting_auth' => 'posting_auth', + 'core.viewtopic_cache_user_data' => 'viewtopic_user_data', 'core.viewforum_modify_topic_ordering' => 'viewforum_gen_sort_selects', // phpBB >= 3.2.5 'core.viewtopic_gen_sort_selects_before' => 'viewtopic_gen_sort_selects', // phpBB >= 3.2.8 'core.viewtopic_modify_post_data' => 'viewtopic_modify_data', @@ -96,9 +99,9 @@ /** * Add Permissions to ACP * - * @param array $event Event data + * @param \phpbb\event\data $event Event data */ - public function add_permissions($event) + public function add_permissions(\phpbb\event\data $event) { $event['permissions'] = array_merge($event['permissions'], [ 'm_reactions' => ['lang' => 'ACL_M_REACTIONS', 'cat' => 'post_actions'], @@ -110,9 +113,9 @@ /** * Delete reaction's data on post delete * - * @param array $event Event data + * @param \phpbb\event\data $event Event data */ - public function delete_posts($event) + public function delete_posts(\phpbb\event\data $event) { $table_ary = $event['table_ary']; $delete_notifications_types = $event['delete_notifications_types']; @@ -127,9 +130,9 @@ /** * Delete reaction's data on user delete * - * @param array $event Event data + * @param \phpbb\event\data $event Event data */ - public function delete_user($event) + public function delete_user(\phpbb\event\data $event) { $post_ids = []; @@ -162,9 +165,9 @@ /** * Load Reactions language * - * @param array $event Event data + * @param \phpbb\event\data $event Event data */ - public function load_language($event) + public function load_language(\phpbb\event\data $event) { $lang_set_ext = $event['lang_set_ext']; $lang_set_ext[] = [ @@ -177,9 +180,9 @@ /** * Load profile stats * - * @param array $event Event data + * @param \phpbb\event\data $event Event data */ - public function memberlist_view_profile($event) + public function memberlist_view_profile(\phpbb\event\data $event) { $user_id = (int)$event['member']['user_id']; @@ -194,19 +197,39 @@ if($this->config['reactions_score_on_profile']) { - $score_ary = $this->reactions->get_users_score([$user_id]); - $this->template->assign_var('REACTIONS_USER_SCORE', $score_ary[$user_id]); + $score = $event['member']['user_reaction_score']; + + /** + * Adjust score count for user. + * Correction of the error with the creation of the index in phpbb posts in large forums. + */ + if(is_null($score)) + { + $score_ary = $this->reactions->get_users_score([$user_id]); + + $score = $score_ary[$user_id]; + + $sql = 'UPDATE ' . USERS_TABLE . ' + SET user_reaction_score = ' . $score . ' + WHERE user_id = ' . $user_id; + $this->db->sql_query($sql); + } + + $this->template->assign_var('REACTIONS_USER_SCORE', $score); } } /** * Determine if user can posting a reply * - * @param array $event Event data + * @param \phpbb\event\data $event Event data */ - public function posting_auth($event) + public function posting_auth(\phpbb\event\data $event) { - if($event['mode'] == 'reply' && $this->config['reactions_force_reply'] && $event['post_data']['topic_poster'] != $this->user->data['user_id']) + if($event['mode'] == 'reply' && + $this->config['reactions_force_reply'] && + $event['post_data']['topic_poster'] != $this->user->data['user_id'] && + $this->is_valid_forum($event['forum_id'])) { $sql = 'SELECT reaction_id FROM ' . $this->reactions_data_table . ' @@ -226,9 +249,9 @@ /** * Add sort option to Viewforum * - * @param array $event Event data + * @param \phpbb\event\data $event Event data */ - public function viewforum_gen_sort_selects($event) + public function viewforum_gen_sort_selects(\phpbb\event\data $event) { $sort_by_text = $event['sort_by_text']; $sort_by_sql = $event['sort_by_sql']; @@ -243,9 +266,9 @@ /** * Add sort option to Viewtopic * - * @param array $event Event data + * @param \phpbb\event\data $event Event data */ - public function viewtopic_gen_sort_selects($event) + public function viewtopic_gen_sort_selects(\phpbb\event\data $event) { $sort_by_text = $event['sort_by_text']; $sort_by_sql = $event['sort_by_sql']; @@ -263,11 +286,11 @@ /** * Assign variables on viewtopic page * - * @param array $event Event data + * @param \phpbb\event\data $event Event data */ - public function viewtopic_modify_data($event) + public function viewtopic_modify_data(\phpbb\event\data $event) { - if(!sizeof($event['post_list'])) + if(!count($event['post_list']) || !$this->is_valid_forum($event['forum_id'])) { return; } @@ -286,34 +309,71 @@ // Load post's reactions $this->cache_ary = $this->reactions->get_posts_reactions($event['post_list']); - // Load user's scores + /** + * Adjust score count for users. + * Correction of the error with the creation of the index in phpbb posts in large forums. + */ if($this->config['reactions_score_on_profile']) { $user_cache = $event['user_cache']; - $score_ary = $this->reactions->get_users_score(array_keys($user_cache)); + $user_ary = []; - foreach($score_ary as $user_id => $user_score) + foreach($user_cache as $user_id => $row) { - $user_cache[$user_id]['reactions_score'] = $user_score; + if(array_key_exists('reactions_score', $row) && is_null($row['reactions_score'])) + { + $user_ary[$user_id] = $user_id; + } } - $event['user_cache'] = $user_cache; + if(count($user_ary)) + { + $score_ary = $this->reactions->get_users_score($user_ary); + $user_ary = []; + + foreach($score_ary as $user_id => $user_score) + { + $user_cache[$user_id]['reactions_score'] = $user_score; + + $user_ary[$user_id] = [ + 'user_reaction_score' => $user_score, + ]; + } + + foreach($user_ary as $user_id => $update_ary) + { + $sql = 'UPDATE ' . USERS_TABLE . ' + SET ' . $this->db->sql_build_array('UPDATE', $update_ary) . ' + WHERE user_id = ' . $user_id; + $this->db->sql_query($sql); + } + + $event['user_cache'] = $user_cache; + } } - $this->template->assign_var('REACTIONS_BTN_POSITION', $this->config['reactions_button_position']); + // Template variables and assets + $service_options = [ + 'allowChange' => (bool)$this->config['reactions_allow_change'], + 'defaultID' => (int)$this->config['reactions_default'], + 'simpleList' => !$this->reactions->can_view_usernames(), + ]; + + $this->template->assign_vars([ + 'REACTIONS_BTN_POSITION' => $this->config['reactions_button_position'], + 'REACTIONS_OPTIONS' => json_encode($service_options), + ]); - // Assets $this->template->append_asset('css', '@canidev_reactions/reactions.css'); - $this->template->append_asset('javascript', 'window.reactionsAllowChange=' . (int)$this->config['reactions_allow_change'] . ';'); $this->template->append_asset('js', '@canidev_reactions/reactions.min.js'); } /** * Add reaction variables to post rows in viewtopic * - * @param array $event Event data + * @param \phpbb\event\data $event Event data */ - public function viewtopic_modify_row($event) + public function viewtopic_modify_row(\phpbb\event\data $event) { $postrow = $event['post_row']; $row = $event['row']; @@ -321,6 +381,11 @@ $post_id = (int)$row['post_id']; $url_params = []; + if(!$this->is_valid_forum($event['row']['forum_id'])) + { + return; + } + $postrow['REACTION_HAS_MINE'] = false; if($this->reactions->get($this->cache_ary[$post_id]['mine'])->is_enabled()) @@ -328,7 +393,7 @@ $postrow['REACTION_HAS_MINE'] = true; } - $postrow['REACTION_LAUNCHER_ICON'] = $this->reactions->get($this->cache_ary[$post_id]['mine'])->get_launcher_html(); + $postrow['REACTION_LAUNCHER_ICON'] = $this->reactions->get_launcher_html($this->cache_ary[$post_id]['mine']); $postrow['CAN_USE_REACTIONS'] = $this->auth->acl_get('u_reactions'); // Current user is the author and can't react to their own post @@ -389,6 +454,11 @@ $postrow['U_REACTION'] = $this->reactions->get_user_link($post_id, $this->user->data['user_id'], $url_params); + $postrow['REACTION_LIST_ATTR'] = Dom::arrayToAttr([ + 'data-post-id' => $post_id, + 'data-title' => $this->language->lang('REACTIONS'), + ]); + if($this->cache_ary[$post_id]['length'] && $this->auth->acl_get('u_reactions_view')) { $postrow = array_merge($postrow, $this->reactions->parse_post_score_list($this->cache_ary[$post_id])); @@ -398,15 +468,46 @@ } /** + * Assign data to user profile in viewtopic + * + * @param \phpbb\event\data $event Event data + */ + public function viewtopic_user_data(\phpbb\event\data $event) + { + $user_cache_data = $event['user_cache_data']; + $user_cache_data['reactions_score'] = $event['row']['user_reaction_score']; + $event['user_cache_data'] = $user_cache_data; + } + + /** * Update reactions username when some username is changed * - * @param array $event Event data + * @param \phpbb\event\data $event Event data */ - public function update_username($event) + public function update_username(\phpbb\event\data $event) { $sql = 'UPDATE ' . $this->reactions_data_table . " SET username = '" . $this->db->sql_escape($event['new_name']) . "' WHERE username = '" . $this->db->sql_escape($event['old_name']) . "'"; $this->db->sql_query($sql); } + + /** + * Check if reactions can be used in selected forum + * + * @param int $forum_id + * @return bool + * @access private + */ + protected function is_valid_forum($forum_id) + { + if(!$this->config['reactions_forums']) + { + return true; + } + + $forum_ary = explode(',', $this->config['reactions_forums']); + + return in_array($forum_id, $forum_ary); + } } --- event/media.php @@ -1,9 +1,9 @@ container->get('notification_manager'); - $phpbb_notifications->enable_notifications('canidev.reactions.notification.post'); - - return 'notifications'; - - default: - // Create upload folder - \canidev\core\tools::mkdir($this->container->getParameter('core.root_path') . 'files/reactions/', true); - - // Run parent enable step method - return parent::enable_step($old_state); + /** @var \phpbb\config\db */ + $config = $this->container->get('config'); + + // Fresh install + if(!$config->offsetExists('reactions_allow_change')) + { + define('REACTIONS_FRESH_INSTALL', true); + } + + // Enable notifications + /** @var \phpbb\notification\manager */ + $phpbb_notifications = $this->container->get('notification_manager'); + $phpbb_notifications->enable_notifications('canidev.reactions.notification.post'); + + // Create upload folder + \canidev\core\tools::mkdir($this->container->getParameter('core.root_path') . 'files/reactions/', true); + + return true; } + + // Run parent enable step method + return parent::enable_step($old_state); } /** --- language/en/acp.php @@ -2,9 +2,9 @@ /** * [English [En]] * @package cBB Reactions - * @version 1.0.3 26/02/2024 + * @version 1.0.4 01/04/2025 * - * @copyright (c) 2024 CaniDev + * @copyright (c) 2025 CaniDev * @license https://creativecommons.org/licenses/by-nc/4.0/ */ @@ -44,12 +44,19 @@ 'REACTIONS_ALLOW_CHANGE_EXPLAIN' => 'Defines whether users will be able to modify their reaction on messages.', 'REACTIONS_ALLOW_MYSELF' => 'Allow self reactions', 'REACTIONS_ALLOW_MYSELF_EXPLAIN' => 'If enabled, users will be able to react to their own messages.', + 'REACTIONS_ANONYMOUS' => 'Anonymous reactions', + 'REACTIONS_ANONYMOUS_EXPLAIN' => 'Enable this option if you want user names not to appear in reaction listings.
+ This option does not affect users with permission to moderate reactions.', 'REACTIONS_BUTTON_POSITION' => 'Button position', 'REACTIONS_BUTTON_POSITION_EXPLAIN' => 'Defines where the button to react will appear in the message.', 'REACTIONS_FORCE_ATTACH' => 'Force reaction to view attachments', 'REACTIONS_FORCE_ATTACH_EXPLAIN' => 'If enabled, users will have to react to a message in order to see its attachments.', 'REACTIONS_FORCE_REPLY' => 'Force reaction to reply', 'REACTIONS_FORCE_REPLY_EXPLAIN' => 'If enabled, users will have to react to the first post in a topic in order to post replies to it.', + 'REACTIONS_FORUMS' => 'Forums where users can react', + 'REACTIONS_FORUMS_EXPLAIN' => 'Defines the forums in which reactions will be displayed.
+ If you don\'t select any, reactions will be displayed on all forums.
+ You can select as many as you like using the Ctrl key on your keyboard.', 'REACTIONS_LIST_ORDER' => 'List order', 'REACTIONS_LIST_ORDER_EXPLAIN' => 'Defines the criteria that will be used to sort the users in the reaction list.', 'REACTIONS_ORDER_TIME' => 'Reaction date', @@ -66,6 +73,7 @@ 'SCORE_CUSTOM' => 'Custom', 'SCORE_CUSTOM_VALUE' => 'Custom (%s)', 'SELECT_COLOUR' => 'Select color', + 'SET_AS_DEFAULT' => 'Set as default', 'scores' => [ 1 => 'Positive (+1)', --- language/en/info_acp_reactions.php @@ -2,9 +2,9 @@ /** * [English [En]] * @package cBB Reactions - * @version 1.0.3 26/02/2024 + * @version 1.0.4 01/04/2025 * - * @copyright (c) 2024 CaniDev + * @copyright (c) 2025 CaniDev * @license https://creativecommons.org/licenses/by-nc/4.0/ */ --- language/en/main.php @@ -2,9 +2,9 @@ /** * [English [En]] * @package cBB Reactions - * @version 1.0.3 26/02/2024 + * @version 1.0.4 01/04/2025 * - * @copyright (c) 2024 CaniDev + * @copyright (c) 2025 CaniDev * @license https://creativecommons.org/licenses/by-nc/4.0/ */ @@ -30,9 +30,17 @@ 'REACTIONS_NOTIFICATION_TYPE_POST' => 'Someone reacted to a message you posted', - 'REACTION_SCORE_LABEL_SIMPLE' => '%1$s', - 'REACTION_SCORE_LABEL_COUNT_ONE' => '%1$s and another user', - 'REACTION_SCORE_LABEL_COUNT_MULTIPLE' => '%1$s and another %2$d users', + 'REACTION_SCORE_LABEL_ANONYMOUS' => [ + 1 => '1 user has reacted', + 2 => '%d users have reacted', + ], + + 'REACTION_SCORE_LABEL_SIMPLE' => '%1$s', + + 'REACTION_SCORE_LABEL_COUNT' => [ + 1 => '%1$s and another user', + 2 => '%1$s and another %2$d users', + ], 'REACTIONS' => 'Reactions', 'REACTIONS_ALL' => 'All', --- language/en/permissions_reactions.php @@ -2,9 +2,9 @@ /** * [English [En]] * @package cBB Reactions - * @version 1.0.3 26/02/2024 + * @version 1.0.4 01/04/2025 * - * @copyright (c) 2024 CaniDev + * @copyright (c) 2025 CaniDev * @license https://creativecommons.org/licenses/by-nc/4.0/ */ --- language/es/acp.php @@ -2,9 +2,9 @@ /** * [Spanish [Es]] * @package cBB Reactions - * @version 1.0.3 26/02/2024 + * @version 1.0.4 01/04/2025 * - * @copyright (c) 2024 CaniDev + * @copyright (c) 2025 CaniDev * @license https://creativecommons.org/licenses/by-nc/4.0/ */ @@ -44,12 +44,19 @@ 'REACTIONS_ALLOW_CHANGE_EXPLAIN' => 'Define si los usuarios podrán modificar su reacción en los mensajes.', 'REACTIONS_ALLOW_MYSELF' => 'Permitir reacciones de uno mismo', 'REACTIONS_ALLOW_MYSELF_EXPLAIN' => 'Si se habilita, los usuarios podrán reaccionar a sus propios mensajes.', + 'REACTIONS_ANONYMOUS' => 'Reacciones anónimas', + 'REACTIONS_ANONYMOUS_EXPLAIN' => 'Habilite esta opción si desea que los nombres de los usuarios no aparezcan en los listados de reacciones.
+ Esta opción no afecta a los usuarios con permiso para moderar las reacciones.', 'REACTIONS_BUTTON_POSITION' => 'Posición del botón', 'REACTIONS_BUTTON_POSITION_EXPLAIN' => 'Define en que lugar del mensaje aparecerá el botón para reaccionar.', 'REACTIONS_FORCE_ATTACH' => 'Forzar reacción para ver adjuntos', 'REACTIONS_FORCE_ATTACH_EXPLAIN' => 'Si se habilita, los usuarios tendrán que reaccionar a un mensaje para poder ver sus archivos adjuntos.', 'REACTIONS_FORCE_REPLY' => 'Forzar reacción para responder', 'REACTIONS_FORCE_REPLY_EXPLAIN' => 'Si se habilita, los usuarios tendrán que reaccionar al primer mensaje de un tema para poder publicar respuestas en el mismo.', + 'REACTIONS_FORUMS' => 'Foros en los que se puede reaccionar', + 'REACTIONS_FORUMS_EXPLAIN' => 'Define los foros en los que se mostrarán las reacciones.
+ Si no selecciona ninguno, las reacciones se mostrarán en todos los foros.
+ Puede seleccionar tantos como quiera usando la tecla Ctrl de su teclado.', 'REACTIONS_LIST_ORDER' => 'Orden de la lista', 'REACTIONS_LIST_ORDER_EXPLAIN' => 'Define el criterio que se usará para ordenar los usuarios en la lista de reacciones.', 'REACTIONS_ORDER_TIME' => 'Fecha de reacción', @@ -66,6 +73,7 @@ 'SCORE_CUSTOM' => 'Personalizado', 'SCORE_CUSTOM_VALUE' => 'Personalizado (%s)', 'SELECT_COLOUR' => 'Seleccionar color', + 'SET_AS_DEFAULT' => 'Definir como predeterminada', 'scores' => [ 1 => 'Positivo (+1)', --- language/es/info_acp_reactions.php @@ -2,9 +2,9 @@ /** * [Spanish [Es]] * @package cBB Reactions - * @version 1.0.3 26/02/2024 + * @version 1.0.4 01/04/2025 * - * @copyright (c) 2024 CaniDev + * @copyright (c) 2025 CaniDev * @license https://creativecommons.org/licenses/by-nc/4.0/ */ --- language/es/main.php @@ -2,9 +2,9 @@ /** * [Spanish [Es]] * @package cBB Reactions - * @version 1.0.3 26/02/2024 + * @version 1.0.4 01/04/2025 * - * @copyright (c) 2024 CaniDev + * @copyright (c) 2025 CaniDev * @license https://creativecommons.org/licenses/by-nc/4.0/ */ @@ -30,9 +30,17 @@ 'REACTIONS_NOTIFICATION_TYPE_POST' => 'Alguien reaccionó a un mensaje que ha publicado', - 'REACTION_SCORE_LABEL_SIMPLE' => '%1$s', - 'REACTION_SCORE_LABEL_COUNT_ONE' => '%1$s y otro usuario', - 'REACTION_SCORE_LABEL_COUNT_MULTIPLE' => '%1$s y otros %2$d usuarios', + 'REACTION_SCORE_LABEL_ANONYMOUS' => [ + 1 => '1 usuario ha reaccionado', + 2 => '%d usuarios han reaccionado', + ], + + 'REACTION_SCORE_LABEL_SIMPLE' => '%1$s', + + 'REACTION_SCORE_LABEL_COUNT' => [ + 1 => '%1$s y otro usuario', + 2 => '%1$s y otros %2$d usuarios', + ], 'REACTIONS' => 'Reacciones', 'REACTIONS_ALL' => 'Todas', --- language/es/permissions_reactions.php @@ -2,9 +2,9 @@ /** * [Spanish [Es]] * @package cBB Reactions - * @version 1.0.3 26/02/2024 + * @version 1.0.4 01/04/2025 * - * @copyright (c) 2024 CaniDev + * @copyright (c) 2025 CaniDev * @license https://creativecommons.org/licenses/by-nc/4.0/ */ --- libraries/constants.php @@ -1,9 +1,9 @@ manager->get_notification_type_id($notification_type_name); - $notification = $this->manager->get_item_type_class($notification_type_name); $item_parent_id = isset($notification_data['item_parent_id']) ? $notification_data['item_parent_id'] : 0; + + /** @var \canidev\reactions\notification\post */ + $notification = $this->manager->get_item_type_class($notification_type_name); $time = $this->user->create_datetime(); $time->setTime(0, 0, 0); @@ -58,9 +60,9 @@ $sql = 'SELECT * FROM ' . $this->notifications_table . ' WHERE notification_type_id = ' . (int)$notification_type_id . ' - AND item_id = ' . (int)$notification_data['item_id'] . ' - AND item_parent_id = ' . $item_parent_id . ' - AND notification_time >= ' . $time->getTimestamp(); + AND item_id = ' . (int)$notification_data['item_id'] . ' + AND item_parent_id = ' . $item_parent_id . ' + AND notification_time >= ' . $time->getTimestamp(); $result = $this->db->sql_query($sql); $notification_row = $this->db->sql_fetchrow($result); $this->db->sql_freeresult($result); @@ -100,9 +102,9 @@ $sql = 'SELECT * FROM ' . $this->notifications_table . ' WHERE notification_type_id = ' . (int)$notification_type_id . ' - AND item_id = ' . (int)$item_id . ' - AND item_parent_id = ' . (int)$item_parent_id . ' - ORDER BY notification_time DESC'; + AND item_id = ' . (int)$item_id . ' + AND item_parent_id = ' . (int)$item_parent_id . ' + ORDER BY notification_time DESC'; $result = $this->db->sql_query($sql); $notification_row = $this->db->sql_fetchrow($result); $this->db->sql_freeresult($result); @@ -127,7 +129,7 @@ unset($user_ary[$key]); // If there are still users, update the notification - if(sizeof($user_ary)) + if(count($user_ary)) { $data['user_ids'] = array_values($user_ary); @@ -202,4 +204,4 @@ return $message; } -} \ No hay ningún carácter de nueva línea al final del fichero +} --- libraries/reaction_manager.php @@ -1,19 +1,22 @@ auth = $auth; $this->cache = $cache; + $this->config = $config; $this->container = $container; $this->db = $db; $this->dispatcher = $dispatcher; @@ -69,6 +78,16 @@ } /** + * Check if user can view usernames + * + * @return bool + */ + public function can_view_usernames() + { + return !$this->config['reactions_anonymous'] || $this->auth->acl_get('m_reactions'); + } + + /** * Get singular reaction * * @param int $reaction_id Reaction ID @@ -76,11 +95,6 @@ */ public function get($reaction_id = null) { - if($reaction_id === null) - { - return $this->data; - } - if(is_numeric($reaction_id) && isset($this->data[$reaction_id])) { return $this->data[$reaction_id]; @@ -99,6 +113,22 @@ } /** + * Get default launcher icon + * + * @param int|false $reaction_id + * @return string + */ + public function get_launcher_html($reaction_id) + { + if(!$reaction_id && $this->config['reactions_default']) + { + return 'reaction'; + } + + return $this->get($reaction_id)->get_launcher_html(); + } + + /** * Get Reactions of posts * * @param array $post_ary Array with post IDs @@ -258,7 +288,7 @@ $output['reaction_scores'][] = [ 'S_REACTION_IMAGE' => $reaction->get_image_url(), 'S_REACTION_TITLE' => $reaction->get_title(), - 'S_TITLE' => $reaction->get_title() . ' (' . sizeof($reactions) . ')', + 'S_TITLE' => $reaction->get_title() . ' (' . count($reactions) . ')', 'U_VIEW' => $this->routing->route('canidev_reactions_controller', [ 'mode' => 'view', 'post' => $reactions_data['post_id'], @@ -266,11 +296,11 @@ ]), ]; - if(sizeof($scores_usernames) < 2) + if(count($scores_usernames) < 2) { foreach($reactions as $row) { - if(sizeof($scores_usernames) >= 2) + if(count($scores_usernames) >= 2) { break; } @@ -284,25 +314,32 @@ } } - if($reactions_data['length'] < 4) + if(!$this->can_view_usernames()) { - $key = ($reactions_data['length'] == 3) ? 'REACTION_SCORE_LABEL_COUNT_ONE' : 'REACTION_SCORE_LABEL_SIMPLE'; - - $output['REACTION_SCORE_LABEL'] = $this->language->lang($key, implode(', ', $scores_usernames)); + $output['REACTION_SCORE_LABEL'] = $this->language->lang('REACTION_SCORE_LABEL_ANONYMOUS', $reactions_data['length']); } else { - $output['REACTION_SCORE_LABEL'] = $this->language->lang( - 'REACTION_SCORE_LABEL_COUNT_MULTIPLE', - implode(', ', $scores_usernames), - $reactions_data['length'] - sizeof($scores_usernames) - ); - } + if($reactions_data['length'] < 4) + { + $key = ($reactions_data['length'] == 3) ? 'REACTION_SCORE_LABEL_COUNT' : 'REACTION_SCORE_LABEL_SIMPLE'; + + $output['REACTION_SCORE_LABEL'] = $this->language->lang($key, implode(', ', $scores_usernames), 1); + } + else + { + $output['REACTION_SCORE_LABEL'] = $this->language->lang( + 'REACTION_SCORE_LABEL_COUNT', + implode(', ', $scores_usernames), + $reactions_data['length'] - count($scores_usernames) + ); + } - $output['U_REACTION_SCORE_ALL'] = $this->routing->route('canidev_reactions_controller', [ - 'mode' => 'view', - 'post' => $reactions_data['post_id'], - ]); + $output['U_REACTION_SCORE_ALL'] = $this->routing->route('canidev_reactions_controller', [ + 'mode' => 'view', + 'post' => $reactions_data['post_id'], + ]); + } return $output; } @@ -342,7 +379,7 @@ $post_ids = [$post_ids]; } - if(!sizeof($post_ids)) + if(!count($post_ids)) { return; } --- libraries/reaction.php @@ -1,9 +1,9 @@ db_tools->sql_index_exists($this->table_prefix . 'posts', 'reaction_score'); + return true; } /** @@ -28,32 +30,4 @@ '\canidev\reactions\migrations\v102', ]; } - - /** - * {@inheritDoc} - */ - public function update_schema() - { - return [ - 'add_index' => [ - $this->table_prefix . 'posts' => [ - 'reaction_score' => ['poster_id', 'post_reaction_score'], - ], - ], - ]; - } - - /** - * {@inheritDoc} - */ - public function revert_schema() - { - return [ - 'drop_keys' => [ - $this->table_prefix . 'posts' => [ - 'reaction_score', - ], - ], - ]; - } } --- migrations/v104.php @@ -0,0 +1,88 @@ +db_tools->sql_column_exists($this->table_prefix . 'users', 'user_reaction_score'); + } + + /** + * {@inheritDoc} + */ + static public function depends_on() + { + return [ + '\canidev\reactions\migrations\v102', + ]; + } + + /** + * {@inheritDoc} + */ + public function update_schema() + { + return [ + 'add_columns' => [ + $this->table_prefix . 'users' => [ + 'user_reaction_score' => ['INT:4', null], + ], + ], + ]; + } + + /** + * {@inheritDoc} + */ + public function revert_schema() + { + return [ + 'drop_columns' => [ + $this->table_prefix . 'users' => [ + 'user_reaction_score', + ], + ], + ]; + } + + /** + * {@inheritDoc} + */ + public function update_data() + { + return [ + ['config.add', ['reactions_forums', '']], + ['config.add', ['reactions_default', 0]], + ['config.add', ['reactions_anonymous', 0]], + + ['custom', [[$this, 'adjust_first_score']]], + ]; + } + + /** + * Set first user score. 0 on fresh install, null on update. (Fix phpbb_posts index problem) + */ + public function adjust_first_score() + { + if(defined('REACTIONS_FRESH_INSTALL')) + { + $sql = 'UPDATE ' . USERS_TABLE . ' + SET user_reaction_score = 0 + WHERE user_reaction_score IS NULL'; + $this->db->sql_query($sql); + } + } +} --- notification/post.php @@ -1,9 +1,9 @@ + (function($) { + cbbCore.initService('reactions', {{ REACTIONS_OPTIONS }}); + })(jQuery); + +{% endif %} --- styles/all/template/event/viewtopic_body_postrow_post_notices_after.html @@ -1,6 +1,8 @@ -{% if REACTIONS_BTN_POSITION == 'below' %} - {% set launcher_need_wrapper = true %} - {% include "@canidev_reactions/launcher_button.html" %} -{% endif %} +
+ {% include "@canidev_reactions/score_list.html" %} -{% include "@canidev_reactions/score_list.html" %} \ No hay ningún carácter de nueva línea al final del fichero + {% if REACTIONS_BTN_POSITION == 'below' %} + {% set launcher_need_wrapper = true %} + {% include "@canidev_reactions/launcher_button.html" %} + {% endif %} +
--- styles/all/template/launcher_button.html @@ -1,37 +1,27 @@ {% if launcher_need_wrapper %}
    {% endif %} -{% if postrow is defined %} - {% if postrow.CAN_USE_REACTIONS %} +{% if not postrow is defined %} + {% set postrow = score_rows|first %} +{% endif %} + +{% if postrow.CAN_USE_REACTIONS %}
  • - + {{ postrow.REACTION_LAUNCHER_ICON }}
      {% for reaction in reactions %} -
    • {{ reaction.S_TITLE }}
    • - {% endfor %} -
    -
    -
  • - {% endif %} -{% else %} - {% if CAN_USE_REACTIONS %} -
  • - - {{ REACTION_LAUNCHER_ICON }} - - -
    -
      - {% for reaction in reactions %} -
    • {{ reaction.S_TITLE }}
    • +
    • + + {{ reaction.S_TITLE }} + +
    • {% endfor %}
  • - {% endif %} {% endif %} {% if launcher_need_wrapper %}
{% endif %} \ No hay ningún carácter de nueva línea al final del fichero --- styles/all/template/reactions_list.html @@ -30,7 +30,7 @@
{{ item.S_REACTION_TIME }}
- {% if item.U_DELETE %}{% endif %} + {% if item.U_DELETE %}{% endif %} {{ item.CUSTOM_ACTIONS }}
--- styles/all/template/reactions_list_simple.html @@ -0,0 +1,11 @@ +
    +{% for tab in loops.tab %} + {% if tab.S_ID <> 0 %} +
  • + {% if tab.S_IMAGE %}{{ tab.S_TITLE }}{% endif %} + {{ tab.S_TITLE }} + {{ tab.S_COUNT }} +
  • + {% endif %} +{% endfor %} +
--- styles/all/template/score_list.html @@ -1,19 +1,18 @@ -{% if postrow is defined %} -
- -
- {% for score in postrow.reaction_scores %} - {{ score.S_REACTION_TITLE }} - {% endfor %} -
+{% if not postrow is defined %} + {% set postrow = score_rows|first %} +{% endif %} + +
+
+ {% if postrow.U_REACTION_SCORE_ALL %} + {{ postrow.REACTION_SCORE_LABEL }} + {% else %} + {{ postrow.REACTION_SCORE_LABEL }} + {% endif %}
-{% else %} -
- -
- {% for score in reaction_scores %} - {{ score.S_REACTION_TITLE }} - {% endfor %} -
+
+ {% for score in postrow.reaction_scores %} + {{ score.S_REACTION_TITLE }} + {% endfor %}
-{% endif %} \ No hay ningún carácter de nueva línea al final del fichero +
--- styles/all/theme/reactions.css @@ -1,7 +1,7 @@ /* cBB Reactions StyleSheet -------------------------------------------------------------- Style: All - Copyright (c) 2024 CaniDev ( https://www.canidev.com ) + Copyright (c) 2025 CaniDev ( https://www.canidev.com ) -------------------------------------------------------------- */ @@ -32,16 +32,26 @@ -webkit-touch-callout: none; } -.reactions-launcher > .reaction-button.default-icon { - width: 24px; -} - .reactions-launcher > .reaction-button:hover { background: none; box-shadow: none; text-shadow: none; } +.reactions-launcher > .reaction-button.default-icon { + width: 24px; +} + +.reactions-launcher > .default-icon img { + filter: grayscale(1); + opacity: .7; +} + +.reactions-launcher > .default-icon:hover img { + filter: grayscale(.3); + opacity: .8; +} + .reactions-launcher > .reaction-button span { font-size: 0.85em; font-weight: bold; @@ -55,10 +65,28 @@ } .reaction-launcher-wrapper { - clear: both; + display: flex; list-style: none; - float: right; - margin-bottom: 8px; + position: relative; +} + +.reaction-launcher-wrapper .reactions-launcher { + float: none; +} + +.reaction-score-list:not(.empty) + .reaction-launcher-wrapper { + margin-left: 8px; + padding-left: 6px; +} + +.reaction-score-list:not(.empty) + .reaction-launcher-wrapper::before { + background-color: #ccc; + bottom: 5px; + content: ""; + left: 0; + position: absolute; + top: 5px; + width: 1px; } .reaction-button img { @@ -87,14 +115,25 @@ transform: scale(1.3); } -.reaction-score-list { +.reaction-post-bar { align-items: center; clear: both; display: flex; - float: right; - height: 16px; - margin: 0 0 5px 5px; - padding: 4px; + justify-content: flex-end; + margin: 12px 4px; +} + +.reaction-post-bar::after { + content: ""; + clear: both; + display: block; +} + +.reaction-score-list { + align-items: center; + display: flex; + font-size: 1em; + padding: 0; } .reaction-score-list.empty { @@ -111,6 +150,7 @@ border: 2px solid transparent; border-radius: 50%; float: left; + line-height: 1px; position: relative; } @@ -155,10 +195,38 @@ transform: translate(-50%); } +.cbb-dropdown .reactions-loading { + margin: 20px auto; +} + +.cbb-dropdown .reactions-loading, +.cbb-dropdown .reactions-loading > span { + height: 35px; + width: 35px; +} + .reactions-view-dialog .cbb-dialog-content { display: flex; } +.reactions-view-list li { + align-items: center; + display: flex; + font-size: 11px; + gap: 10px; + padding: 6px 8px; +} + +.reactions-view-list img { + height: 24px; + max-width: 24px; +} + +.reactions-view-list .tab-counter { + font-weight: bold; + margin-left: auto; +} + .reactions-list { display: flex; flex-direction: column; @@ -264,7 +332,7 @@ .reaction-row-info .username, .reaction-row-info .username-coloured { - font-size: 1.2em; + font-size: 1.1em; font-weight: bold; } @@ -321,6 +389,10 @@ border-color: #444; } +.dark-theme .reaction-launcher-wrapper:not(:first-child)::before { + background-color: #3f3f3f; +} + /* Responsive ------------------------------ */ @media only screen and (max-width: 500px) {