diff --git a/Languages/en_US/Admin.php b/Languages/en_US/Admin.php index a7314dae06a..3599e959413 100644 --- a/Languages/en_US/Admin.php +++ b/Languages/en_US/Admin.php @@ -781,7 +781,11 @@ $txt['board_perms_deny'] = 'Deny'; $txt['all_boards_in_cat'] = 'All boards in this category'; -$txt['likes_like'] = 'Membergroups allowed to like posts'; +$txt['reactions'] = 'Reactions'; +$txt['reacts_react'] = 'Membergroups allowed to react to posts'; +$txt['reactions_settings'] = 'Reactions Settings'; +$txt['manage_reactions'] = 'Manage Reactions'; +$txt['manage_reactions_desc'] = 'This area allows you to manage reactions'; $txt['mention'] = 'Membergroups allowed to mention users'; diff --git a/Languages/en_US/Alerts.php b/Languages/en_US/Alerts.php index 597fc80be57..846fd2f59b8 100644 --- a/Languages/en_US/Alerts.php +++ b/Languages/en_US/Alerts.php @@ -26,7 +26,7 @@ $txt['alert_topic_unapproved_reply'] = '{member_link} replied to your unapproved topic, {topic_msg}, in {board_msg}'; $txt['alert_msg_quote'] = '{member_link} quoted you in {msg_msg}'; $txt['alert_msg_mention'] = '{member_link} mentioned you in {msg_msg}'; -$txt['alert_msg_like'] = '{member_link} liked your post, {msg_msg}'; +$txt['alert_msg_react'] = '{member_link} reacted to your post, {msg_msg}'; $txt['alert_msg_report'] = '{member_link} reported the post {msg_msg}'; $txt['alert_msg_report_reply'] = '{member_link} replied to the report about {msg_msg}'; $txt['alert_member_report'] = '{member_link} reported the profile of {profile_msg}'; diff --git a/Languages/en_US/General.php b/Languages/en_US/General.php index 32e4927fd2e..e42ac52d4b7 100644 --- a/Languages/en_US/General.php +++ b/Languages/en_US/General.php @@ -823,25 +823,29 @@ // Mentions $txt['mentions'] = 'Mentions'; -// Likes -$txt['likes'] = 'Likes'; +// Reactions. Previously "likes". +$txt['reactions'] = 'Reactions'; +// Leave these two for now - "like" will still be the default $txt['like'] = 'Like'; $txt['unlike'] = 'Unlike'; -$txt['like_success'] = 'Your content was successfully liked.'; -$txt['like_delete'] = 'Your content was successfully deleted.'; -$txt['like_insert'] = 'Your content was successfully inserted.'; -$txt['like_error'] = 'There was an error with your request.'; -$txt['like_disable'] = 'Likes feature is disabled.'; -$txt['not_valid_like_type'] = 'The liked type is not a valid type.'; -$txt['likes_count'] = '{num, plural, - one {# person likes this.} - other {# people like this.} + +// Todo - i18n this? Maybe react_{type}_success? +$txt['react_success'] = 'You succesfully reacted to the content '; +$txt['react_delete'] = 'Your content was successfully deleted.'; +$txt['react_insert'] = 'Your content was successfully inserted.'; +$txt['react_error'] = 'There was an error with your request.'; +$txt['reactions_disable'] = 'Reactions feature is disabled.'; +$txt['not_valid_react_type'] = 'The reacted type is not a valid type.'; +$txt['reactions_count'] = '{num, plural, + one {# person reacted to this.} + other {# people reacted to this.} }'; -$txt['you_likes_count'] = '{num, plural, - =0 {You like this.} - one {You and # other person like this.} - other {You and # other people like this.} +$txt['you_reactions_count'] = '{num, plural, + =0 {You reacted to this.} + one {You and # other person reacted to this.} + other {You and # other people reacted to this.} }'; +$txt['invalid_reaction'] = 'Invalid reaction ID'; $txt['report_to_mod'] = 'Report to moderator'; $txt['report_profile'] = 'Report profile of {member_name}'; diff --git a/Languages/en_US/ManagePermissions.php b/Languages/en_US/ManagePermissions.php index e495d0bd9a4..61df176bb7e 100644 --- a/Languages/en_US/ManagePermissions.php +++ b/Languages/en_US/ManagePermissions.php @@ -241,9 +241,9 @@ $txt['permissionname_report_any'] = 'Report posts to the moderators'; $txt['permissionhelp_report_any'] = 'This permission adds a link to each message, allowing a user to report a post to a moderator. On reporting, all moderators on that board will receive an email with a link to the reported post and a description of the problem (as given by the reporting user).'; -$txt['permissiongroup_likes'] = 'Likes'; -$txt['permissionname_likes_like'] = 'Can like any content'; -$txt['permissionhelp_likes_like'] = 'This permission allows a user to like any content. Users are not allowed to like their own content.'; +$txt['permissiongroup_reactions'] = 'Reactions'; +$txt['permissionname_reactions_react'] = 'Can react to any content'; +$txt['permissionhelp_reactions_react'] = 'This permission allows a user to react to any content. Users are not allowed to react to their own content.'; $txt['permissiongroup_mentions'] = 'Mentions'; $txt['permissionname_mention'] = 'Mention others via @name'; diff --git a/Languages/en_US/ManageReactions.php b/Languages/en_US/ManageReactions.php new file mode 100644 index 00000000000..e35dcd1fa39 --- /dev/null +++ b/Languages/en_US/ManageReactions.php @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/Languages/en_US/ManageSettings.php b/Languages/en_US/ManageSettings.php index ecbf7a4c940..2cce9116db6 100644 --- a/Languages/en_US/ManageSettings.php +++ b/Languages/en_US/ManageSettings.php @@ -115,8 +115,8 @@ $txt['force_ssl_complete'] = 'Force SSL throughout the forum'; $txt['search_language'] = 'Fulltext Search Language'; -// Like settings. -$txt['enable_likes'] = 'Enable Likes'; +// Reaction settings. +$txt['enable_reacts'] = 'Enable Reactions'; // Mention settings. $txt['enable_mentions'] = 'Enable Mentions'; diff --git a/Languages/en_US/Profile.php b/Languages/en_US/Profile.php index 292a6ee7e2a..b973ff384c2 100644 --- a/Languages/en_US/Profile.php +++ b/Languages/en_US/Profile.php @@ -136,7 +136,7 @@ $txt['alert_board_notify'] = 'When a new topic is created in a board I follow, I normally want to know via...'; $txt['alert_msg_mention'] = 'When my @name is mentioned in a post'; $txt['alert_msg_quote'] = 'When one of my posts is quoted'; -$txt['alert_msg_like'] = 'When one of my posts is liked'; +$txt['alert_msg_react'] = 'When someoe reacts to one of my posts'; $txt['alert_unapproved_reply'] = 'When a reply is made to my unapproved topic'; $txt['alert_group_pm'] = 'Personal Messages'; $txt['alert_pm_new'] = 'When I receive a new personal message'; diff --git a/Languages/en_US/Stats.php b/Languages/en_US/Stats.php index f86a7ac714a..ef3f06ce15a 100644 --- a/Languages/en_US/Stats.php +++ b/Languages/en_US/Stats.php @@ -21,8 +21,8 @@ $txt['top_starters'] = 'Top Topic Starters'; $txt['top_time_online'] = 'Most Time Online'; $txt['stats_more_detailed'] = 'more detailed »'; -$txt['top_liked_messages'] = 'Top liked messages'; -$txt['top_liked_users'] = 'Top liked users'; +$txt['top_reacted_messages'] = 'Top reacted messages'; +$txt['top_reacted_users'] = 'Top reacted users'; $txt['average_members'] = 'Average registrations per day'; $txt['average_posts'] = 'Average posts per day'; diff --git a/Sources/Actions/Admin/ACP.php b/Sources/Actions/Admin/ACP.php index 544f219ecdc..9a2961b695c 100644 --- a/Sources/Actions/Admin/ACP.php +++ b/Sources/Actions/Admin/ACP.php @@ -183,9 +183,6 @@ class ACP implements ActionInterface, Routable 'profile' => [ 'label' => 'custom_profile_shorttitle', ], - 'likes' => [ - 'label' => 'likes', - ], 'mentions' => [ 'label' => 'mentions', ], @@ -339,6 +336,20 @@ class ACP implements ActionInterface, Routable ], ], ], + 'managereactions' => [ + 'label' => 'reactions', + 'function' => __NAMESPACE__ . '\\Reactions::call', + 'icon' => 'reactions', + 'permission' => ['admin_forum'], + 'subsections' => [ + 'settings' => [ + 'label' => 'reactions_settings', + ], + 'edit' => [ + 'label' => 'manage_reactions', + ], + ], + ], 'smileys' => [ 'label' => 'smileys_manage', 'function' => __NAMESPACE__ . '\\Smileys::call', diff --git a/Sources/Actions/Admin/Features.php b/Sources/Actions/Admin/Features.php index e3ba292b314..c28967d3980 100644 --- a/Sources/Actions/Admin/Features.php +++ b/Sources/Actions/Admin/Features.php @@ -73,7 +73,6 @@ class Features implements ActionInterface 'sig' => 'signature', 'profile' => 'profile', 'profileedit' => 'profileEdit', - 'likes' => 'likes', 'mentions' => 'mentions', 'alerts' => 'alerts', ]; @@ -1443,32 +1442,6 @@ public function profileEdit(): void SecurityToken::create('admin-ecp'); } - /** - * Handles modifying the likes settings. - * - * Accessed from ?action=admin;area=featuresettings;sa=likes - */ - public function likes(): void - { - $config_vars = self::likesConfigVars(); - - // Saving? - if (isset($_GET['save'])) { - User::$me->checkSession(); - - IntegrationHook::call('integrate_save_likes_settings'); - - ACP::saveDBSettings($config_vars); - $_SESSION['adm-save'] = true; - Utils::redirectexit('action=admin;area=featuresettings;sa=likes'); - } - - Utils::$context['post_url'] = Config::$scripturl . '?action=admin;area=featuresettings;save;sa=likes'; - Utils::$context['settings_title'] = Lang::getTxt('likes', file: 'General'); - - ACP::prepareDBSettingContext($config_vars); - } - /** * Handles modifying the mentions settings. * @@ -1821,23 +1794,6 @@ public static function sigConfigVars(): array return $config_vars; } - /** - * Gets the configuration variables for the likes sub-action. - * - * @return array $config_vars for the likes sub-action. - */ - public static function likesConfigVars(): array - { - $config_vars = [ - ['check', 'enable_likes'], - ['permissions', 'likes_like'], - ]; - - IntegrationHook::call('integrate_likes_settings', [&$config_vars]); - - return $config_vars; - } - /** * Gets the configuration variables for the mentions sub-action. * @@ -1967,9 +1923,7 @@ protected function init() 'description' => Lang::getTxt('signature_settings_desc', file: 'ManageSettings'), ], 'profile' => [ - 'description' => Lang::getTxt('custom_profile_desc', file: 'ManageSettings'), - ], - 'likes' => [ + 'description' => Lang::$txt['custom_profile_desc'], ], 'mentions' => [ ], diff --git a/Sources/Actions/Admin/Find.php b/Sources/Actions/Admin/Find.php index c07c146fc05..c033ac9dc14 100644 --- a/Sources/Actions/Admin/Find.php +++ b/Sources/Actions/Admin/Find.php @@ -62,7 +62,6 @@ class Find implements ActionInterface [__NAMESPACE__ . '\\Features::basicConfigVars', 'area=featuresettings;sa=basic'], [__NAMESPACE__ . '\\Features::bbcConfigVars', 'area=featuresettings;sa=bbc'], [__NAMESPACE__ . '\\Features::layoutConfigVars', 'area=featuresettings;sa=layout'], - [__NAMESPACE__ . '\\Features::likesConfigVars', 'area=featuresettings;sa=likes'], [__NAMESPACE__ . '\\Features::mentionsConfigVars', 'area=featuresettings;sa=mentions'], [__NAMESPACE__ . '\\Features::sigConfigVars', 'area=featuresettings;sa=sig'], [__NAMESPACE__ . '\\AntiSpam::getConfigVars', 'area=antispam'], @@ -80,6 +79,7 @@ class Find implements ActionInterface [__NAMESPACE__ . '\\Posts::postConfigVars', 'area=postsettings;sa=posts'], [__NAMESPACE__ . '\\Posts::topicConfigVars', 'area=postsettings;sa=topics'], [__NAMESPACE__ . '\\Posts::draftConfigVars', 'area=postsettings;sa=drafts'], + [__NAMESPACE__ . '\\Reactions::getConfigVars', 'area=managereactions;sa=settings'], [__NAMESPACE__ . '\\Search::getConfigVars', 'area=managesearch;sa=settings'], [__NAMESPACE__ . '\\Smileys::getConfigVars', 'area=smileys;sa=settings'], [__NAMESPACE__ . '\\Server::generalConfigVars', 'area=serversettings;sa=general'], @@ -90,6 +90,7 @@ class Find implements ActionInterface [__NAMESPACE__ . '\\Server::exportConfigVars', 'area=serversettings;sa=export'], [__NAMESPACE__ . '\\Server::loadBalancingConfigVars', 'area=serversettings;sa=loads'], [__NAMESPACE__ . '\\Languages::getConfigVars', 'area=languages;sa=settings'], + [__NAMESPACE__ . '\\Reactions::getConfigVars', 'area=reactions;sa=settings'], [__NAMESPACE__ . '\\Registration::getConfigVars', 'area=regcenter;sa=settings'], [__NAMESPACE__ . '\\SearchEngines::getConfigVars', 'area=sengines;sa=settings'], [__NAMESPACE__ . '\\Subscriptions::getConfigVars', 'area=paidsubscribe;sa=settings'], @@ -116,6 +117,7 @@ class Find implements ActionInterface 'ManageMail', 'ManagePaid', 'ManagePermissions', + 'ManageReactions', 'ManageSettings', 'ManageSmileys', 'Search', diff --git a/Sources/Actions/Admin/Permissions.php b/Sources/Actions/Admin/Permissions.php index 4b09c0f4900..5bfe44aab07 100644 --- a/Sources/Actions/Admin/Permissions.php +++ b/Sources/Actions/Admin/Permissions.php @@ -110,7 +110,7 @@ class Permissions implements ActionInterface 'member_admin', 'profile', 'profile_account', - 'likes', + 'reactions', 'mentions', 'bbc', ], diff --git a/Sources/Actions/Admin/Reactions.php b/Sources/Actions/Admin/Reactions.php new file mode 100644 index 00000000000..df21eb0221d --- /dev/null +++ b/Sources/Actions/Admin/Reactions.php @@ -0,0 +1,387 @@ + 'editreactions', + 'settings' => 'settings', + ]; + + /*********************** + * Public methods + ***********************/ + + /** + * Handles modifying reactions settings + */ + public static function settings(): void + { + $config_vars = self::getConfigVars(); + + // Setup the basics of the settings template. + Utils::$context['sub_template'] = 'show_settings'; + Utils::$context['page_title'] = Lang::$txt['reactions_settings']; + + if (isset($_REQUEST['save'])) { + User::$me->checkSession(); + SecurityToken::validate('admin-mr'); + IntegrationHook::call('integrate_save_reactions_settings'); + + // Yeppers, saving this... + ACP::saveDBSettings($config_vars); + + $_SESSION['adm-save'] = true; + Utils::redirectexit('action=admin;area=managereactions;sa=settings'); + } + + // Finish up the form... + Utils::$context['post_url'] = Config::$scripturl . '?action=admin;area=managereactions;save;sa=settings'; + Utils::$context['settings_title'] = Lang::$txt['reactions_settings']; + + // We need this for the in-line permissions + SecurityToken::create('admin-mr'); + + ACP::prepareDBSettingContext($config_vars); + } + + /*********************** + * Public static methods + ***********************/ + + /** + * Gets the configuration variables for the settings sub-action. + * + * @return array $config_vars for the settings sub-action. + */ + public static function getConfigVars(): array + { + $config_vars = [ + ['check', 'enable_reacts'], + ['permissions', 'reactions_react'], + ]; + + IntegrationHook::call('integrate_reactions_settings', [&$config_vars]); + + return $config_vars; + } + + /** + * Dispatcher to whichever sub-action method is necessary. + */ + public function execute(): void + { + $call = method_exists($this, self::$subactions[$this->subaction]) ? [$this, self::$subactions[$this->subaction]] : Utils::getCallable(self::$subactions[$this->subaction]); + + if (!empty($call)) { + call_user_func($call); + } + } + + /** + * Handle adding, deleting and editing reactions + */ + public function editreactions(): void + { + // Load the language file + Lang::load('ManageReactions'); + + // Make sure we select the right menu item + Menu::$loaded['admin']['currentsubsection'] = 'editreactions'; + + // Get the reactions. If we're updating things then we'll overwrite this later + $reactions = $this->getReactions(); + + // They must have submitted a form. + if (isset($_POST['reacts_save']) || isset($_POST['reacts_delete'])) { + User::$me->checkSession(); + SecurityToken::validate('admin-mre'); + + // This will indicate whether we need to update the reactions cache later... + $do_update = false; + + // Anything to delete? + if (isset($_POST['reacts_delete']) && isset($_POST['delete_reacts'])) { + $do_update = true; + $deleted = []; + + foreach ($_POST['delete_reacts'] as $to_delete) { + $deleted[] = (int) $to_delete; + } + + // Now to do the actual deleting + Db::$db->query(' + DELETE FROM {db_prefix}reactions + WHERE id_reaction IN ({array_int:deleted})', + [ + 'deleted' => $deleted, + ] + ); + + // Are there any posts that used these reactions? + $get_reacted_posts = Db::$db->query(' + SELECT id_msg, COUNT (id_react) AS num_reacts + FROM {db_prefix}reactions + WHERE id_reaction IN ({array_int:deleted}) + GROUP BY id_msg', + [ + 'deleted' => $deleted, + ] + ); + + // Update the number of reactions for the affected post(s) + // Did we find anything? + if (Db::$db->num_rows($get_reacted_posts) > 0) { + while ($reacted_post = $get_reacted_posts->fetchAssoc()) { + Db::$db->query(' + UPDATE {db_prefix}messages + SET reactions = reactions-{int:deleted} + WHERE id_msg = {int:msg}', + [ + 'deleted' => $reacted_post['num_reacts'], + 'msg' => $reacted_post['id_msg'], + ] + ); + } + } + } // Updating things? + elseif (isset($_POST['reacts'])) { + // Adding things? + if (isset($_POST['reacts_add'])) { + foreach ($_POST['reacts_add'] as $new_react) { + // No funny stuff now.. + $new_react = trim($new_react); + if (!empty($new_react)) { + $add[] = [$new_react]; + } + } + + if (!empty($add)) { + $do_update = true; + + // Insert the new reactions + Db::$db->insert('', '{db_prefix}reactions', ['name' => 'string'], $add, []); + } + } + + // Updating things... + $updates = []; + foreach ($_POST['reacts'] as $id => $name) { + // Again, no funny stuff... + $name = trim($name); + + // Did they update this one? Ignore empty ones for now + if ($reactions[$id] != $name && !empty($name)) { + $updates[] = [$id, $name]; + } + } + + // Anything to update? + if (!empty($updates)) { + $do_update = true; + // Do the update + Db::$db->insert('replace', '{db_prefix}reactions', ['id_reaction' => 'int', 'name' => 'string'], $updates, ['id_reaction']); + } + } + + // If we updated anything, re-cache everything + if ($do_update) { + // Re-cache the reactions and update the reactions variable so the form will show the changes + CacheApi::put('reactions', null); + $reactions = $this->getReactions(); + CacheApi::put('reactions', $reactions, 480); + } + } + + // Set up the form now... + + // Create our token + SecurityToken::create('admin-mre'); + + // Set up our list. Use a special function for the get_items so we can output things in input fields... + $listOptions = [ + 'id' => 'reactions_list', + 'title' => Lang::$txt['reactions'], + 'no_items_label' => Lang::$txt['no_reactions'], + 'base_href' => Config::$scripturl . '?action=admin;area=managereactions;sa=edit', + 'get_items' => [ + 'function' => function (int $start, int $items_per_page, string $sort_by) use ($reactions): array { + $items = []; + foreach ($reactions as $id => $name) { + $items[] = [ + 'id' => $id, + 'name' => $name, + ]; + } + return $items; + }, + ], + 'get_count' => [ + 'value' => count($reactions), + ], + 'columns' => + [ + 'name' => + [ + 'header' => [ + 'value' => Lang::$txt['reactions_name'], + ], + 'data' => [ + 'function' => function ($rowData) { + return ''; + } + ], + ], + 'check' => [ + 'header' => [ + 'value' => '', + 'class' => 'centercol', + ], + 'data' => [ + 'function' => function ($rowData) { + return ''; + }, + 'class' => 'centercol', + ] + ] + ] + ]; + + // Add a row for a blank field to add a reaction, and a link to add another blank field. + $listOptions['additional_rows'] = [ + [ + 'position' => 'below_table_data', + 'value' => '' + ], + [ + // Clicking this magic button adds a new row... + 'position' => 'below_table_data', + 'value' => '' + ], + [ + // And last but not least our input buttons + 'position' => 'below_table_data', + 'value' => ' + ' + ] + ]; + + // And some inline JS to handle adding another row + $listOptions['javascript'] = ' + function addrow() { + reacts_table = document.getElementById(\'reactions_list\'); + new_row = document.getElementById(\'reactions_list\').insertRow(reacts_table.rows.length); + new_row.insertCell(0).innerHTML = \'\'; + new_row.insertCell(1).innerHTML = \'\'; + }'; + + // Now that we have our list options set up, have some fun... + $listOptions['form'] = [ + 'href' => Config::$scripturl . '?action=admin;area=managereactions;sa=edit;' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], + 'name' => 'list_reactions', + 'token' => 'admin-mre', + ]; + + new ItemList($listOptions); + + Utils::$context['page_title'] = Lang::$txt['manage_reactions']; + Utils::$context['sub_template'] = 'show_list'; + Utils::$context['default_list'] = 'reactions_list'; + } + + /****************** + * Internal methods + ******************/ + + /** + * Constructor. Protected to force instantiation via self::load(). + */ + protected function __construct() + { + // Load up our language and set up the menu. + Lang::load('ManageReactions'); + + // Setup the admin tabs. + Menu::$loaded['admin']->tab_data = [ + 'title' => Lang::$txt['reactions'], + 'help' => 'manage_reactions', + 'description' => Lang::$txt['admin_manage_reactions'], + 'tabs' => [ + 'settings' => [ + 'description' => Lang::$txt['reaction_settings_explain'], + ], + 'edit' => [ + 'description' => Lang::$txt['manage_reactions_desc'], + 'disabled' => !Config::$modSettings['enable_reacts'], + ], + ], + ]; + + Utils::$context['last_tab'] = Config::$modSettings['enable_reacts'] ? 'edit' : 'settings'; + + if (!empty($_REQUEST['sa']) && isset(self::$subactions[$_REQUEST['sa']])) { + $this->subaction = $_REQUEST['sa']; + } + + Utils::$context['sub_action'] = &$this->subaction; + } +} +?> \ No newline at end of file diff --git a/Sources/Actions/Display.php b/Sources/Actions/Display.php index 802b8484300..a82480f44f1 100644 --- a/Sources/Actions/Display.php +++ b/Sources/Actions/Display.php @@ -1227,7 +1227,7 @@ protected function getMessagesAndPosters(): void } /** - * Initializes Msg::get() and loads attachments and likes. + * Initializes Msg::get() and loads attachments and reactions. */ protected function initDisplayContext(): void { @@ -1242,9 +1242,9 @@ protected function initDisplayContext(): void Attachment::prepareByMsg($this->messages); } - // And the likes - if (!empty(Config::$modSettings['enable_likes'])) { - Utils::$context['my_likes'] = Topic::$info->getLikedMsgs(); + // And the reactions + if (!empty(Config::$modSettings['enable_reacts'])) { + Utils::$context['my_reactions'] = Topic::$info->getReactedMsgs(); } // Go to the last message if the given time is beyond the time of the last message. @@ -1264,7 +1264,7 @@ protected function initDisplayContext(): void Msg::$getter = []; Utils::$context['first_message'] = 0; Utils::$context['first_new_message'] = false; - Utils::$context['likes'] = []; + Utils::$context['reactions'] = []; } // Set the callback. (do you REALIZE how much memory all the messages would take?!?) diff --git a/Sources/Actions/Like.php b/Sources/Actions/React.php similarity index 70% rename from Sources/Actions/Like.php rename to Sources/Actions/React.php index 42b28084784..9c7d5683a51 100644 --- a/Sources/Actions/Like.php +++ b/Sources/Actions/React.php @@ -27,6 +27,7 @@ use SMF\Lang; use SMF\OutputTypeInterface; use SMF\OutputTypes; +use SMF\ReactionTrait; use SMF\Routable; use SMF\Theme; use SMF\Time; @@ -36,10 +37,11 @@ /** * Handles liking posts and displaying the list of who liked a post. */ -class Like implements ActionInterface, Routable +class React implements ActionInterface, Routable { use ActionRouter; use ActionTrait; + use ReactionTrait; /******************* * Public properties @@ -51,7 +53,7 @@ class Like implements ActionInterface, Routable * The requested sub-action. * This should be set by the constructor. */ - public string $subaction = 'like'; + public string $subaction = 'react'; /************************** * Public static properties @@ -68,7 +70,7 @@ class Like implements ActionInterface, Routable * regarding hooks, etc., assumes that they are only called by like(). */ public static array $subactions = [ - 'like' => 'like', + 'react' => 'react', 'view' => 'view', 'delete' => 'delete', 'insert' => 'insert', @@ -123,14 +125,14 @@ class Like implements ActionInterface, Routable * * The number of times the content has been liked. */ - protected int $num_likes = 0; + protected int $num_reacts = 0; /** * @var bool * * If the current user has already liked this content. */ - protected bool $already_liked = false; + protected bool $already_reacted = false; /** * @var array @@ -138,15 +140,15 @@ class Like implements ActionInterface, Routable * Mostly used for external integration. Needs to be filled as an array * with the following keys: * - * 'can_like' bool|string True if the current user can actually like + * 'can_react' bool|string True if the current user can actually react to * this content, or a Lang::$txt key for an * error message if not. * - * 'redirect' string URL to redirect to after the like is submitted. + * 'redirect' string URL to redirect to after the react is submitted. * If not set, will redirect to the forum index. * * 'type' string 6 character unique identifier for the content. - * Must match what was sent in $_GET['ltype'] + * Must match what was sent in $_GET['rtype'] * * 'flush_cache' bool If true, reset the like content's cache entry * after a new entry has been inserted. Optional. @@ -159,8 +161,8 @@ class Like implements ActionInterface, Routable * 'json' bool If true, the class will return a JSON object as * a response instead of HTML. Default: false. */ - protected array $valid_likes = [ - 'can_like' => false, + protected array $valid_reacts = [ + 'can_react' => false, 'redirect' => '', 'type' => '', 'flush_cache' => '', @@ -192,6 +194,18 @@ class Like implements ActionInterface, Routable */ protected mixed $data; + /** + * @var int + * + * The ID of the selected reaction. Should match an entry in the reactions table. + */ + protected int $id_react = 0; + + /** @var array + * + * An array of available reactions + */ + /**************** * Public methods ****************/ @@ -285,10 +299,11 @@ protected function __construct() $this->subaction = $_REQUEST['sa']; } - $this->type = $_GET['ltype'] ?? ''; - $this->content = (int) ($_GET['like'] ?? 0); + $this->type = $_GET['rtype'] ?? ''; + $this->content = (int) ($_GET['react'] ?? 0); $this->js = isset($_GET['js']); $this->extra = $_GET['extra'] ?? false; + $this->id_react = $_GET['id_react'] ?? 0; // We do not want to output debug information here. if ($this->js) { @@ -299,14 +314,14 @@ protected function __construct() /** * Performs basic checks on the data provided, checks for a valid msg like. * - * Calls integrate_valid_likes hook for retrieving all the data needed and + * Calls integrate_valid_reacts hook for retrieving all the data needed and * apply checks based on the data provided. */ protected function check(): void { // This feature is currently disable. - if (empty(Config::$modSettings['enable_likes'])) { - $this->error = 'like_disable'; + if (empty(Config::$modSettings['enable_reacts'])) { + $this->error = 'react_disable'; return; } @@ -322,6 +337,13 @@ protected function check(): void return; } + // Is this a valid reaction ID? + if ($this->id_react != 0 && in_array($this->id_react, $this->getReactions())) { + $this->error = 'invalid_reaction'; + + return; + } + // First we need to verify whether the user can see the type of content. // This is set up to be extensible, so we'll check for the one type we // do know about, and if it's not that, we'll defer to any hooks. @@ -356,48 +378,48 @@ protected function check(): void // So we know what topic it's in and more importantly we know the // user can see it. If we're not viewing, we need some info set up. - $this->valid_likes['type'] = 'msg'; - $this->valid_likes['flush_cache'] = 'likes_topic_' . $this->id_topic . '_' . User::$me->id; - $this->valid_likes['redirect'] = 'topic=' . $this->id_topic . '.msg' . $this->content . '#msg' . $this->content; + $this->valid_reacts['type'] = 'msg'; + $this->valid_reacts['flush_cache'] = 'reacts_topic_' . $this->id_topic . '_' . User::$me->id; + $this->valid_reacts['redirect'] = 'topic=' . $this->id_topic . '.msg' . $this->content . '#msg' . $this->content; - $this->valid_likes['can_like'] = (User::$me->id == $topicOwner ? 'cannot_like_content' : (User::$me->allowedTo('likes_like') ? true : 'cannot_like_content')); + $this->valid_reacts['can_react'] = (User::$me->id == $topicOwner ? 'cannot_react_content' : (User::$me->allowedTo('reacts_react') ? true : 'cannot_react_content')); } else { /* * MOD AUTHORS: This will give you whatever the user offers up in - * terms of liking, e.g. $this->type=msg, $this->content=1. + * terms of reacting, e.g. $this->type=msg, $this->content=1. * * When you hook this, check $this->type first. If it is not * something your mod worries about, return false. * * Otherwise, return an array according to the documentation for - * $this->valid_likes. Determine (however you need to) that the user + * $this->valid_reacts. Determine (however you need to) that the user * can see and can_like the relevant liked content (and it exists). * Remember that users can't like their own content. * * If the user can like it, you MUST return your type in the 'type' * key of the returned array. * - * See also issueLike() for further notes. + * See also issueReact() for further notes. */ - $can_like = IntegrationHook::call('integrate_valid_likes', [$this->type, $this->content, $this->subaction, $this->js, $this->extra]); + $can_react = IntegrationHook::call('integrate_valid_reacts', [$this->type, $this->content, $this->subaction, $this->js, $this->extra]); $found = false; - if (!empty($can_like)) { - $can_like = (array) $can_like; + if (!empty($can_react)) { + $can_react = (array) $can_react; - foreach ($can_like as $result) { + foreach ($can_react as $result) { if ($result !== false) { // Match the type with what we already have. if (!isset($result['type']) || $result['type'] != $this->type) { - $this->error = 'not_valid_like_type'; + $this->error = 'not_valid_react_type'; return; } // Fill out the rest. $this->type = $result['type']; - $this->valid_likes = array_merge($this->valid_likes, $result); + $this->valid_reacts = array_merge($this->valid_reacts, $result); $found = true; break; @@ -412,10 +434,10 @@ protected function check(): void } } - // Is the user able to like this? - // Viewing a list of likes doesn't require this permission. - if ($this->subaction != 'view' && isset($this->valid_likes['can_like']) && \is_string($this->valid_likes['can_like'])) { - $this->error = $this->valid_likes['can_like']; + // Is the user able to react to this? + // Viewing a list of reactions doesn't require this permission. + if ($this->subaction != 'view' && isset($this->valid_reacts['can_react']) && is_string($this->valid_reacts['can_react'])) { + $this->error = $this->valid_reacts['can_react']; return; } @@ -427,13 +449,14 @@ protected function check(): void protected function delete(): void { Db::$db->query( - 'DELETE FROM {db_prefix}user_likes - WHERE content_id = {int:like_content} - AND content_type = {string:like_type} + '', + 'DELETE FROM {db_prefix}user_reacts + WHERE content_id = {int:react_content} + AND content_type = {string:react_type} AND id_member = {int:id_member}', [ - 'like_content' => $this->content, - 'like_type' => $this->type, + 'react_content' => $this->content, + 'react_type' => $this->type, 'id_member' => User::$me->id, ], ); @@ -446,15 +469,15 @@ protected function delete(): void // Check to see if there is an unread alert to delete as well... Alert::deleteWhere( [ - 'content_id = {int:like_content}', - 'content_type = {string:like_type}', + 'content_id = {int:react_content}', + 'content_type = {string:react_type}', 'id_member_started = {int:id_member_started}', 'content_action = {string:content_action}', 'is_read = {int:unread}', ], [ - 'like_content' => $this->content, - 'like_type' => $this->type, + 'react_content' => $this->content, + 'react_type' => $this->type, 'id_member_started' => User::$me->id, 'content_action' => 'like', 'unread' => 0, @@ -463,7 +486,7 @@ protected function delete(): void } /** - * Inserts a new entry on user_likes table. + * Inserts a new entry on user_reacts table. * Creates a background task for the inserted entry. */ protected function insert(): void @@ -475,18 +498,20 @@ protected function insert(): void $content = $this->content; $user = (array) User::$me; $time = time(); + $id = $this->id_react; - IntegrationHook::call('integrate_issue_like_before', [&$type, &$content, &$user, &$time]); + IntegrationHook::call('integrate_issue_react_before', [&$type, &$content, &$user, &$time, &$id]); // Insert the like. Db::$db->insert( 'insert', - '{db_prefix}user_likes', + '{db_prefix}user_reacts', [ 'content_id' => 'int', 'content_type' => 'string-6', 'id_member' => 'int', - 'like_time' => 'int', + 'react_time' => 'int', + 'id_react' => 'int', ], [ [ @@ -505,7 +530,7 @@ protected function insert(): void // Add a background task to process sending alerts. // MOD AUTHORS: you can add your own background task for your own custom - // like event using the "integrate_issue_like" hook or your callback, + // react event using the "integrate_issue_react" hook or your callback, // both are immediately called after this. if ($this->type == 'msg') { Db::$db->insert( @@ -518,7 +543,7 @@ protected function insert(): void ], [ [ - 'SMF\\Tasks\\Likes_Notify', + 'SMF\\Tasks\\Reacts_Notify', Utils::jsonEncode([ 'content_id' => $content, 'content_type' => $type, @@ -540,36 +565,36 @@ protected function insert(): void } /** - * Sets $this->num_likes to the actual number of likes that the content has. + * Sets $this->num_reacts to the actual number of reactions that the content has. */ protected function count(): void { $request = Db::$db->query( 'SELECT COUNT(*) - FROM {db_prefix}user_likes - WHERE content_id = {int:like_content} - AND content_type = {string:like_type}', + FROM {db_prefix}user_reacts + WHERE content_id = {int:react_content} + AND content_type = {string:react_type}', [ - 'like_content' => $this->content, - 'like_type' => $this->type, + 'react_content' => $this->content, + 'react_type' => $this->type, ], ); - list($likes) = Db::$db->fetch_row($request); + list($reacts) = Db::$db->fetch_row($request); Db::$db->free_result($request); - $this->num_likes = (int) $likes; + $this->num_reacts = (int) $reacts; if ($this->subaction == __FUNCTION__) { - $this->data = $this->num_likes; + $this->data = $this->num_reacts; } } /** - * Performs a like action, either like or unlike. + * Performs a reaction action, either react or "unreact" * - * Counts the total of likes and calls a hook after the event. + * Counts the total of reactions and calls a hook after the event. */ - protected function like(): void + protected function react(): void { // Safety first! if (empty($this->type) || empty($this->content)) { @@ -578,23 +603,23 @@ protected function like(): void return; } - // Do we already like this? + // Did we already react to this? $request = Db::$db->query( 'SELECT content_id, content_type, id_member - FROM {db_prefix}user_likes - WHERE content_id = {int:like_content} - AND content_type = {string:like_type} + FROM {db_prefix}user_reacts + WHERE content_id = {int:react_content} + AND content_type = {string:react_type} AND id_member = {int:id_member}', [ - 'like_content' => $this->content, - 'like_type' => $this->type, + 'react_content' => $this->content, + 'react_type' => $this->type, 'id_member' => User::$me->id, ], ); - $this->already_liked = Db::$db->num_rows($request) != 0; + $this->already_reacted = Db::$db->num_rows($request) != 0; Db::$db->free_result($request); - if ($this->already_liked) { + if ($this->already_reacted) { $this->delete(); } else { $this->insert(); @@ -608,103 +633,106 @@ protected function like(): void if ($this->type == 'msg') { Db::$db->query( 'UPDATE {db_prefix}messages - SET likes = {int:num_likes} + SET reacts = {int:num_reacts} WHERE id_msg = {int:id_msg}', [ 'id_msg' => $this->content, - 'num_likes' => $this->num_likes, + 'num_reacts' => $this->num_reacts, ], ); } // Any callbacks? - elseif (!empty($this->valid_likes['callback'])) { - $call = Utils::getCallable($this->valid_likes['callback']); + elseif (!empty($this->valid_reacts['callback'])) { + $call = Utils::getCallable($this->valid_reacts['callback']); if (!empty($call)) { \call_user_func_array($call, [$this]); } } - // Sometimes there might be other things that need updating after we do this like. - IntegrationHook::call('integrate_issue_like', [$this]); + // Sometimes there might be other things that need updating after we do this reaction. + IntegrationHook::call('integrate_issue_react', [$this]); // Now some clean up. This is provided here for any like handlers that // want to do any cache flushing. - // This way a like handler doesn't need to explicitly declare anything - // in integrate_issue_like, but do so in integrate_valid_likes where it + // This way a reaction handler doesn't need to explicitly declare anything + // in integrate_issue_react, but do so in integrate_valid_reacts where it // absolutely has to exist. - if (!empty($this->valid_likes['flush_cache'])) { - CacheApi::put($this->valid_likes['flush_cache'], null); + if (!empty($this->valid_reacts['flush_cache'])) { + CacheApi::put($this->valid_reacts['flush_cache'], null); } // All done, start building the data to pass as response. $this->data = [ 'id_topic' => !empty($this->id_topic) ? $this->id_topic : 0, 'id_content' => $this->content, - 'count' => $this->num_likes, - 'can_like' => $this->valid_likes['can_like'], - 'already_liked' => empty($this->already_liked), + 'count' => $this->num_reacts, + 'can_react' => $this->valid_reacts['can_react'], + 'already_reacted' => empty($this->already_reacted), 'type' => $this->type, ]; } /** - * This is for viewing the people who liked a thing. + * This is for viewing the people who reacted to a thing. * * Accessed from index.php?action=likes;view and should generally load in a * popup. * * We use a template for this in case themers want to style it. + * @TODO: Handle filtering by reaction */ protected function view(): void { // Firstly, load what we need. We already know we can see this, so that's something. - Utils::$context['likers'] = []; + Utils::$context['reactors'] = []; $request = Db::$db->query( - 'SELECT id_member, like_time - FROM {db_prefix}user_likes - WHERE content_id = {int:like_content} - AND content_type = {string:like_type} - ORDER BY like_time DESC', + '', + 'SELECT id_member, react_time, id_react + FROM {db_prefix}user_reacts + WHERE content_id = {int:react_content} + AND content_type = {string:react_type} + ORDER BY react_time DESC', [ - 'like_content' => $this->content, - 'like_type' => $this->type, + 'react_content' => $this->content, + 'react_type' => $this->type, ], ); while ($row = Db::$db->fetch_assoc($request)) { - Utils::$context['likers'][$row['id_member']] = ['timestamp' => $row['like_time']]; + Utils::$context['reactors'][$row['id_member']] = ['timestamp' => $row['react_time'], 'id_react' => $row['id_react']]; } Db::$db->free_result($request); // Now to get member data, including avatars and so on. - $members = array_keys(Utils::$context['likers']); + $members = array_keys(Utils::$context['reactors']); $loaded = User::load($members); if (\count($loaded) != \count($members)) { $members = array_diff($members, array_map(fn($member) => $member->id, $loaded)); foreach ($members as $not_loaded) { - unset(Utils::$context['likers'][$not_loaded]); + unset(Utils::$context['reactors'][$not_loaded]); } } - foreach (Utils::$context['likers'] as $liker => $dummy) { - if (!isset(User::$loaded[$liker])) { - unset(Utils::$context['likers'][$liker]); + foreach (Utils::$context['reactors'] as $reactor => $dummy) { + if (!isset(User::$loaded[$reactor])) { + unset(Utils::$context['reactors'][$reactor]); continue; } - Utils::$context['likers'][$liker]['profile'] = User::$loaded[$liker]->format(); - Utils::$context['likers'][$liker]['time'] = !empty($dummy['timestamp']) ? Time::create('@' . $dummy['timestamp'])->format() : ''; + Utils::$context['reactors'][$reactor]['profile'] = User::$loaded[$reactor]->format(); + Utils::$context['reactors'][$reactor]['time'] = !empty($dummy['timestamp']) ? Time::create('@' . $dummy['timestamp'])->format() : ''; } - Utils::$context['page_title'] = strip_tags(Lang::getTxt('likes_count', ['num' => \count(Utils::$context['likers'])], file: 'General')); + Utils::$context['page_title'] = strip_tags(Lang::getTxt('reacts_count', ['num' => count(Utils::$context['reactors'])])); // Lastly, setting up for display. - Theme::loadTemplate('Likes'); + Theme::loadTemplate('Reacts'); + Utils::$context['template_layers'] = []; Utils::$context['sub_template'] = 'popup'; @@ -725,42 +753,42 @@ protected function respond(): void } // Want a JSON response, do they? - if ($this->valid_likes['json']) { + if ($this->valid_reacts['json']) { $this->sendJsonReponse(); return; } // Set everything up for display. - Theme::loadTemplate('Likes'); + Theme::loadTemplate('Reacts'); Utils::$context['template_layers'] = []; // If there are any errors, process them first. if ($this->error) { // If this is a generic error, set it up good. if ($this->error == 'cannot_') { - $this->error = $this->subaction == 'view' ? 'cannot_view_likes' : 'cannot_like_content'; + $this->error = $this->subaction == 'view' ? 'cannot_view_reacts' : 'cannot_react_content'; } // Is this request coming from an AJAX call? if ($this->js) { Utils::$context['sub_template'] = 'generic'; - Utils::$context['data'] = Lang::getTxt(Lang::txtExists($this->error, file: 'General') ? $this->error : 'like_error', file: 'General'); + Utils::$context['data'] = Lang::getTxt(Lang::txtExists($this->error, file: 'General') ? $this->error : 'react_error', file: 'General'); } // Nope? Then just do a redirect to whatever URL was provided. else { - Utils::redirectexit(!empty($this->valid_likes['redirect']) ? $this->valid_likes['redirect'] . ';error=' . $this->error : ''); + Utils::redirectexit(!empty($this->valid_reacts['redirect']) ? $this->valid_reacts['redirect'] . ';error=' . $this->error : ''); } return; } - // A like operation. + // A react operation. // Not an AJAX request so send the user back to the previous // location or the main page. if (!$this->js) { - Utils::redirectexit(!empty($this->valid_likes['redirect']) ? $this->valid_likes['redirect'] : ''); + Utils::redirectexit(!empty($this->valid_reacts['redirect']) ? $this->valid_reacts['redirect'] : ''); } // These fine gentlemen all share the same template. @@ -768,7 +796,7 @@ protected function respond(): void if (\in_array($this->subaction, $generic)) { Utils::$context['sub_template'] = 'generic'; - Utils::$context['data'] = Lang::txtExists('like_' . $this->data, file: 'General') ? Lang::getTxt('like_' . $this->data, file: 'General') : $this->data; + Utils::$context['data'] = Lang::txtExists('react_' . $this->data, file: 'General') ? Lang::getTxt('react_' . $this->data, file: 'General') : $this->data; } // Directly pass the current called sub-action and the data // generated by its associated Method. @@ -790,14 +818,14 @@ protected function sendJsonReponse(): void // If there is an error, send it. if ($this->error) { if ($this->error == 'cannot_') { - $this->error = $this->subaction == 'view' ? 'cannot_view_likes' : 'cannot_like_content'; + $this->error = $this->subaction == 'view' ? 'cannot_view_reacts' : 'cannot_react_content'; } $print['error'] = $this->error; } // Do you want to add something at the very last minute? - IntegrationHook::call('integrate_likes_json_response', [&$print]); + IntegrationHook::call('integrate_reacts_json_response', [&$print]); // Print the data. Utils::serverResponse(Utils::jsonEncode($print)); diff --git a/Sources/Actions/Stats.php b/Sources/Actions/Stats.php index 06ee3bab3fe..0e694b41d93 100644 --- a/Sources/Actions/Stats.php +++ b/Sources/Actions/Stats.php @@ -602,25 +602,25 @@ public function execute(): void CacheApi::put('stats_total_time_members', $temp2, 480); } - // Likes. - if (!empty(Config::$modSettings['enable_likes'])) { + // Reactions. + if (!empty(Config::$modSettings['enable_reacts'])) { // Liked messages top 10. - Utils::$context['stats_blocks']['liked_messages'] = []; - $max_liked_message = 1; - $liked_messages = Db::$db->query( - 'SELECT m.id_msg, m.subject, m.likes, m.id_board, m.id_topic, t.approved + Utils::$context['stats_blocks']['reacted_messages'] = []; + $max_reacted_message = 1; + $reacted_messages = Db::$db->query( + 'SELECT m.id_msg, m.subject, m.reactions, m.id_board, m.id_topic, t.approved FROM ( - SELECT n.id_msg, n.subject, n.likes, n.id_board, n.id_topic + SELECT n.id_msg, n.subject, n.reactions, n.id_board, n.id_topic FROM {db_prefix}messages as n - ORDER BY n.likes DESC + ORDER BY n.reactions DESC LIMIT 1000 ) AS m INNER JOIN {db_prefix}topics AS t ON (m.id_topic = t.id_topic) INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board' . (!empty(Config::$modSettings['recycle_enable']) && Config::$modSettings['recycle_board'] > 0 ? ' AND b.id_board != {int:recycle_board}' : '') . ') - WHERE m.likes > 0 AND {query_see_board}' . (Config::$modSettings['postmod_active'] ? ' + WHERE m.reactions > 0 AND {query_see_board}' . (Config::$modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ' - ORDER BY m.likes DESC + ORDER BY m.reactions DESC LIMIT 10', [ 'recycle_board' => Config::$modSettings['recycle_board'], @@ -628,34 +628,34 @@ public function execute(): void ], ); - while ($row_liked_message = Db::$db->fetch_assoc($liked_messages)) { - Lang::censorText($row_liked_message['subject']); + while ($row_reacted_message = Db::$db->fetch_assoc($reacted_messages)) { + Lang::censorText($row_reacted_message['subject']); - Utils::$context['stats_blocks']['liked_messages'][] = [ - 'id' => $row_liked_message['id_topic'], - 'subject' => $row_liked_message['subject'], - 'num' => $row_liked_message['likes'], - 'href' => Config::$scripturl . '?msg=' . $row_liked_message['id_msg'], - 'link' => '' . $row_liked_message['subject'] . '', + Utils::$context['stats_blocks']['reacted_messages'][] = [ + 'id' => $row_reacted_message['id_topic'], + 'subject' => $row_reacted_message['subject'], + 'num' => $row_reacted_message['reacts'], + 'href' => Config::$scripturl . '?msg=' . $row_reacted_message['id_msg'], + 'link' => '' . $row_reacted_message['subject'] . '', ]; - if ($max_liked_message < $row_liked_message['likes']) { - $max_liked_message = $row_liked_message['likes']; + if ($max_reacted_message < $row_reacted_message['reacts']) { + $max_reacted_message = $row_reacted_message['reacts']; } } - Db::$db->free_result($liked_messages); + Db::$db->free_result($reacted_messages); - foreach (Utils::$context['stats_blocks']['liked_messages'] as $i => $liked_messages) { - Utils::$context['stats_blocks']['liked_messages'][$i]['percent'] = round(($liked_messages['num'] * 100) / $max_liked_message); + foreach (Utils::$context['stats_blocks']['reacted_messages'] as $i => $reacted_messages) { + Utils::$context['stats_blocks']['reacted_messages'][$i]['percent'] = round(($reacted_messages['num'] * 100) / $max_reacted_message); } - // Liked users top 10. - Utils::$context['stats_blocks']['liked_users'] = []; - $max_liked_users = 1; - $liked_users = Db::$db->query( - 'SELECT m.id_member AS liked_user, COUNT(l.content_id) AS count, mem.real_name - FROM {db_prefix}user_likes AS l - INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg) + // Reacted users top 10. + Utils::$context['stats_blocks']['reacted_users'] = []; + $max_reacted_users = 1; + $reacted_users = Db::$db->query( + 'SELECT m.id_member AS reacted_user, COUNT(r.content_id) AS count, mem.real_name + FROM {db_prefix}user_reacts AS r + INNER JOIN {db_prefix}messages AS m ON (r.content_id = m.id_msg) INNER JOIN {db_prefix}members AS mem ON (m.id_member = mem.id_member) WHERE content_type = {literal:msg} AND m.id_member > {int:zero} @@ -668,24 +668,24 @@ public function execute(): void ], ); - while ($row_liked_users = Db::$db->fetch_assoc($liked_users)) { - Utils::$context['stats_blocks']['liked_users'][] = [ - 'id' => $row_liked_users['liked_user'], - 'num' => $row_liked_users['count'], - 'href' => Config::$scripturl . '?action=profile;u=' . $row_liked_users['liked_user'], - 'name' => $row_liked_users['real_name'], - 'link' => '' . $row_liked_users['real_name'] . '', + while ($row_reacted_users = Db::$db->fetch_assoc($reacted_users)) { + Utils::$context['stats_blocks']['reacted_users'][] = [ + 'id' => $row_reacted_users['reacted_user'], + 'num' => $row_reacted_users['count'], + 'href' => Config::$scripturl . '?action=profile;u=' . $row_reacted_users['reacted_user'], + 'name' => $row_reacted_users['real_name'], + 'link' => '' . $row_reacted_users['real_name'] . '', ]; - if ($max_liked_users < $row_liked_users['count']) { - $max_liked_users = $row_liked_users['count']; + if ($max_reacted_users < $row_reacted_users['count']) { + $max_reacted_users = $row_reacted_users['count']; } } - Db::$db->free_result($liked_users); + Db::$db->free_result($reacted_users); - foreach (Utils::$context['stats_blocks']['liked_users'] as $i => $liked_users) { - Utils::$context['stats_blocks']['liked_users'][$i]['percent'] = round(($liked_users['num'] * 100) / $max_liked_users); + foreach (Utils::$context['stats_blocks']['reacted_users'] as $i => $reacted_users) { + Utils::$context['stats_blocks']['reacted_users'][$i]['percent'] = round(($reacted_users['num'] * 100) / $max_reacted_users); } } diff --git a/Sources/Db/Schema/v3_0/Messages.php b/Sources/Db/Schema/v3_0/Messages.php index 6963693f9bd..2548cf4994d 100644 --- a/Sources/Db/Schema/v3_0/Messages.php +++ b/Sources/Db/Schema/v3_0/Messages.php @@ -181,8 +181,8 @@ public function __construct() not_null: true, default: 1, ), - 'likes' => new Column( - name: 'likes', + 'reactions' => new Column( + name: 'reactions', type: 'smallint', unsigned: true, not_null: true, diff --git a/Sources/Db/Schema/v3_0/Reactions.php b/Sources/Db/Schema/v3_0/Reactions.php new file mode 100644 index 00000000000..bb2233118a1 --- /dev/null +++ b/Sources/Db/Schema/v3_0/Reactions.php @@ -0,0 +1,67 @@ +name = 'reactions'; + + $this->columns = [ + 'id_reaction' => new Column( + name: 'id_reaction', + type: 'smallint', + unsigned: true, + not_null: true, + default: 0, + auto: true, + ), + 'name' => new Column( + name: 'name', + type: 'varchar', + size: 255, + not_null: true, + default: '', + ) + ]; + + $this->indexes = [ + 'primary' => new DbIndex( + type: 'primary', + columns: [ + [ + 'name' => 'id_reaction', + ], + ], + ), + ]; + } +} \ No newline at end of file diff --git a/Sources/Db/Schema/v3_0/UserLikes.php b/Sources/Db/Schema/v3_0/UserReacts.php similarity index 84% rename from Sources/Db/Schema/v3_0/UserLikes.php rename to Sources/Db/Schema/v3_0/UserReacts.php index 63d3ac54c8c..f595a8468ab 100644 --- a/Sources/Db/Schema/v3_0/UserLikes.php +++ b/Sources/Db/Schema/v3_0/UserReacts.php @@ -22,7 +22,7 @@ /** * Defines all the properties for a database table. */ -class UserLikes extends Table +class UserReacts extends Table { /**************** * Public methods @@ -33,7 +33,7 @@ class UserLikes extends Table */ public function __construct() { - $this->name = 'user_likes'; + $this->name = 'user_reacts'; $this->columns = [ 'id_member' => new Column( @@ -43,6 +43,13 @@ public function __construct() not_null: true, default: 0, ), + 'id_react' => new Column( + name: 'id_react', + type: 'smallint', + unsigned: true, + not_null: true, + default: 0 + ), 'content_type' => new Column( name: 'content_type', type: 'char', @@ -57,8 +64,8 @@ public function __construct() not_null: true, default: 0, ), - 'like_time' => new Column( - name: 'like_time', + 'react_time' => new Column( + name: 'react_time', type: 'int', unsigned: true, not_null: true, @@ -92,8 +99,8 @@ public function __construct() ], ], ), - 'liker' => new DbIndex( - name: 'liker', + 'reactor' => new DbIndex( + name: 'reactor', columns: [ [ 'name' => 'id_member', diff --git a/Sources/Forum.php b/Sources/Forum.php index 63f7124a514..02b35812225 100644 --- a/Sources/Forum.php +++ b/Sources/Forum.php @@ -143,9 +143,6 @@ class Forum 'jsoption' => [ '', Actions\ThemeSetOption::class, ], - 'likes' => [ - '', Actions\Like::class, - ], 'lock' => [ '', Actions\TopicLock::class, ], @@ -222,6 +219,9 @@ class Forum 'quickmod2' => [ '', Actions\QuickModerationInTopic::class, ], + 'reacts' => [ + '', Actions\React::class, + ], 'recent' => [ '', Actions\Recent::class, ], diff --git a/Sources/Maintenance/Migration/v3_0/UserReactions.php b/Sources/Maintenance/Migration/v3_0/UserReactions.php new file mode 100644 index 00000000000..6a4594b8dc1 --- /dev/null +++ b/Sources/Maintenance/Migration/v3_0/UserReactions.php @@ -0,0 +1,107 @@ +list_columns('messages'); + + // If the reactions column exists in the messages table, there's nothing to do + return !in_array('reactions', $cols); + } + + /** + * + */ + public function execute(): bool + { + // Does the user_likes table exist? + $table_exists = Db::$db->list_tables(false, '%user_likes'); + if (!empty($table_exists)) + { + // Rename the table + Db::$db->rename_table('{db_prefix}user_likes', '{db_prefix}user_reacts', true); + + // Add the new column + $tbl = new UserReacts(); + Db::$db->add_column('{db_prefix}user_reacts', $tbl->columns); + + // Default reaction is like for now + Db::$db->query('UPDATE {db_prefix}user_reacts SET id_react={int:one}', ['one' => 1]); + + // Rename the like_time column + Db::$db->change_column('{db_prefix}user_reacts', 'like_time', ['name' => 'react_time']); + + // Rename the index + Db::$db->rename_index('{db_prefix}user_reacts', 'idx_liker', 'idx_reactor'); + + // Rename the likes column in the messages table + Db::$db->remove_index('{db_prefix}messages', 'idx_likes'); + Db::$db->change_column('{db_prefix}messages', 'likes', ['name' => 'reactions']); + Db::$db->add_index('{db_prefix}messages', ['name' => 'idx_reacts', 'columns' => ['reactions']]); + + // Update user alert prefs + Db::$db->query('UPDATE {db_prefix}user_alert_prefs SET alert_pref = {string:msg_react} WHERE alert_pref = {string:msg_like}', ['msg_react' => 'msg_react', 'msg_like' => 'msg_iike']); + + // Update permissions + Db::$db->query('UPDATE {db_prefix}permissions SET permission = {string:r_perm} WHERE permission = {string:l_perm}', ['r_perm' => 'reactions_react', 'l_perm' => 'likes_like']); + + // Finally, the settting + Db::$db->query('UPDATE {db_prefix}settings SET variable = {string:r_set} WHERE variable = {string:l_set}', ['r_set' => 'enable_reacts', 'l_set' => 'enable_likes']); + } + + // Create the table + else + { + // Shortcuts are fun... + $table = new UserReacts; + $table->create(); + + // Add the reactions column and related index to the messages table + Db::$db->add_column('{db_prefix}messages', ['name' => 'reactions', 'type' => 'smallint', 'not_null' => true, 'default' => '0']); + Db::$db->add_index('{db_prefix}messages', ['name' => 'idx_messages_reactions', 'columns' => ['reactions']]); + } + + $reacts_table = new \SMF\Db\Schema\v3_0\Reactions(); + $reacts_table->create(); + + // Add our default reaction + Db::$db->insert('update', '{db_prefix}reactions', ['id_reaction', 'name'], [1, 'like'], []); + + return true; + } +} \ No newline at end of file diff --git a/Sources/Msg.php b/Sources/Msg.php index d70493420d2..de01e107e75 100644 --- a/Sources/Msg.php +++ b/Sources/Msg.php @@ -162,9 +162,9 @@ class Msg implements \ArrayAccess, Routable /** * @var int * - * The number of likes this message has received. + * The number of reactions this message has received. */ - public int $likes = 0; + public int $reactions = 0; /** * @var bool @@ -216,6 +216,13 @@ class Msg implements \ArrayAccess, Routable */ public static $getter; + /** + * @var array + * + * Variable to hold info about how many of each reaction we have + */ + public static $reacts_count = []; + /********************* * Internal properties *********************/ @@ -307,7 +314,7 @@ public function save(): void 'icon' => 'string-16', 'smileys_enabled' => 'int', 'approved' => 'int', - 'likes' => 'int', + 'reactions' => 'int', 'version' => 'string-5', ]; @@ -325,7 +332,7 @@ public function save(): void $this->icon, (int) $this->smileys_enabled, $this->approved, - $this->likes, + $this->reactions, $this->version, ]; @@ -382,7 +389,7 @@ public function save(): void 'icon = {string:icon}', 'smileys_enabled = {int:smileys_enabled}', 'approved = {int:approved}', - 'likes = {int:likes}', + 'reactions = {int:reactions}', 'version = {string:version}', ]; @@ -402,7 +409,7 @@ public function save(): void 'icon' => (string) $this->icon, 'smileys_enabled' => (int) $this->smileys_enabled, 'approved' => (int) $this->approved, - 'likes' => (int) $this->likes, + 'reactions' => (int) $this->reactions, 'version' => (string) $this->version, ]; @@ -497,7 +504,8 @@ public function format(int $counter = 0, array $format_options = []): array 'body' => $this->body ?? '', 'new' => empty($this->is_read), 'first_new' => isset(Utils::$context['start_from']) && Utils::$context['start_from'] == $counter, - 'is_ignored' => !empty(Config::$modSettings['enable_buddylist']) && !empty(Theme::$current->options['posts_apply_ignore_list']) && \in_array($this->id_member, User::$me->ignoreusers), + 'is_ignored' => !empty(Config::$modSettings['enable_buddylist']) && !empty(Theme::$current->options['posts_apply_ignore_list']) && in_array($this->id_member, User::$me->ignoreusers), + 'num_reactions' => (int) $this->reactions, ]; // Are we showing the icon? @@ -656,15 +664,38 @@ public function format(int $counter = 0, array $format_options = []): array $this->formatted['short_subject'] = Utils::shorten($this->formatted['subject'], $format_options['shorten_subject']); } - // Are likes enabled? - if (!empty(Config::$modSettings['enable_likes'])) { - $this->formatted['likes'] = [ - 'count' => $this->likes, - 'you' => \in_array($this->id, Utils::$context['my_likes'] ?? []), - ]; + // Are reactions enabled? + if (!empty(Config::$modSettings['enable_reacts'])) { + $this->formatted['reacts'] = [ + 'count' => $this->reactions, + 'you' => in_array($this->id, Utils::$context['my_reactions'] ?? []), + ];; + + if ($this->reactions != 0) { + // Load up the number of each type of reactions + $query = Db::$db->query( + '', + 'SELECT id_react, COUNT(*) AS num_reacts + FROM {db_prefix}user_reacts + WHERE content_type = {string:content_type} + AND content_id = {int:content_id} + GROUP BY id_react + ORDER BY num_reacts DESC', + [ + 'content_type' => 'msg', + 'content_id' => $this->id, + ] + ); + + // Loop through the results + while ($row = Db::$db->fetchAssoc($query)) { + $this->reactions[$row['id_react']] = $row['num_reacts']; + } + Db::$db->freeResult($query); + } if ($format_options['do_permissions']) { - $this->formatted['likes']['can_like'] = !User::$me->is_guest && $this->id_member != User::$me->id && !empty($topic->permissions['can_like']); + $this->formatted['reactions']['can_react'] = !User::$me->is_guest && $this->id_member != User::$me->id && !empty($topic->permissions['can_react']); } } @@ -2803,6 +2834,17 @@ public static function remove(int $message, bool $decreasePostCount = true): boo ], ); + // Drop any reactions related to this post. We can recalculate stats later + Db::$db->query( + '', + 'DELETE FROM {db_prefix}user_reacts + WHERE content_type = {string:msg} AND content_id = {int:id_msg}', + [ + 'msg' => 'msg', + 'id_msg' => $message, + ], + ); + // Delete attachment(s) if they exist. $attachmentQuery = [ 'attachment_type' => Attachment::TYPE_STANDARD, @@ -2812,7 +2854,7 @@ public static function remove(int $message, bool $decreasePostCount = true): boo Attachment::remove($attachmentQuery); } - // Allow mods to remove message related data of their own (likes, maybe?) + // Allow mods to remove message related data of their own (reactions, maybe?) IntegrationHook::call('integrate_remove_message', [$message, $row, $recycle]); // Update the pesky statistics. diff --git a/Sources/ReactionTrait.php b/Sources/ReactionTrait.php new file mode 100644 index 00000000000..90655ea442f --- /dev/null +++ b/Sources/ReactionTrait.php @@ -0,0 +1,49 @@ +query( + 'SELECT * FROM {db_prefix}reactions', + []); + + while ($result = Db::$db->fetch_assoc($request)) { + $reactions[$result['id_reaction']] = $result['name']; + } + + Db::$db->free_result($request); + + // Cache the results + CacheApi::put('reactions', $reactions, 480); + } + return $reactions; + } +} \ No newline at end of file diff --git a/Sources/ServerSideIncludes.php b/Sources/ServerSideIncludes.php index 73c98426b02..da0999db6be 100644 --- a/Sources/ServerSideIncludes.php +++ b/Sources/ServerSideIncludes.php @@ -772,7 +772,7 @@ public static function queryPosts( if (!empty(Config::$modSettings['enable_likes'])) { $posts[$row['id_msg']]['likes'] = [ 'count' => $row['likes'], - 'you' => \in_array($row['id_msg'], $topic->getLikedMsgs()), + 'you' =>\in_array($row['id_msg'], $topic->getReactedMsgs()), 'can_like' => !User::$me->is_guest && $row['id_member'] != User::$me->id && !empty(Utils::$context['can_like']), ]; } @@ -2485,7 +2485,7 @@ public static function boardNews(?int $board = null, ?int $limit = null, ?int $s // Nasty ternary for likes not messing around the "is_last" check. 'likes' => !empty(Config::$modSettings['enable_likes']) ? [ 'count' => $row['likes'], - 'you' => \in_array($row['id_msg'], $topic->getLikedMsgs()), + 'you' => in_array($row['id_msg'], $topic->getLikedMsgs()), 'can_like' => !User::$me->is_guest && $row['id_member'] != User::$me->id && !empty(Utils::$context['can_like']), ] : [], ]; diff --git a/Sources/Topic.php b/Sources/Topic.php index cae63040523..57ceb4815c4 100644 --- a/Sources/Topic.php +++ b/Sources/Topic.php @@ -674,28 +674,29 @@ public function getNotificationPrefs(): array } /** - * Gets the IDs of messages in this topic that the current user likes. + * Gets the IDs of messages in this topic that the current user reacted to + * as well as the ID of the chosen reaction for each message. * - * @return array IDs of messages in this topic that the current user likes. + * @return array An array of arrays each containing the ID of a reacted post and ID of the reaction */ - public function getLikedMsgs(): array + public function getReactedMsgs(): array { if (User::$me->is_guest) { return []; } - $cache_key = 'likes_topic_' . $this->id . '_' . User::$me->id; + $cache_key = 'reacts_topic_' . $this->id . '_' . User::$me->id; $ttl = 180; - if (($liked_messages = CacheApi::get($cache_key, $ttl)) === null) { - $liked_messages = []; + if (($reacted_messages = CacheApi::get($cache_key, $ttl)) === null) { + $reacted_messages = []; $request = Db::$db->query( - 'SELECT content_id - FROM {db_prefix}user_likes AS l - INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg) - WHERE l.id_member = {int:current_user} - AND l.content_type = {literal:msg} + 'SELECT r.content_id, r.id_reaction + FROM {db_prefix}user_reacts AS r + INNER JOIN {db_prefix}messages AS m ON (r.content_id = m.id_msg) + WHERE r.id_member = {int:current_user} + AND r.content_type = {literal:msg} AND m.id_topic = {int:topic}', [ 'current_user' => User::$me->id, @@ -704,14 +705,14 @@ public function getLikedMsgs(): array ); while ($row = Db::$db->fetch_assoc($request)) { - $liked_messages[] = (int) $row['content_id']; + $reacted_messages[] = [(int) $row['content_id'], (int) $row['id_reaction']]; } Db::$db->free_result($request); - CacheApi::put($cache_key, $liked_messages, $ttl); + CacheApi::put($cache_key, $reacted_messages, $ttl); } - return $liked_messages; + return $reacted_messages; } /** @@ -1707,6 +1708,21 @@ public static function remove(array|int $topics, bool $decreasePostCount = true, Attachment::remove($attachmentQuery, 'messages'); // Delete anything related to the topic. + // Do this first because we need the message IDs... + Db::$db->query( + '', + 'DELETE FROM {db_prefix}user_reacts + WHERE content_type={string:msg} + AND content_id IN + (SELECT id_msg + FROM {db_prefix}messages + WHERE id_topic IN ({array_int:topics}) + )', + [ + 'msg' => 'msg', + 'topics' => $topics, + ] + ); Db::$db->query( 'DELETE FROM {db_prefix}messages WHERE id_topic IN ({array_int:topics})', diff --git a/Themes/default/Likes.template.php b/Themes/default/Reacts.template.php similarity index 97% rename from Themes/default/Likes.template.php rename to Themes/default/Reacts.template.php index ed381a260a0..002fd8f64e0 100644 --- a/Themes/default/Likes.template.php +++ b/Themes/default/Reacts.template.php @@ -57,12 +57,12 @@ function template_popup() /** * Display a like button and info about how many people liked something */ -function template_like() +function template_react() { echo '