Procházet zdrojové kódy

Tickets #5026, #5032, #5033 - Connections flow improvements. Friends and Subscriptions.

AntonLV před 1 rokem
rodič
revize
6155011188

+ 5 - 1
inc/classes/BxDolConnection.php

@@ -129,10 +129,13 @@ define('BX_CONNECTIONS_TRIGGER_TYPE_CONTENT', 'content');
  */
 class BxDolConnection extends BxDolFactory implements iBxDolFactoryObject
 {
+    protected $_oQuery;
+
     protected $_sObject;
     protected $_aObject;
-    protected $_oQuery;
     protected $_sType;
+    protected $_bMutual;
+
     protected $_aTriggerTypes;
 
     /**
@@ -148,6 +151,7 @@ class BxDolConnection extends BxDolFactory implements iBxDolFactoryObject
         $this->_aObject['per_page_default'] = 20;
 
         $this->_sType = $aObject['type'];
+        $this->_bMutual = $this->_sType == BX_CONNECTIONS_TYPE_MUTUAL;
 
         $this->_aTriggerTypes = [BX_CONNECTIONS_TRIGGER_TYPE_INITIATOR, BX_CONNECTIONS_TRIGGER_TYPE_CONTENT];
 

+ 16 - 0
inc/classes/BxDolConnectionFriend.php

@@ -0,0 +1,16 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolConnectionFriend extends BxTemplConnection
+{
+    protected function __construct($aObject)
+    {
+        parent::__construct($aObject);
+    }
+}

+ 16 - 0
inc/classes/BxDolConnectionSubscription.php

@@ -0,0 +1,16 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolConnectionSubscription extends BxTemplConnection
+{
+    protected function __construct($aObject)
+    {
+        parent::__construct($aObject);
+    }
+}

+ 51 - 5
modules/base/profile/classes/BxBaseModProfileMenuViewActionsAll.php

@@ -50,6 +50,41 @@ class BxBaseModProfileMenuViewActionsAll extends BxBaseModGeneralMenuViewActions
         return '';
     }
 
+    protected function _getMenuItemConnection($aItem, $aParams = [])
+    {
+        $sObject = !empty($aParams['object']) ? $aParams['object'] : 'sys_profiles_friends';
+
+        $iId = !empty($aParams['id']) ? (int)$aParams['id'] : '';
+        if(empty($iId))
+            $iId = $this->_aProfileInfo['id'];
+
+        $oObject = !empty($sObject) ? BxDolConnection::getObjectInstance($sObject) : false;
+        if(!$oObject)
+            return '';
+
+        $aObjectOptions = [
+            'dynamic_mode' => $this->_bDynamicMode,
+            'show_do_as_button' => $this->_bShowAsButton,
+            'show_do_label' => $this->_bShowTitle
+        ];
+        if(!empty($aParams['object_options']) && is_array($aParams['object_options']))
+            $aObjectOptions = array_merge($aObjectOptions, $aParams['object_options']);
+
+        if($this->_bIsApi)
+            return [
+                'id' => $aItem['id'],
+                'name' => $aItem['name'],
+                'display_type' => 'element',
+                'data' => $oObject->getElementApi($aObjectOptions)
+            ];
+
+        $sResult = $oObject->getElement($iId, false, $aObjectOptions);
+        if(empty($sResult))
+            return '';
+
+    	return [$sResult, $this->_sClassMiSa];
+    }
+
     protected function _getMenuItemConnectionApi($sConnection, $sAction, &$aItem)
     {
         if(!isLogged() || (isset($this->_aConnectionToFunctionCheck[$sConnection]) && $this->_oModule->{$this->_aConnectionToFunctionCheck[$sConnection][$sAction]}($this->_aContentInfo) !== CHECK_ACTION_RESULT_ALLOWED))
@@ -76,13 +111,25 @@ class BxBaseModProfileMenuViewActionsAll extends BxBaseModGeneralMenuViewActions
                 'iid' => $iInitiatorProfile,
                 'cid' => $iContentProfile,
                 'title' => $sTitle,
-                'primary' => $aItem['primary'],
-                
-            ],
-            'persistent' => $aItem['persistent']
+                'primary' => !empty($aItem['primary']),
+            ]
         ];
     }
 
+    protected function _getMenuItemProfileFriends($aItem, $aParams = [])
+    {
+        return $this->_getMenuItemConnection($aItem, array_merge($aParams, [
+            'object' => 'sys_profiles_friends'
+        ]));
+    }
+
+    protected function _getMenuItemProfileSubscriptions($aItem, $aParams = [])
+    {
+        return $this->_getMenuItemConnection($aItem, array_merge($aParams, [
+            'object' => 'sys_profiles_subscriptions'
+        ]));
+    }
+
     protected function _getMenuItemProfileFriendAdd($aItem)
     {
         if($this->_bIsApi)
@@ -245,7 +292,6 @@ class BxBaseModProfileMenuViewActionsAll extends BxBaseModGeneralMenuViewActions
                 'on_callback' => 'redirect'
             ],
             'primary' => $aItem['primary'],
-            'persistent' => $aItem['persistent'],
         ];
     }
 }

+ 28 - 10
modules/base/profile/classes/BxBaseModProfileMenuViewMeta.php

@@ -109,7 +109,7 @@ class BxBaseModProfileMenuViewMeta extends BxTemplMenuUnitMeta
 
         if(!$this->_oModule->_oConfig->isFriends() || !$this->_bContentPublic || !$this->_oContentProfile)
             return false;
-        
+
         $oConnection = BxDolConnection::getObjectInstance('sys_profiles_friends');
         if(!$oConnection)
             return false;
@@ -117,7 +117,10 @@ class BxBaseModProfileMenuViewMeta extends BxTemplMenuUnitMeta
         $iContentProfileId = $this->_oContentProfile->id();
 
         if($this->_bIsApi) {
-            $aCounter = $oConnection->getCounterAPI($iContentProfileId, true, ['caption' => $aItem['title']], BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+            $aCounter = $oConnection->getCounterAPI($iContentProfileId, true, [
+                'content_type' => BX_CONNECTIONS_CONTENT_TYPE_INITIATORS, 
+                'caption' => $aItem['title']
+            ]);
 
             $sUrl = $this->_oContentProfile->getUrl();
             if(!empty($CNF['URI_VIEW_FRIENDS']))
@@ -130,8 +133,11 @@ class BxBaseModProfileMenuViewMeta extends BxTemplMenuUnitMeta
             ]);
         }
 
-        $sIcon = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '');
-        return $oConnection->getCounter($iContentProfileId, true, ['caption' => $aItem['title'], 'custom_icon' => $sIcon], BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+        return $oConnection->getCounter($iContentProfileId, true, [
+            'content_type' => BX_CONNECTIONS_CONTENT_TYPE_INITIATORS, 
+            'caption' => $aItem['title'], 
+            'custom_icon' => BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '')
+        ]);
     }
 
     protected function _getMenuItemSubscribers($aItem)
@@ -148,7 +154,10 @@ class BxBaseModProfileMenuViewMeta extends BxTemplMenuUnitMeta
         $iContentProfileId = $this->_oContentProfile->id();
 
         if($this->_bIsApi) {
-            $aCounter = $oConnection->getCounterAPI($iContentProfileId, false, ['caption' => $aItem['title']], BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+            $aCounter = $oConnection->getCounterAPI($iContentProfileId, false, [
+                'content_type' => BX_CONNECTIONS_CONTENT_TYPE_INITIATORS, 
+                'caption' => $aItem['title']
+            ]);
 
             $sUrl = $this->_oContentProfile->getUrl();
             if(!empty($CNF['URI_VIEW_SUBSCRIPTIONS']))
@@ -161,8 +170,11 @@ class BxBaseModProfileMenuViewMeta extends BxTemplMenuUnitMeta
             ]);
         }
 
-        $sIcon = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '');
-        return $oConnection->getCounter($iContentProfileId, false, ['caption' => $aItem['title'], 'custom_icon' => $sIcon], BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+        return $oConnection->getCounter($iContentProfileId, false, [
+            'content_type' => BX_CONNECTIONS_CONTENT_TYPE_INITIATORS, 
+            'caption' => $aItem['title'], 
+            'custom_icon' => BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '')
+        ]);
     }
 
     protected function _getMenuItemRelations($aItem)
@@ -179,7 +191,10 @@ class BxBaseModProfileMenuViewMeta extends BxTemplMenuUnitMeta
         $iContentProfileId = $this->_oContentProfile->id();
 
         if($this->_bIsApi) {
-            $aCounter = $oConnection->getCounterAPI($iContentProfileId, false, ['caption' => $aItem['title']], BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+            $aCounter = $oConnection->getCounterAPI($iContentProfileId, false, [
+                'content_type' => BX_CONNECTIONS_CONTENT_TYPE_INITIATORS, 
+                'caption' => $aItem['title']
+            ]);
 
             $sUrl = $this->_oContentProfile->getUrl();
             if(!empty($CNF['URI_VIEW_RELATIONS']))
@@ -192,8 +207,11 @@ class BxBaseModProfileMenuViewMeta extends BxTemplMenuUnitMeta
             ]);
         }
 
-        $sIcon = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '');
-        return $oConnection->getCounter($iContentProfileId, true, ['caption' => $aItem['title'], 'custom_icon' => $sIcon], BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+        return $oConnection->getCounter($iContentProfileId, true, [
+            'content_type' => BX_CONNECTIONS_CONTENT_TYPE_INITIATORS, 
+            'caption' => $aItem['title'], 
+            'custom_icon' => BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '')
+        ]);
     }
 
     protected function _getMenuItemViews($aItem)

+ 9 - 1
modules/boonex/english/data/langs/system/en.xml

@@ -2491,6 +2491,14 @@ If it is not enabled then please consider implement this optimization, since it
     <string name="_sys_close"><![CDATA[Close]]></string>
     <string name="_sys_confirm"><![CDATA[Confirm]]></string>
     <string name="_sys_confirmed"><![CDATA[Confirmed]]></string>
+    <string name="_sys_conn_do_by"><![CDATA[Connected by]]></string>
+    <string name="_sys_conn_friends_counter"><![CDATA[{0} friends]]></string>
+    <string name="_sys_conn_friends_do_initiator"><![CDATA[Friends]]></string>
+    <string name="_sys_conn_friends_do_content"><![CDATA[Respond]]></string>
+    <string name="_sys_conn_subscriptions_do_initiator"><![CDATA[Following]]></string>
+    <string name="_sys_conn_subscriptions_counter"><![CDATA[{0} followers]]></string>
+    <string name=""><![CDATA[]]></string>
+    <string name=""><![CDATA[]]></string>
     <string name="_sys_conn_err_connection_already_exists"><![CDATA[Connection already exists]]></string>
     <string name="_sys_conn_err_connection_does_not_exists"><![CDATA[Connection doesn't exist]]></string>
     <string name="_sys_conn_err_connection_is_awaiting_confirmation"><![CDATA[Connection request is awaiting confirmation]]></string>
@@ -3250,7 +3258,7 @@ If it is not enabled then please consider implement this optimization, since it
     <string name="_sys_menu_item_title_sm_befriend"><![CDATA[Add friend]]></string>
     <string name="_sys_menu_item_title_sm_befriend_sent"><![CDATA[Add friend (requested)]]></string>
     <string name="_sys_menu_item_title_sm_befriend_confirm"><![CDATA[Accept friend request]]></string>
-    <string name="_sys_menu_item_title_sm_unfriend"><![CDATA[Remove friend]]></string>
+    <string name="_sys_menu_item_title_sm_unfriend"><![CDATA[Unfriend]]></string>
     <string name="_sys_menu_item_title_sm_unfriend_cancel"><![CDATA[Cancel friend request]]></string>
     <string name="_sys_menu_item_title_sm_unfriend_reject"><![CDATA[Reject friend request]]></string>
     <string name="_sys_menu_item_title_sm_join_paid"><![CDATA[Pay & Join]]></string>

+ 6 - 4
modules/boonex/persons/install/sql/enable.sql

@@ -263,12 +263,14 @@ INSERT INTO `sys_menu_sets`(`set_name`, `module`, `title`, `deletable`) VALUES
 ('bx_persons_view_actions_all', 'bx_persons', '_sys_menu_set_title_view_actions', 0);
 
 INSERT INTO `sys_menu_items`(`set_name`, `module`, `name`, `title_system`, `title`, `link`, `onclick`, `target`, `icon`, `addon`, `submenu_object`, `submenu_popup`, `visible_for_levels`, `visibility_custom`, `active`, `copyable`, `order`) VALUES 
-('bx_persons_view_actions_all', 'bx_persons', 'profile-friend-add', '_bx_persons_menu_item_title_system_befriend', '', '', '', '', '', '', '', 0, 2147483647, '', 1, 0, 10),
-('bx_persons_view_actions_all', 'bx_persons', 'profile-friend-remove', '_bx_persons_menu_item_title_system_unfriend', '', '', '', '', '', '', '', 0, 2147483647, '', 1, 0, 20),
+('bx_persons_view_actions_all', 'bx_persons', 'profile-friends', '_bx_persons_menu_item_title_system_friends', '', '', '', '', '', '', '', 0, 2147483647, '', 1, 0, 10),
+('bx_persons_view_actions_all', 'bx_persons', 'profile-friend-add', '_bx_persons_menu_item_title_system_befriend', '', '', '', '', '', '', '', 0, 2147483647, '', 0, 0, 12),
+('bx_persons_view_actions_all', 'bx_persons', 'profile-friend-remove', '_bx_persons_menu_item_title_system_unfriend', '', '', '', '', '', '', '', 0, 2147483647, '', 0, 0, 14),
 ('bx_persons_view_actions_all', 'bx_persons', 'profile-relation-add', '_bx_persons_menu_item_title_system_relation_add', '', '', '', '', '', '', '', 0, 2147483647, '', 1, 0, 23),
 ('bx_persons_view_actions_all', 'bx_persons', 'profile-relation-remove', '_bx_persons_menu_item_title_system_relation_delete', '', '', '', '', '', '', '', 0, 2147483647, '', 1, 0, 27),
-('bx_persons_view_actions_all', 'bx_persons', 'profile-subscribe-add', '_bx_persons_menu_item_title_system_subscribe', '', '', '', '', '', '', '', 0, 2147483647, '', 1, 0, 30),
-('bx_persons_view_actions_all', 'bx_persons', 'profile-subscribe-remove', '_bx_persons_menu_item_title_system_unsubscribe', '', '', '', '', '', '', '', 0, 2147483647, '', 1, 0, 40),
+('bx_persons_view_actions_all', 'bx_persons', 'profile-subscriptions', '_bx_persons_menu_item_title_system_subscriptions', '', '', '', '', '', '', '', 0, 2147483647, '', 1, 0, 30),
+('bx_persons_view_actions_all', 'bx_persons', 'profile-subscribe-add', '_bx_persons_menu_item_title_system_subscribe', '', '', '', '', '', '', '', 0, 2147483647, '', 0, 0, 32),
+('bx_persons_view_actions_all', 'bx_persons', 'profile-subscribe-remove', '_bx_persons_menu_item_title_system_unsubscribe', '', '', '', '', '', '', '', 0, 2147483647, '', 0, 0, 34),
 ('bx_persons_view_actions_all', 'bx_persons', 'profile-set-acl-level', '_sys_menu_item_title_system_set_acl_level', '', '', '', '', '', '', '', 0, 2147483647, '', 1, 0, 50),
 ('bx_persons_view_actions_all', 'bx_persons', 'profile-set-badges', '_sys_menu_item_title_system_set_badges', '', '', '', '', '', '', '', 0, 192, 'a:2:{s:6:"module";s:10:"bx_persons";s:6:"method";s:19:"is_badges_avaliable";}', 1, 0, 60),
 ('bx_persons_view_actions_all', 'bx_persons', 'comment', '_sys_menu_item_title_system_va_comment', '', '', '', '', '', '', '', 0, 2147483647, '', 0, 0, 200),

+ 8 - 0
modules/boonex/russian/data/langs/system/ru.xml

@@ -2466,6 +2466,14 @@
     <string name="_sys_close"><![CDATA[Закрыть]]></string>
     <string name="_sys_confirm"><![CDATA[Подтвердить]]></string>
     <string name="_sys_confirmed"><![CDATA[Подтвержденный]]></string>
+    <string name="_sys_conn_do_by"><![CDATA[Присоединились]]></string>
+    <string name="_sys_conn_friends_counter"><![CDATA[Друзья: {0}]]></string>
+    <string name="_sys_conn_friends_do_initiator"><![CDATA[Друзья]]></string>
+    <string name="_sys_conn_friends_do_content"><![CDATA[Ответить]]></string>
+    <string name="_sys_conn_subscriptions_do_initiator"><![CDATA[Слежу]]></string>
+    <string name="_sys_conn_subscriptions_counter"><![CDATA[Подписчики: {0}]]></string>
+    <string name=""><![CDATA[]]></string>
+    <string name=""><![CDATA[]]></string>
     <string name="_sys_conn_err_connection_already_exists"><![CDATA[Связь уже существует]]></string>
     <string name="_sys_conn_err_connection_does_not_exists"><![CDATA[Связь не существует]]></string>
     <string name="_sys_conn_err_connection_is_awaiting_confirmation"><![CDATA[Запрос на связь ожидает подтверждения]]></string>

+ 29 - 0
template/connection_element.html

@@ -0,0 +1,29 @@
+<div id="__html_id__" class="sys-action __style_prefix__ __class__ flex items-center whitespace-nowrap">
+    <div class="sys-action-cnt __style_prefix__-cnt __style_prefix__-do bx-def-valign-center bx-clearfix">
+        <bx_if:show_do>
+            <div class="sys-action-element-holder __style_prefix__-element-holder">
+                <bx_if:show_action>
+                    <a class="__style_prefix__-do__class__ flex" href="javascript:void(0)" onclick="javascript:bx_conn_action(this, '__object__', '__action__', __profile_id__)">
+                        <div class="sys-action-do __style_prefix__-do bx-def-valign-center">
+                            <bx_if:show_icon><span class="sys-action-do-icon __style_prefix__-do-icon bx-def-align-center">__icon__</span></bx_if:show_icon>
+                            <bx_if:show_text><span class="sys-action-do-text __style_prefix__-do-text bx-def-margin-sec-left-auto bx-dir">__text__</span></bx_if:show_text>
+                        </div>
+                    </a>
+                </bx_if:show_action>
+                <bx_if:show_actions>
+                    <a class="__style_prefix__-do__class__ flex" href="javascript:void(0)" onclick="javascript:bx_menu_popup_inline('#__html_id_popup__', this)">
+                        <div class="sys-action-do __style_prefix__-do bx-def-valign-center">
+                            <bx_if:show_icon><span class="sys-action-do-icon __style_prefix__-do-icon bx-def-align-center">__icon__</span></bx_if:show_icon>
+                            <bx_if:show_text><span class="sys-action-do-text __style_prefix__-do-text bx-def-margin-sec-left-auto bx-dir">__text__</span></bx_if:show_text>
+                        </div>
+                    </a>
+                    __do_popup__
+                </bx_if:show_actions>
+            </div>
+        </bx_if:show_do>
+        <bx_if:show_counter>
+            <div class="sys-action-counter-holder __style_prefix__-counter-holder ltr:ml-2 ltr:first:ml-0 rtl:mr-2 rtl:first:mr-0">__counter__</div>
+        </bx_if:show_counter>
+    </div>
+    __script__
+</div>

+ 279 - 91
template/scripts/BxBaseConnection.php

@@ -12,20 +12,60 @@
  */
 class BxBaseConnection extends BxDolConnection
 {
+    protected $_oTemplate;
+    protected $_oFunctions;
+
     protected $_sStylePrefix;
     protected $_sJsObjName;
+
+    protected $_sHp;
     protected $_aHtmlIds;
-    protected $_oTemplate;
     
+    protected $_aT;
+
+    protected $_sTmplContentElement;
+    protected $_sTmplContentCounter;
+
+    protected $_aElementDefaults;
+
     public function __construct($aObject)
     {
         parent::__construct($aObject);
-        
-        $this->_sStylePrefix = 'bx-conn';
-        $this->_sJsObjName = 'oConn' . bx_gen_method_name($this->_sObject, array('_' , '-')) . '';
+
         $this->_oTemplate = BxDolTemplate::getInstance();
+        $this->_oFunctions = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate);
+
+        $this->_sStylePrefix = 'bx-conn';
+        $this->_sJsObjName = 'oConn' . bx_gen_method_name($this->_sObject, ['_' , '-']) . '';
+
+        $this->_sHp = str_replace('_', '-', $this->_sObject);
+        $this->_aHtmlIds = [
+            'main' => 'bx-conn-' . $this->_sHp . '-',
+            'do_popup' => 'bx-conn-do-popup-' . $this->_sHp . '-',
+            'do_menu' => 'bx-conn-do-menu-' . $this->_sHp . '-',
+            'counter' => 'bx-conn-counter-' . $this->_sHp . '-',
+            'by_popup' => 'bx-conn-by-popup-' . $this->_sHp . '-',
+        ];
+
+        $this->_aT = [
+            'do_by' => '_sys_conn_do_by'
+        ];
+
+        $this->_sTmplContentElement = $this->_oTemplate->getHtml('connection_element.html');
+        $this->_sTmplContentCounter = $this->_oTemplate->getHtml('connection_counter.html');
+
+        $this->_aElementDefaults = [
+            'dynamic_mode' => true,
+            'show_do_as_button_small' => false,
+            'show_do_as_button' => false,
+            'show_do_icon' => true,
+            'show_do_label' => true,
+            'show_counter' => false,
+            'show_counter_label_with_profiles' => true,
+            'show_script' => true
+        ];
     }
-    
+
     public function actionGetConnected ($iStart = 0, $iPerPage = 0)
     {
         $sContentType = bx_process_input(bx_get('content_type'));
@@ -52,94 +92,183 @@ class BxBaseConnection extends BxDolConnection
     {
         return $this->_sJsObjName . $iProfileId;
     }
-    
-    public function getCounter ($iProfileId, $bIsMutual, $aParams, $sContentType)
+
+    public function getElement($iContent, $iInitiator = false, $aParams = [])
     {
-        if (!$iProfileId)
-            $iProfileId = bx_get_logged_profile_id();
+    	$aParams = array_merge($this->_aElementDefaults, $aParams);
+    	$bDynamicMode = isset($aParams['dynamic_mode']) && $aParams['dynamic_mode'] === true;
 
-        if ('mutual' != $this->_aObject['type'])
-            $bIsMutual = false;
+        $bShowDo = !isset($aParams['show_do']) || (bool)$aParams['show_do'] === true;
+        $bShowDoAsButtonSmall = isset($aParams['show_do_as_button_small']) && $aParams['show_do_as_button_small'] == true;
+        $bShowDoAsButton = !$bShowDoAsButtonSmall && isset($aParams['show_do_as_button']) && $aParams['show_do_as_button'] == true;
+        $bShowDoLabel = isset($aParams['show_do_label']) && $aParams['show_do_label'] === true;
+        $bShowCounter = isset($aParams['show_counter']) && $aParams['show_counter'] === true;
 
-        if(empty($aParams))
-            $aParams = [];
+        if(!$iInitiator)
+            $iInitiator = bx_get_logged_profile_id();
 
-        $this->_sStylePrefix = 'bx-conn';
-        
-        $aHtmlIds = array(
-            'main' => 'bx-conn-' . $this->_sObject . $iProfileId,
-            'counter' => 'bx-conn-counter-' . $this->_sObject . $iProfileId,
-            'by_popup' => 'bx-conn-by-popup-' . $this->_sObject . $iProfileId,
-        );
-        
-        $aParamsDefault = [
-            'dynamic_mode' => true,
-            'show_do_as_button_small' => false,
-            'show_do_as_button' => false,
-            'show_counter_label_with_profiles' => true,
-            'show_script' => true,
-        ];
-        $aParams = array_merge($aParams, $aParamsDefault);
+        $aActions = $this->_getActions($iInitiator, $iContent, $aParams);
 
-        $iCount = $sContentType == BX_CONNECTIONS_CONTENT_TYPE_INITIATORS ? $this->getConnectedInitiatorsCount($iProfileId, $bIsMutual) : $this->getConnectedContentCount($iProfileId, $bIsMutual);
-        
-        if ($iCount > 0){   
-            $sJsObject = $this->getJsObjectName($iProfileId);
-
-            $bDynamicMode = isset($aParams['dynamic_mode']) && $aParams['dynamic_mode'] === true;
-            $bShowDoAsButtonSmall = isset($aParams['show_do_as_button_small']) && $aParams['show_do_as_button_small'] === true;
-            $bShowDoAsButton = !$bShowDoAsButtonSmall && isset($aParams['show_do_as_button']) && $aParams['show_do_as_button'] === true;
-            $bShowScript = !isset($aParams['show_script']) || $aParams['show_script'] == true;
-
-            $sClass = $this->_sStylePrefix . '-counter';
-            if($bShowDoAsButtonSmall)
-                $sClass .= ' bx-btn-small-height';
-            if($bShowDoAsButton)
-                $sClass .= ' bx-btn-height';
-        
-            $sContent = $this->_getCounterLabel($iCount, $iProfileId, $bIsMutual, $aParams, $sContentType);
+        $iCount = $this->_getCounterCount($iContent, $this->_bMutual, $aParams);
+
+        if((empty($aActions['items']) || !is_array($aActions['items'])) && (!$bShowCounter || !$iCount))
+            return '';        
+
+        $sClassDo = '';
+        if($bShowDoAsButton)
+            $sClassDo = ' bx-btn';
+        else if ($bShowDoAsButtonSmall)
+            $sClassDo = ' bx-btn bx-btn-small';
+
+        $aTmplVarsAction = $aTmplVarsActions = [];
+        if(count($aActions['items']) == 1) {
+            $aAction = reset($aActions['items']);
+
+            $aTmplVarsAction = array_merge([
+                'style_prefix' => $this->_sStylePrefix,
+                'class' => $sClassDo,
+                'object' => $this->_sObject, 
+                'action' => $aAction['name'],
+                'profile_id' => $iContent,
+                'title' => $bShowDoLabel && !empty($aAction['title']) ? _t($aAction['title']) : ''
+            ], $this->_getTmplVarsAction($aAction));
+        }
+        else {
+            $sHtmlIdDoPopup = $this->_aHtmlIds['do_popup'] . $iContent;
+            $sHtmlIdDoMenu = $this->_aHtmlIds['do_menu'] . $iContent;
+
+            $aMenuItems = [];
+            foreach($aActions['items'] as $aItem)
+                if(!empty($aItem) && is_array($aItem))
+                    $aMenuItems[] = [
+                        'id' => $aItem['name'], 
+                        'name' => $aItem['name'], 
+                        'class' => '', 
+                        'icon' => $this->_getActionIcon($aItem, $aParams), 
+                        'link' => 'javascript:void(0)', 
+                        'onclick' => 'javascript:bx_conn_action(this, \'' . $this->_sObject . '\', \'' . $aItem['name'] . '\', ' . $iContent . ')', 
+                        'target' => '_self', 
+                        'title' => !empty($aItem['title']) ? _t($aItem['title']) : '', 
+                        'active' => 1
+                    ];
 
-            $this->_oTemplate->addCss(array('connection.css'));
-            return $this->_oTemplate->parseHtmlByName('connection_counter.html', array(
-                'html_id' => $aHtmlIds['counter'],
+            $oMenu = new BxTemplMenu(['template' => 'menu_vertical.html', 'menu_id'=> $sHtmlIdDoMenu, 'menu_items' => $aMenuItems]);
+
+            $aTmplVarsActions = array_merge([
                 'style_prefix' => $this->_sStylePrefix,
-                'bx_if:show_text' => array(
-                    'condition' => false,
-                    'content' => array(
-                        'class' => $sClass,
-                        'bx_repeat:attrs' => array(
-                            array('key' => 'id', 'value' => $aHtmlIds['counter']),
-                        ),
-                        'content' => $sContent
-                    )
-                ),
-                'bx_if:show_link' => array(
-                    'condition' => true,
-                    'content' => array(
-                        'class' => $sClass,
-                        'bx_repeat:attrs' => array(
-                            array('key' => 'id', 'value' => $aHtmlIds['counter']),
-                            array('key' => 'href', 'value' => 'javascript:void(0)'),
-                            array('key' => 'onclick', 'value' => 'javascript:' . $sJsObject . '.toggleByPopup(this)'),
-                            array('key' => 'title', 'value' => _t('_view_do_view_by'))
-                        ),
-                        'content' => $sContent
-                    )
-                ),
-                'script' => $bShowScript ? $this->_getJsScript($iProfileId, $sContentType, $bIsMutual, $aHtmlIds, $bDynamicMode) : ''
-            ));
+                'class' => $sClassDo,
+                'html_id_popup' => $sHtmlIdDoPopup,
+                'do_popup' => $this->_oFunctions->transBox($sHtmlIdDoPopup, $oMenu->getCode(), true),
+            ], $this->_getTmplVarsAction([
+                'name' => $aActions['name'],
+                'title' => $bShowDoLabel && !empty($aActions['title']) ? $aActions['title'] : ''
+            ]));
         }
+
+        $bTmplVarsAction = !empty($aTmplVarsAction) && is_array($aTmplVarsAction);
+        $bTmplVarsActions = !empty($aTmplVarsActions) && is_array($aTmplVarsActions);
+
+        return $this->_oTemplate->parseHtmlByContent($this->_getTmplContentElement(), [
+            'style_prefix' => $this->_sStylePrefix,
+            'html_id' => $this->_aHtmlIds['main'] . $iContent,
+            'class' => $this->_sStylePrefix . ($bShowDoAsButton ? '-button' : '-link') . ($bShowDoAsButtonSmall ? '-button-small' : ''),
+            'bx_if:show_do' => [
+                'condition' => $bShowDo && ($bTmplVarsAction || $bTmplVarsActions),
+                'content' => [
+                    'style_prefix' => $this->_sStylePrefix,
+                    'bx_if:show_action' => [
+                        'condition' => $bTmplVarsAction,
+                        'content' => $aTmplVarsAction
+                    ],
+                    'bx_if:show_actions' => [
+                        'condition' => $bTmplVarsActions,
+                        'content' => $aTmplVarsActions
+                    ],
+                ]
+            ],
+            'bx_if:show_counter' => [
+                'condition' => $bShowCounter && $iCount,
+                'content' => [
+                    'style_prefix' => $this->_sStylePrefix,
+                    'counter' => $this->getCounter($iContent, $this->_bMutual, array_merge($aParams, ['count' => $iCount]))
+                ]
+            ],
+            'script' => $this->_getJsScript($iContent, $this->_bMutual, $aParams)
+        ]);
+    }
+
+    public function getCounter($iProfileId, $bIsMutual = false, $aParams = [])
+    {
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        if($this->_aObject['type'] != 'mutual')
+            $bIsMutual = false;
+
+        $aParams = array_merge($this->_aElementDefaults, (!empty($aParams) ? $aParams : []));
+
+        $iCount = !empty($aParams['count']) ? $aParams['count'] : $this->_getCounterCount($iProfileId, $bIsMutual, $aParams);
+        if(!$iCount)
+            return '';
+
+        $sJsObject = $this->getJsObjectName($iProfileId);
+        $sHtmlIdCounter = $this->_aHtmlIds['counter'] . $iProfileId;
+
+        $bDynamicMode = isset($aParams['dynamic_mode']) && $aParams['dynamic_mode'] === true;
+        $bShowDoAsButtonSmall = isset($aParams['show_do_as_button_small']) && $aParams['show_do_as_button_small'] === true;
+        $bShowDoAsButton = !$bShowDoAsButtonSmall && isset($aParams['show_do_as_button']) && $aParams['show_do_as_button'] === true;
+        $bShowScript = !isset($aParams['show_script']) || $aParams['show_script'] == true;
+
+        $sClass = $this->_sStylePrefix . '-counter';
+        if($bShowDoAsButtonSmall)
+            $sClass .= ' bx-btn-small-height';
+        if($bShowDoAsButton)
+            $sClass .= ' bx-btn-height';
+
+        $sContent = $this->_getCounterLabel($iCount, $iProfileId, $bIsMutual, $aParams);
+
+        $this->_oTemplate->addCss(['connection.css']);
+        return $this->_oTemplate->parseHtmlByContent($this->_getTmplContentCounter(), array(
+            'html_id' => $sHtmlIdCounter,
+            'style_prefix' => $this->_sStylePrefix,
+            'bx_if:show_text' => array(
+                'condition' => false,
+                'content' => array(
+                    'class' => $sClass,
+                    'bx_repeat:attrs' => array(
+                        array('key' => 'id', 'value' => $sHtmlIdCounter),
+                    ),
+                    'content' => $sContent
+                )
+            ),
+            'bx_if:show_link' => array(
+                'condition' => true,
+                'content' => array(
+                    'class' => $sClass,
+                    'bx_repeat:attrs' => array(
+                        array('key' => 'id', 'value' => $sHtmlIdCounter),
+                        array('key' => 'href', 'value' => 'javascript:void(0)'),
+                        array('key' => 'onclick', 'value' => 'javascript:' . $sJsObject . '.toggleByPopup(this)'),
+                        array('key' => 'title', 'value' => _t('_sys_conn_do_by'))
+                    ),
+                    'content' => $sContent
+                )
+            ),
+            'script' => $bShowScript ? $this->_getJsScript($iProfileId, $bIsMutual, $aParams) : ''
+        ));
     }
 
-    public function getCounterAPI($iProfileId, $bIsMutual, $aParams, $sContentType)
+    public function getCounterAPI($iProfileId, $bIsMutual = false, $aParams = [])
     {
+        $sContentType = ($sKey = 'content_type') && isset($aParams[$sKey]) ? $aParams[$sKey] : BX_CONNECTIONS_CONTENT_TYPE_INITIATORS;
         $iCount = $this->{'getConnected' . ($sContentType == BX_CONNECTIONS_CONTENT_TYPE_INITIATORS ? 'Initiators' : 'Content') . 'Count'}($iProfileId, $bIsMutual);
+
         return [
             'count' => $iCount,
-            'countf' => _t(isset($aParams['caption']) ? $aParams['caption'] : '_view_counter', $iCount)
+            'countf' => _t(isset($aParams['caption']) ? $aParams['caption'] : $this->_aT['counter'], $iCount)
         ];
     }
-    
+
     public function getConnectedListAPI($iProfileId, $bIsMutual, $sContentType, $iCount = BX_CONNECTIONS_LIST_COUNTER)
     {
         $aProfiles = [];
@@ -162,42 +291,51 @@ class BxBaseConnection extends BxDolConnection
         return $aProfiles;
     }
 
-    protected function _getJsScript($iProfileId, $sContentType, $bIsMutual, $aHtmlIds, $bDynamicMode = false)
+    protected function _getJsScript($iProfileId, $bIsMutual = false, $aParams = [])
     {
         $sJsObject = $this->getJsObjectName($iProfileId);
 
-        $aParamsJs = array(
-            'sObjName' => $sJsObject,
+        $aParamsJs = [
             'sSystem' => $this->_sObject,
             'iObjId' => $iProfileId,
-            'sContentType' => $sContentType,
             'bIsMutual' => $bIsMutual ? 1 : 0,
+            'sObjName' => $sJsObject,
             'sRootUrl' => BX_DOL_URL_ROOT,
             'sStylePrefix' => $this->_sStylePrefix,
-            'aHtmlIds' => $aHtmlIds
-        );
-        
+            'aHtmlIds' => $this->_aHtmlIds
+        ];
+        if(($sKey = 'content_type') && !empty($aParams[$sKey]))
+            $aParamsJs['sContentType'] = $aParams[$sKey];
+
         $sCode = "if(window['" . $sJsObject . "'] == undefined) var " . $sJsObject . " = new BxDolConnection(" . json_encode($aParamsJs) . ");";
 
         return $this->_oTemplate->_wrapInTagJsCode($sCode);
     }
-    
-    protected function _getCounterLabel($iCount, $iProfileId, $bIsMutual, $aParams, $sContentType)
+
+    protected function _getCounterCount($iProfileId, $bIsMutual, $aParams = [])
+    {
+        $sContentType = ($sKey = 'content_type') && isset($aParams[$sKey]) ? $aParams[$sKey] : BX_CONNECTIONS_CONTENT_TYPE_INITIATORS;
+        return $this->{'getConnected' . ($sContentType == BX_CONNECTIONS_CONTENT_TYPE_INITIATORS ? 'Initiators' : 'Content') . 'Count'}($iProfileId, $bIsMutual);
+    }
+
+    protected function _getCounterLabel($iCount, $iProfileId, $bIsMutual, $aParams)
     {
         if ('mutual' != $this->_aObject['type'])
             $bIsMutual = false;
 
+        $sContentType = ($sKey = 'content_type') && isset($aParams[$sKey]) ? $aParams[$sKey] : BX_CONNECTIONS_CONTENT_TYPE_INITIATORS;
+
         $bShowWithProfiles = isset($aParams['show_counter_label_with_profiles']) && $aParams['show_counter_label_with_profiles'] === true;
         $bShowWithIcon = (!isset($aParams['show_counter_label_icon']) || $aParams['show_counter_label_icon'] === true) && !$bShowWithProfiles;
 
-        $aTmplVarsWithProfiles = array();
+        $aTmplVarsWithProfiles = [];
         if($bShowWithProfiles) {
             $aTmplVarsWithProfiles = [
                 'style_prefix' => $this->_sStylePrefix,
                 'bx_repeat:profiles' => []
             ];
 
-            $aIds = $this->{BX_CONNECTIONS_CONTENT_TYPE_INITIATORS == $sContentType ? 'getConnectedInitiators' : 'getConnectedContent'}($iProfileId, $bIsMutual, 0, BX_CONNECTIONS_LIST_COUNTER);
+            $aIds = $this->{'getConnected' . (BX_CONNECTIONS_CONTENT_TYPE_INITIATORS == $sContentType ? 'Initiators' : 'Content')}($iProfileId, $bIsMutual, 0, BX_CONNECTIONS_LIST_COUNTER);
             foreach($aIds as $iId)
                 if(($oProfile = BxDolProfile::getInstanceMagic($iId)) !== false)
                     $aTmplVarsWithProfiles['bx_repeat:profiles'][] = [
@@ -212,7 +350,7 @@ class BxBaseConnection extends BxDolConnection
                 'condition' => $bShowWithIcon,
                 'content' => array(
                     'style_prefix' => $this->_sStylePrefix,
-                    'name' => isset($aParams['custom_icon']) && $aParams['custom_icon'] != '' ? $aParams['custom_icon'] : BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getFontIconAsHtml($this->_getIconDo())
+                    'name' => isset($aParams['custom_icon']) && $aParams['custom_icon'] != '' ? $aParams['custom_icon'] : $this->_oFunctions->getFontIconAsHtml($this->_getIconDo())
                 )
             ),
             'bx_if:show_profiles' => array(
@@ -223,12 +361,12 @@ class BxBaseConnection extends BxDolConnection
                 'condition' => !isset($aParams['show_counter_label_text']) || $aParams['show_counter_label_text'] === true,
                 'content' => array(
                     'style_prefix' => $this->_sStylePrefix,
-                    'text' => _t(isset($aParams['caption']) ? $aParams['caption'] : '_view_counter', $iCount)
+                    'text' => _t(isset($aParams['caption']) ? $aParams['caption'] : $this->_aT['counter'], $iCount)
                 )
             )
         ));
     }
-    
+
     protected function _getIconDo()
     {
         if ($this->_sObject == 'sys_profiles_subscriptions')
@@ -288,4 +426,54 @@ class BxBaseConnection extends BxDolConnection
             $oProfile->getUnit(0, array('template' => 'unit_wo_info'))
         );
     }
+    
+    protected function _getActions($iInitiator, $iContent, $aParams = [])
+    {
+        return [];
+    }
+
+    protected function _getActionIcon($aAction, $aParams = [])
+    {
+        return !empty($aAction['name']) ? $this->{'_getActionIconAs' . bx_gen_method_name($this->_useIconAs($aParams))}($aAction['name']) : '';
+    }
+
+    protected function _useIconAs($aParams = [])
+    {
+        if(!empty($aParams['use_icon_as']) && in_array($aParams['use_icon_as'], ['icon', 'emoji', 'image']))
+            return $aParams['use_icon_as'];
+
+        return 'icon';
+    }
+
+    protected function _getTmplContentElement()
+    {
+        return $this->_sTmplContentElement;
+    }
+    
+    protected function _getTmplContentCounter()
+    {
+        return $this->_sTmplContentCounter;
+    }
+
+    protected function _getTmplVarsAction($aAction, $aParams = [])
+    {
+        $sIcon = $this->_getActionIcon($aAction, $aParams);
+
+        return [
+            'bx_if:show_icon' => [
+                'condition' => !empty($sIcon),
+                'content' => [
+                    'style_prefix' => $this->_sStylePrefix,
+                    'icon' => $this->_oTemplate->getImageAuto($sIcon)
+                ]
+            ],
+            'bx_if:show_text' => [
+                'condition' => !empty($aAction['title']),
+                'content' => [
+                    'style_prefix' => $this->_sStylePrefix,
+                    'text' => _t($aAction['title'])
+                ]
+            ]
+        ];
+    }
 }

+ 107 - 0
template/scripts/BxBaseConnectionFriend.php

@@ -0,0 +1,107 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * @see BxBaseConnection
+ */
+class BxBaseConnectionFriend extends BxDolConnectionFriend
+{
+    protected function __construct($aObject)
+    {
+        parent::__construct($aObject);
+
+        $this->_aT = array_merge($this->_aT, [
+            'do_initiator' => '_sys_conn_friends_do_initiator',
+            'do_content' => '_sys_conn_friends_do_content',
+            'counter' => '_sys_conn_friends_counter'
+        ]);
+    }
+
+    protected function _getActions($iInitiator, $iContent, $aParams = [])
+    {
+        $sName = $sTitle = '';
+        $aActions = [];
+
+        if($this->isConnectedNotMutual($iInitiator, $iContent)) {
+            $sName = 'remove';
+            $sTitle = $this->_aT['do_initiator'];
+            $aActions = [[
+                'name' => 'add',
+                'title' => ($sKey = 'txt_befriend_sent') && !empty($aParams[$sKey]) ? $aParams[$sKey] : '_sys_menu_item_title_sm_befriend_sent'
+            ], [
+                'name' => 'remove',
+                'title' => ($sKey = 'txt_unfriend_cancel') && !empty($aParams[$sKey]) ? $aParams[$sKey] : '_sys_menu_item_title_sm_unfriend_cancel'
+            ]];
+        }
+        else if($this->isConnectedNotMutual($iContent, $iInitiator)) {
+            $sName = 'add';
+            $sTitle = $this->_aT['do_content'];
+            $aActions = [[
+                'name' => 'add',
+                'title' => ($sKey = 'txt_befriend_confirm') && !empty($aParams[$sKey]) ? $aParams[$sKey] : '_sys_menu_item_title_sm_befriend_confirm'
+            ], [
+                'name' => 'remove',
+                'title' => ($sKey = 'txt_unfriend_reject') && !empty($aParams[$sKey]) ? $aParams[$sKey] : '_sys_menu_item_title_sm_unfriend_reject'
+            ]];
+        }
+        else if($this->isConnected($iInitiator, $iContent, true))
+            $aActions = [[
+                'name' => 'remove',
+                'title' => ($sKey = 'txt_unfriend') && !empty($aParams[$sKey]) ? $aParams[$sKey] : '_sys_menu_item_title_sm_unfriend'
+            ]];
+        else
+            $aActions = [[
+                'name' => 'add',
+                'title' => ($sKey = 'txt_befriend') && !empty($aParams[$sKey]) ? $aParams[$sKey] : '_sys_menu_item_title_sm_befriend'
+            ]];
+
+        return [
+            'name' => $sName,
+            'title' => $sTitle,
+            'items' => $aActions
+        ];
+    }
+
+    protected function _getActionIconAsIcon($sAction)
+    {
+        $sDefault = 'user';
+
+        $aA2I = [
+            'add' => 'user-plus',
+            'remove' => 'user-times'
+        ];
+
+        return isset($aA2I[$sAction]) ? $aA2I[$sAction] : $sDefault;
+    }
+
+    //TODO: Get emoji similar to 
+    protected function _getActionIconAsEmoji($sAction)
+    {
+        $sDefault = '';
+
+        $aA2I = [
+            'add' => '',
+            'remove' => ''
+        ];
+
+        return isset($aA2I[$sAction]) ? $aA2I[$sAction] : $sDefault;
+    }
+
+    protected function _getActionIconAsImage($sAction)
+    {
+        $sDefault = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-icon lucide-user"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>';
+
+        $aA2I = [
+            'add' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-round-plus-icon lucide-user-round-plus"><path d="M2 21a8 8 0 0 1 13.292-6"/><circle cx="10" cy="8" r="5"/><path d="M19 16v6"/><path d="M22 19h-6"/></svg>',
+            'remove' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-round-x-icon lucide-user-round-x"><path d="M2 21a8 8 0 0 1 11.873-7"/><circle cx="10" cy="8" r="5"/><path d="m17 17 5 5"/><path d="m22 17-5 5"/></svg>'
+        ];
+
+        return isset($aA2I[$sAction]) ? $aA2I[$sAction] : $sDefault;
+    }
+}

+ 91 - 0
template/scripts/BxBaseConnectionSubscription.php

@@ -0,0 +1,91 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * @see BxBaseConnection
+ */
+class BxBaseConnectionSubscription extends BxDolConnectionSubscription
+{
+    protected function __construct($aObject)
+    {
+        parent::__construct($aObject);
+
+        $this->_aT = array_merge($this->_aT, [
+            'do_initiator' => '_sys_conn_subscriptions_do_initiator',
+            'counter' => '_sys_conn_subscriptions_counter'
+        ]);
+    }
+
+    protected function _getActions($iInitiator, $iContent, $aParams = [])
+    {
+        $sName = $sTitle = '';
+        $aActions = [];
+
+        if($this->isConnected($iInitiator, $iContent)) {
+            $sName = 'remove';
+            $sTitle = $this->_aT['do_initiator'];
+            $aActions = [[
+                'name' => 'remove',
+                'title' => ($sKey = 'txt_unsubscribe') && !empty($aParams[$sKey]) ? $aParams[$sKey] : '_sys_menu_item_title_sm_unsubscribe'
+            ], [
+                /*
+                 * An empty array item to show all items in popup.
+                 * More actions will be added later.
+                 */
+            ]];
+        } else
+            $aActions = [[
+                'name' => 'add',
+                'title' => ($sKey = 'txt_subscribe') && !empty($aParams[$sKey]) ? $aParams[$sKey] : '_sys_menu_item_title_sm_subscribe'
+            ]];
+
+        return [
+            'name' => $sName,
+            'title' => $sTitle,
+            'items' => $aActions
+        ];
+    }
+    
+    protected function _getActionIconAsIcon($sAction)
+    {
+        $sDefault = 'check';
+
+        $aA2I = [
+            'add' => $sDefault,
+            'remove' => $sDefault
+        ];
+
+        return isset($aA2I[$sAction]) ? $aA2I[$sAction] : $sDefault;
+    }
+
+    //TODO: Get emoji similar to 
+    protected function _getActionIconAsEmoji($sAction)
+    {
+        $sDefault = '';
+
+        $aA2I = [
+            'add' => $sDefault,
+            'remove' => $sDefault
+        ];
+
+        return isset($aA2I[$sAction]) ? $aA2I[$sAction] : $sDefault;
+    }
+
+    protected function _getActionIconAsImage($sAction)
+    {
+        $sDefault = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check-icon lucide-check"><path d="M20 6 9 17l-5-5"/></svg>';
+
+        $aA2I = [
+            'add' => $sDefault,
+            'remove' => $sDefault
+        ];
+
+        return isset($aA2I[$sAction]) ? $aA2I[$sAction] : $sDefault;
+    }
+}

+ 21 - 0
template/scripts_templ/BxTemplConnectionFriend.php

@@ -0,0 +1,21 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaTemplate UNA Template Classes
+ * @{
+ */
+
+/**
+ * @see BxDolConnection
+ */
+class BxTemplConnectionFriend extends BxBaseConnectionFriend
+{
+    protected function __construct($aObject)
+    {
+        parent::__construct($aObject);
+    }
+}
+
+/** @} */

+ 21 - 0
template/scripts_templ/BxTemplConnectionSubscription.php

@@ -0,0 +1,21 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaTemplate UNA Template Classes
+ * @{
+ */
+
+/**
+ * @see BxDolConnection
+ */
+class BxTemplConnectionSubscription extends BxBaseConnectionSubscription
+{
+    protected function __construct($aObject)
+    {
+        parent::__construct($aObject);
+    }
+}
+
+/** @} */