diff --git a/database.sql b/database.sql index d42f61f902..86207a1221 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ --- Friendica 2019.09-rc (Dalmatian Bellflower) --- DB_UPDATE_VERSION 1322 +-- Friendica 2019.12-dev (Dalmatian Bellflower) +-- DB_UPDATE_VERSION 1324 -- ------------------------------------------ @@ -1281,7 +1281,9 @@ CREATE TABLE IF NOT EXISTS `user-item` ( `uid` mediumint unsigned NOT NULL DEFAULT 0 COMMENT 'User id', `hidden` boolean NOT NULL DEFAULT '0' COMMENT 'Marker to hide an item from the user', `ignored` boolean COMMENT 'Ignore this thread if set', - PRIMARY KEY(`uid`,`iid`) + `pinned` boolean COMMENT 'The item is pinned on the profile page', + PRIMARY KEY(`uid`,`iid`), + INDEX `uid_pinned` (`uid`,`pinned`) ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='User specific item data'; -- diff --git a/include/conversation.php b/include/conversation.php index 84e47d34e3..724d18dfea 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -1451,7 +1451,9 @@ function conv_sort(array $item_list, $order) } } - if (stristr($order, 'received')) { + if (stristr($order, 'pinned_received')) { + usort($parents, 'sort_thr_pinned_received'); + } elseif (stristr($order, 'received')) { usort($parents, 'sort_thr_received'); } elseif (stristr($order, 'commented')) { usort($parents, 'sort_thr_commented'); @@ -1488,6 +1490,24 @@ function conv_sort(array $item_list, $order) return $parents; } +/** + * @brief usort() callback to sort item arrays by pinned and the received key + * + * @param array $a + * @param array $b + * @return int + */ +function sort_thr_pinned_received(array $a, array $b) +{ + if ($b['pinned'] && !$a['pinned']) { + return 1; + } elseif (!$b['pinned'] && $a['pinned']) { + return -1; + } + + return strcmp($b['received'], $a['received']); +} + /** * @brief usort() callback to sort item arrays by the received key * diff --git a/src/Model/Item.php b/src/Model/Item.php index 9501c8e5d2..dde33437a5 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -58,7 +58,7 @@ class Item extends BaseObject 'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network', 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'owner-network', 'contact-id', 'contact-uid', 'contact-link', 'contact-name', 'contact-avatar', - 'writable', 'self', 'cid', 'alias', + 'writable', 'self', 'cid', 'alias', 'pinned', 'event-id', 'event-created', 'event-edited', 'event-start', 'event-finish', 'event-summary', 'event-desc', 'event-location', 'event-type', 'event-nofinish', 'event-adjust', 'event-ignore', 'event-id', @@ -114,6 +114,60 @@ class Item extends BaseObject return self::$legacy_mode; } + /** + * Set the pinned state of an item + * + * @param integer $iid Item ID + * @param integer $uid User ID + * @param boolean $pinned Pinned state + */ + public static function setPinned(int $iid, int $uid, bool $pinned) + { + DBA::update('user-item', ['pinned' => $pinned], ['iid' => $iid, 'uid' => $uid], true); + } + + /** + * Get the pinned state + * + * @param integer $iid Item ID + * @param integer $uid User ID + * + * @return boolean pinned state + */ + public static function getPinned(int $iid, int $uid) + { + $useritem = DBA::selectFirst('user-item', ['pinned'], ['iid' => $iid, 'uid' => $uid]); + if (!DBA::isResult($useritem)) { + return false; + } + return (bool)$useritem['pinned']; + } + + /** + * @brief Select pinned rows from the item table for a given user + * + * @param integer $uid User ID + * @param array $selected Array of selected fields, empty for all + * + * @return boolean|object + * @throws \Exception + */ + public static function selectPinned(int $uid, array $selected = []) + { + $useritems = DBA::select('user-item', ['iid'], ['uid' => $uid, 'pinned' => true]); + if (!DBA::isResult($useritems)) { + return $useritems; + } + + $pinned = []; + while ($useritem = self::fetch($useritems)) { + $pinned[] = $useritem['iid']; + } + DBA::close($useritems); + + return self::selectThreadForUser($uid, $selected, ['iid' => $pinned]); + } + /** * @brief returns an activity index from an activity string * @@ -585,7 +639,7 @@ class Item extends BaseObject 'iaid' => 'internal-iaid']; if ($usermode) { - $fields['user-item'] = ['ignored' => 'internal-user-ignored']; + $fields['user-item'] = ['pinned', 'ignored' => 'internal-user-ignored']; } $fields['item-activity'] = ['activity', 'activity' => 'internal-activity']; diff --git a/src/Module/Profile.php b/src/Module/Profile.php index 69db45acf1..cd0ee66bb9 100644 --- a/src/Module/Profile.php +++ b/src/Module/Profile.php @@ -177,7 +177,7 @@ class Profile extends BaseModule } if (!$update) { - $tab = Strings::escapeTags(trim($_GET['tab'] ?? '')); + $tab = Strings::escapeTags(trim($_GET['tab'] ?? '')); $o .= ProfileModel::getTabs($a, $tab, $is_owner, $a->profile['nickname']); @@ -349,7 +349,13 @@ class Profile extends BaseModule $items = DBA::toArray($items_stmt); - $o .= conversation($a, $items, $pager, 'profile', $update, false, 'received', $a->profile['profile_uid']); + if ($pager->getStart() == 0) { + $pinned_items = Item::selectPinned($a->profile['profile_uid'], ['uri']); + $pinned = Item::inArray($pinned_items); + $items = array_merge($items, $pinned); + } + + $o .= conversation($a, $items, $pager, 'profile', $update, false, 'pinned_received', $a->profile['profile_uid']); if (!$update) { $o .= $pager->renderMinimal(count($items)); diff --git a/src/Object/Post.php b/src/Object/Post.php index babf24e0d6..f9119ee032 100644 --- a/src/Object/Post.php +++ b/src/Object/Post.php @@ -140,8 +140,11 @@ class Post extends BaseObject $sparkle = ''; $buttons = ''; $dropping = false; + $pinned = ''; + $pin = false; $star = false; $ignore = false; + $ispinned = "unpinned"; $isstarred = "unstarred"; $indent = ''; $shiny = ''; @@ -284,6 +287,23 @@ class Post extends BaseObject } if ($conv->getProfileOwner() == local_user() && ($item['uid'] != 0)) { + if ($origin) { + if ($item['pinned']) { + $pinned = L10n::t('pinned item'); + } + + $ispinned = ($item['pinned'] ? 'pinned' : 'unpinned'); + + $pin = [ + 'do' => L10n::t('pin'), + 'undo' => L10n::t('unpin'), + 'toggle' => L10n::t('toggle pin status'), + 'classdo' => $item['pinned'] ? 'hidden' : '', + 'classundo' => $item['pinned'] ? '' : 'hidden', + 'pinned' => L10n::t('pinned'), + ]; + } + $isstarred = (($item['starred']) ? "starred" : "unstarred"); $star = [ @@ -407,6 +427,9 @@ class Post extends BaseObject 'owner_name' => $owner_name_e, 'plink' => Item::getPlink($item), 'edpost' => $edpost, + 'ispinned' => $ispinned, + 'pin' => $pin, + 'pinned' => $pinned, 'isstarred' => $isstarred, 'star' => $star, 'ignore' => $ignore, diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 53f8a8ed44..7f4cd71286 100755 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -34,7 +34,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1323); + define('DB_UPDATE_VERSION', 1324); } return [ @@ -1384,10 +1384,12 @@ return [ "iid" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "primary" => "1", "relation" => ["item" => "id"], "comment" => "Item id"], "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "primary" => "1", "relation" => ["user" => "uid"], "comment" => "User id"], "hidden" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Marker to hide an item from the user"], - "ignored" => ["type" => "boolean", "comment" => "Ignore this thread if set"] + "ignored" => ["type" => "boolean", "comment" => "Ignore this thread if set"], + "pinned" => ["type" => "boolean", "comment" => "The item is pinned on the profile page"] ], "indexes" => [ - "PRIMARY" => ["uid", "iid"] + "PRIMARY" => ["uid", "iid"], + "uid_pinned" => ["uid", "pinned"] ] ], "worker-ipc" => [ diff --git a/static/routes.config.php b/static/routes.config.php index 1f2fe0ad1b..339860afe6 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -179,8 +179,9 @@ return [ '/{type}/{customize}/{name}' => [Module\Photo::class, [R::GET]], ], - '/pretheme' => [Module\ThemeDetails::class, [R::GET]], - '/probe' => [Module\Debug\Probe::class, [R::GET]], + '/pinned/{item:\d+}' => [Module\Pinned::class, [R::GET]], + '/pretheme' => [Module\ThemeDetails::class, [R::GET]], + '/probe' => [Module\Debug\Probe::class, [R::GET]], '/profile' => [ '/{nickname}' => [Module\Profile::class, [R::GET]], diff --git a/view/js/main.js b/view/js/main.js index 40db7c2a13..94644c5dfd 100644 --- a/view/js/main.js +++ b/view/js/main.js @@ -626,6 +626,25 @@ function dostar(ident) { }); } +function dopin(ident) { + ident = ident.toString(); + $('#like-rotator-' + ident).show(); + $.get('pinned/' + ident, function(data) { + if (data.match(/1/)) { + $('#pinned-' + ident).addClass('pinned'); + $('#pinned-' + ident).removeClass('unpinned'); + $('#pin-' + ident).addClass('hidden'); + $('#unpin-' + ident).removeClass('hidden'); + } else { + $('#pinned-' + ident).addClass('unpinned'); + $('#pinned-' + ident).removeClass('pinned'); + $('#pin-' + ident).removeClass('hidden'); + $('#unpin-' + ident).addClass('hidden'); + } + $('#like-rotator-' + ident).hide(); + }); +} + function doignore(ident) { ident = ident.toString(); $('#like-rotator-' + ident).show(); diff --git a/view/templates/wall_thread.tpl b/view/templates/wall_thread.tpl index a4834062c0..0816400d52 100644 --- a/view/templates/wall_thread.tpl +++ b/view/templates/wall_thread.tpl @@ -50,7 +50,7 @@