From fcdd061b4f89d04889025c516f98c9eead53ad1a Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Fri, 27 Jun 2014 13:58:35 +0200 Subject: [PATCH] pluginified most of hasFave, getFaves and related calls The code is now more event-driven when it comes to rendering notices and their related HTML elements, since we can't have direct calls from core to a plugin. lib/activitymover.php has a function to move a Favorite activity which will not happen now. The move must be pluginified and performed as an event which plugins can catch on to. --- EVENTS.txt | 6 - actions/apistatusesupdate.php | 10 +- classes/Notice.php | 75 +------ lib/activitymover.php | 4 +- lib/apiaction.php | 13 +- lib/command.php | 57 +---- lib/commandinterpreter.php | 14 +- lib/noticelist.php | 24 +- lib/noticelistactorsitem.php | 84 +++++++ lib/noticelistitem.php | 23 -- lib/noticelistitemadapter.php | 5 + lib/threadednoticelist.php | 174 ++------------- lib/useractivitystream.php | 33 +-- plugins/Activity/lib/systemlistitem.php | 6 +- plugins/ActivitySpam/ActivitySpamPlugin.php | 7 +- plugins/AnonymousFave/AnonymousFavePlugin.php | 4 +- plugins/AnonymousFave/actions/anonfavor.php | 4 +- plugins/Favorite/EVENTS.txt | 7 + plugins/Favorite/FavoritePlugin.php | 209 +++++++++++++++++- .../Favorite/actions/apifavoritecreate.php | 2 +- plugins/Favorite/actions/favor.php | 6 +- plugins/Favorite/classes/Fave.php | 30 +++ plugins/Favorite/lib/favcommand.php | 52 +++++ .../lib/threadednoticelistfavesitem.php | 80 +++++++ .../lib/threadednoticelistinlinefavesitem.php | 34 +++ .../GroupPrivateMessagePlugin.php | 2 +- plugins/Realtime/RealtimePlugin.php | 2 + scripts/upgrade.php | 30 --- tests/CommandInterperterTest.php | 4 +- 29 files changed, 580 insertions(+), 421 deletions(-) create mode 100644 lib/noticelistactorsitem.php create mode 100644 plugins/Favorite/lib/favcommand.php create mode 100644 plugins/Favorite/lib/threadednoticelistfavesitem.php create mode 100644 plugins/Favorite/lib/threadednoticelistinlinefavesitem.php diff --git a/EVENTS.txt b/EVENTS.txt index bf9cfaa3bc..a9057e7f6a 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -267,12 +267,6 @@ StartShowNoticeOptions: just before showing notice options like fave, repeat, et EndShowNoticeOptions: just after showing notice options like fave, repeat, etc. - $item: the NoticeListItem object being shown -StartShowFaveForm: just before showing the fave form -- $item: the NoticeListItem object being shown - -EndShowFaveForm: just after showing the fave form -- $item: the NoticeListItem object being shown - StartShowPageNotice: just before showing the page notice (instructions or error) - $action: action object being shown diff --git a/actions/apistatusesupdate.php b/actions/apistatusesupdate.php index 73b87cac98..7bc5d899ef 100644 --- a/actions/apistatusesupdate.php +++ b/actions/apistatusesupdate.php @@ -337,12 +337,14 @@ class ApiStatusesUpdateAction extends ApiAuthAction function supported($cmd) { static $cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand', - 'FavCommand', 'OnCommand', 'OffCommand', 'JoinCommand', 'LeaveCommand'); + 'OnCommand', 'OffCommand', 'JoinCommand', 'LeaveCommand'); - if (in_array(get_class($cmd), $cmdlist)) { - return true; + $supported = null; + + if (Event::handle('CommandSupportedAPI', array($cmd, &$supported))) { + $supported = $supported || in_array(get_class($cmd), $cmdlist); } - return false; + return $supported; } } diff --git a/classes/Notice.php b/classes/Notice.php index 9cf676a664..4c831c7cc7 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -194,7 +194,6 @@ class Notice extends Managed_DataObject $this->clearReplies(); $this->clearRepeats(); - $this->clearFaves(); $this->clearTags(); $this->clearGroupInboxes(); $this->clearFiles(); @@ -1730,16 +1729,18 @@ class Notice extends Managed_DataObject // favorite and repeated + $scoped = null; if (!empty($cur)) { - $cp = $cur->getProfile(); - $noticeInfoAttr['favorite'] = ($cp->hasFave($this)) ? "true" : "false"; - $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this)) ? "true" : "false"; + $scoped = $cur->getProfile(); + $noticeInfoAttr['repeated'] = ($scoped->hasRepeated($this)) ? "true" : "false"; } if (!empty($this->repeat_of)) { $noticeInfoAttr['repeat_of'] = $this->repeat_of; } + Event::handle('StatusNetApiNoticeInfo', array($this, &$noticeInfoAttr, $scoped)); + return array('statusnet:notice_info', $noticeInfoAttr, null); } @@ -2021,24 +2022,6 @@ class Notice extends Managed_DataObject } } - function clearFaves() - { - $fave = new Fave(); - $fave->notice_id = $this->id; - - if ($fave->find()) { - while ($fave->fetch()) { - self::blow('fave:ids_by_user_own:%d', $fave->user_id); - self::blow('fave:ids_by_user_own:%d;last', $fave->user_id); - self::blow('fave:ids_by_user:%d', $fave->user_id); - self::blow('fave:ids_by_user:%d;last', $fave->user_id); - $fave->delete(); - } - } - - $fave->free(); - } - function clearTags() { $tag = new Notice_tag(); @@ -2570,14 +2553,13 @@ class Notice extends Managed_DataObject } } - static function _idsOf(&$notices) + static function _idsOf(array &$notices) { $ids = array(); foreach ($notices as $notice) { - $ids[] = $notice->id; + $ids[$notice->id] = true; } - $ids = array_unique($ids); - return $ids; + return array_keys($ids); } static function fillAttachments(&$notices) @@ -2609,47 +2591,6 @@ class Notice extends Managed_DataObject } } - protected $_faves = array(); - - /** - * All faves of this notice - * - * @return array Array of Fave objects - */ - - function getFaves() - { - if (isset($this->_faves[$this->id])) { - return $this->_faves[$this->id]; - } - $faveMap = Fave::listGet('notice_id', array($this->id)); - $this->_faves[$this->id] = $faveMap[$this->id]; - return $this->_faves[$this->id]; - } - - function _setFaves($faves) - { - $this->_faves[$this->id] = $faves; - } - - static function fillFaves(&$notices) - { - $ids = self::_idsOf($notices); - $faveMap = Fave::listGet('notice_id', $ids); - $cnt = 0; - $faved = array(); - foreach ($faveMap as $id => $faves) { - $cnt += count($faves); - if (count($faves) > 0) { - $faved[] = $id; - } - } - foreach ($notices as $notice) { - $faves = $faveMap[$notice->id]; - $notice->_setFaves($faves); - } - } - static function fillReplies(&$notices) { $ids = self::_idsOf($notices); diff --git a/lib/activitymover.php b/lib/activitymover.php index 66ae7e5074..fe33e9081e 100644 --- a/lib/activitymover.php +++ b/lib/activitymover.php @@ -93,7 +93,7 @@ class ActivityMover extends QueueHandler } switch ($act->verb) { - case ActivityVerb::FAVORITE: +/* case ActivityVerb::FAVORITE: $this->log(LOG_INFO, "Moving favorite of {$act->objects[0]->id} by ". "{$act->actor->id} to {$remote->nickname}."); @@ -105,7 +105,7 @@ class ActivityMover extends QueueHandler 'notice_id' => $notice->id)); $fave->delete(); } - break; + break;*/ case ActivityVerb::POST: $this->log(LOG_INFO, "Moving notice {$act->objects[0]->id} by ". diff --git a/lib/apiaction.php b/lib/apiaction.php index 55860efa55..36ccc82135 100644 --- a/lib/apiaction.php +++ b/lib/apiaction.php @@ -295,13 +295,18 @@ class ApiAction extends Action function twitterStatusArray($notice, $include_user=true) { + // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array $base = $this->twitterSimpleStatusArray($notice, $include_user); + Event::handle('NoticeSimpleStatusArray', array($notice, &$base, $this->scoped, + array('include_user'=>$include_user))); if (!empty($notice->repeat_of)) { $original = Notice::getKV('id', $notice->repeat_of); - if (!empty($original)) { - $original_array = $this->twitterSimpleStatusArray($original, $include_user); - $base['retweeted_status'] = $original_array; + if ($original instanceof Notice) { + $orig_array = $this->twitterSimpleStatusArray($original, $include_user); + Event::handle('NoticeSimpleStatusArray', array($original, &$orig_array, $this->scoped, + array('include_user'=>$include_user))); + $base['retweeted_status'] = $orig_array; } } @@ -369,10 +374,8 @@ class ApiAction extends Action } if (!is_null($this->scoped)) { - $twitter_status['favorited'] = $this->scoped->hasFave($notice); $twitter_status['repeated'] = $this->scoped->hasRepeated($notice); } else { - $twitter_status['favorited'] = false; $twitter_status['repeated'] = false; } diff --git a/lib/command.php b/lib/command.php index abc22ed6a8..02324280ba 100644 --- a/lib/command.php +++ b/lib/command.php @@ -276,57 +276,6 @@ class StatsCommand extends Command } } -class FavCommand extends Command -{ - var $other = null; - - function __construct($user, $other) - { - parent::__construct($user); - $this->other = $other; - } - - function handle($channel) - { - $notice = $this->getNotice($this->other); - - $fave = new Fave(); - $fave->user_id = $this->user->id; - $fave->notice_id = $notice->id; - $fave->find(); - - if ($fave->fetch()) { - // TRANS: Error message text shown when a favorite could not be set because it has already been favorited. - $channel->error($this->user, _('Could not create favorite: Already favorited.')); - return; - } - - $fave = Fave::addNew($this->user->getProfile(), $notice); - - if (!$fave) { - // TRANS: Error message text shown when a favorite could not be set. - $channel->error($this->user, _('Could not create favorite.')); - return; - } - - // @fixme favorite notification should be triggered - // at a lower level - - $other = User::getKV('id', $notice->profile_id); - - if ($other && $other->id != $this->user->id) { - if ($other->email && $other->emailnotifyfav) { - mail_notify_fave($other, $this->user, $notice); - } - } - - $this->user->blowFavesCache(); - - // TRANS: Text shown when a notice has been marked as favourite successfully. - $channel->output($this->user, _('Notice marked as fave.')); - } -} - class JoinCommand extends Command { var $other = null; @@ -1036,10 +985,6 @@ class HelpCommand extends Command "whois " => _m('COMMANDHELP', "get profile info on user"), // TRANS: Help message for IM/SMS command "lose ". "lose " => _m('COMMANDHELP', "force user to stop following you"), - // TRANS: Help message for IM/SMS command "fav ". - "fav " => _m('COMMANDHELP', "add user's last notice as a 'fave'"), - // TRANS: Help message for IM/SMS command "fav #". - "fav #" => _m('COMMANDHELP', "add notice with the given id as a 'fave'"), // TRANS: Help message for IM/SMS command "repeat #". "repeat #" => _m('COMMANDHELP', "repeat a notice with a given id"), // TRANS: Help message for IM/SMS command "repeat ". @@ -1089,6 +1034,8 @@ class HelpCommand extends Command // Give plugins a chance to add or override... Event::handle('HelpCommandMessages', array($this, &$commands)); + + sort($commands); foreach ($commands as $command => $help) { $out[] = "$command - $help"; } diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php index d1574c55fd..20e7ae1acf 100644 --- a/lib/commandinterpreter.php +++ b/lib/commandinterpreter.php @@ -37,7 +37,7 @@ class CommandInterpreter $cmd = strtolower($cmd); - if (Event::handle('StartIntepretCommand', array($cmd, $arg, $user, &$result))) { + if (Event::handle('StartInterpretCommand', array($cmd, $arg, $user, &$result))) { switch($cmd) { case 'help': if ($arg) { @@ -231,18 +231,6 @@ class CommandInterpreter } } break; - case 'fav': - if (!$arg) { - $result = null; - } else { - list($other, $extra) = $this->split_arg($arg); - if ($extra) { - $result = null; - } else { - $result = new FavCommand($user, $other); - } - } - break; case 'nudge': if (!$arg) { $result = null; diff --git a/lib/noticelist.php b/lib/noticelist.php index c21d73cff5..346974f1d2 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -116,33 +116,25 @@ class NoticeList extends Widget return new NoticeListItem($notice, $this->out); } - static function prefill(&$notices, $avatarSize=AVATAR_STREAM_SIZE) + static function prefill(array &$notices) { - if (Event::handle('StartNoticeListPrefill', array(&$notices, $avatarSize))) { + $scoped = Profile::current(); + $notice_ids = Notice::_idsOf($notices); + + if (Event::handle('StartNoticeListPrefill', array(&$notices, $notice_ids, $scoped))) { // Prefill attachments Notice::fillAttachments($notices); - // Prefill attachments - Notice::fillFaves($notices); // Prefill repeat data Notice::fillRepeats($notices); // Prefill the profiles $profiles = Notice::fillProfiles($notices); - - $p = Profile::current(); - if ($p instanceof Profile) { - $ids = array(); - - foreach ($notices as $notice) { - $ids[] = $notice->id; - } - - Fave::pivotGet('notice_id', $ids, array('user_id' => $p->id)); - Notice::pivotGet('repeat_of', $ids, array('profile_id' => $p->id)); + if ($scoped instanceof Profile) { + Notice::pivotGet('repeat_of', $notice_ids, array('profile_id' => $scoped->id)); } - Event::handle('EndNoticeListPrefill', array(&$notices, &$profiles, $avatarSize)); + Event::handle('EndNoticeListPrefill', array(&$notices, &$profiles, $notice_ids, $scoped)); } } } diff --git a/lib/noticelistactorsitem.php b/lib/noticelistactorsitem.php new file mode 100644 index 0000000000..fb07a66a96 --- /dev/null +++ b/lib/noticelistactorsitem.php @@ -0,0 +1,84 @@ +. + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Placeholder for showing faves... + */ +abstract class NoticeListActorsItem extends NoticeListItem +{ + /** + * @return array of profile IDs + */ + abstract function getProfiles(); + + abstract function getListMessage($count, $you); + + function show() + { + $links = array(); + $you = false; + $cur = common_current_user(); + foreach ($this->getProfiles() as $id) { + if ($cur && $cur->id == $id) { + $you = true; + // TRANS: Reference to the logged in user in favourite list. + array_unshift($links, _m('FAVELIST', 'You')); + } else { + $profile = Profile::getKV('id', $id); + if ($profile instanceof Profile) { + $links[] = sprintf('%s', + htmlspecialchars($profile->getUrl()), + htmlspecialchars($profile->getBestName())); + } + } + } + + if ($links) { + $count = count($links); + $msg = $this->getListMessage($count, $you); + $out = sprintf($msg, $this->magicList($links)); + + $this->showStart(); + $this->out->raw($out); + $this->showEnd(); + return $count; + } else { + return 0; + } + } + + function magicList($items) + { + if (count($items) == 0) { + return ''; + } else if (count($items) == 1) { + return $items[0]; + } else { + $first = array_slice($items, 0, -1); + $last = array_slice($items, -1, 1); + // TRANS: Separator in list of user names like "Jim, Bob, Mary". + $separator = _(', '); + // TRANS: For building a list such as "Jim, Bob, Mary and 5 others like this". + // TRANS: %1$s is a list of users, separated by a separator (default: ", "), %2$s is the last user in the list. + return sprintf(_m('FAVELIST', '%1$s and %2$s'), implode($separator, $first), implode($separator, $last)); + } + } +} diff --git a/lib/noticelistitem.php b/lib/noticelistitem.php index c05d8cdca9..7e4380e1bc 100644 --- a/lib/noticelistitem.php +++ b/lib/noticelistitem.php @@ -153,7 +153,6 @@ class NoticeListItem extends Widget if ($user) { $this->out->elementStart('div', 'notice-options'); if (Event::handle('StartShowNoticeOptionItems', array($this))) { - $this->showFaveForm(); $this->showReplyLink(); $this->showRepeatForm(); $this->showDeleteLink(); @@ -187,28 +186,6 @@ class NoticeListItem extends Widget } } - /** - * show the "favorite" form - * - * @return void - */ - function showFaveForm() - { - if (Event::handle('StartShowFaveForm', array($this))) { - $user = common_current_user(); - if ($user) { - if ($user->hasFave($this->notice)) { - $disfavor = new DisfavorForm($this->out, $this->notice); - $disfavor->show(); - } else { - $favor = new FavorForm($this->out, $this->notice); - $favor->show(); - } - } - Event::handle('EndShowFaveForm', array($this)); - } - } - /** * show the author of a notice * diff --git a/lib/noticelistitemadapter.php b/lib/noticelistitemadapter.php index fc230e8e3f..a80b63088c 100644 --- a/lib/noticelistitemadapter.php +++ b/lib/noticelistitemadapter.php @@ -70,4 +70,9 @@ class NoticeListItemAdapter { return call_user_func_array(array($this->nli, $name), $arguments); } + + function __get($name) + { + return $this->nli->$name; + } } diff --git a/lib/threadednoticelist.php b/lib/threadednoticelist.php index 3e89cf3a19..62099e9155 100644 --- a/lib/threadednoticelist.php +++ b/lib/threadednoticelist.php @@ -229,16 +229,22 @@ class ThreadedNoticeListItem extends NoticeListItem } if (Event::handle('StartShowThreadedNoticeTail', array($this, $this->notice, &$notices))) { + $threadActive = count($notices) > 0; // has this thread had any activity? + $this->out->elementStart('ul', 'notices threaded-replies xoxo'); - $item = new ThreadedNoticeListFavesItem($this->notice, $this->out); - $hasFaves = $item->show(); + if (Event::handle('StartShowThreadedNoticeTailItems', array($this, $this->notice, &$threadActive))) { - $item = new ThreadedNoticeListRepeatsItem($this->notice, $this->out); - $hasRepeats = $item->show(); + // Show the repeats-button for this notice. If there are repeats, + // the show() function will return true, definitely setting our + // $threadActive flag, which will be used later to show a reply box. + $item = new ThreadedNoticeListRepeatsItem($this->notice, $this->out); + $threadActive = $item->show() || $threadActive; - if ($notices) { + Event::handle('EndShowThreadedNoticeTailItems', array($this, $this->notice, &$threadActive)); + } + if (count($notices)>0) { if ($moreCutoff) { $item = new ThreadedNoticeListMoreItem($moreCutoff, $this->out, count($notices)); $item->show(); @@ -247,18 +253,16 @@ class ThreadedNoticeListItem extends NoticeListItem if (Event::handle('StartShowThreadedNoticeSub', array($this, $this->notice, $notice))) { $item = new ThreadedNoticeListSubItem($notice, $this->notice, $this->out); $item->show(); - Event::handle('StartShowThreadedNoticeSub', array($this, $this->notice, $notice)); + Event::handle('EndShowThreadedNoticeSub', array($this, $this->notice, $notice)); } } } - if ($notices || $hasFaves || $hasRepeats) { + if ($threadActive && Profile::current() instanceof Profile) { // @fixme do a proper can-post check that's consistent // with the JS side - if (common_current_user()) { - $item = new ThreadedNoticeListReplyItem($this->notice, $this->out); - $item->show(); - } + $item = new ThreadedNoticeListReplyItem($this->notice, $this->out); + $item->show(); } $this->out->elementEnd('ul'); Event::handle('EndShowThreadedNoticeTail', array($this, $this->notice, $notices)); @@ -319,10 +323,12 @@ class ThreadedNoticeListSubItem extends NoticeListItem function showEnd() { - $item = new ThreadedNoticeListInlineFavesItem($this->notice, $this->out); - $hasFaves = $item->show(); - $item = new ThreadedNoticeListInlineRepeatsItem($this->notice, $this->out); - $hasRepeats = $item->show(); + $threadActive = null; // unused here for now, but maybe in the future? + if (Event::handle('StartShowThreadedNoticeTailItems', array($this, $this->notice, &$threadActive))) { + $item = new ThreadedNoticeListInlineRepeatsItem($this->notice, $this->out); + $hasRepeats = $item->show(); + Event::handle('EndShowThreadedNoticeTailItems', array($this, $this->notice, &$threadActive)); + } parent::showEnd(); } } @@ -419,144 +425,6 @@ class ThreadedNoticeListReplyItem extends NoticeListItem } } -/** - * Placeholder for showing faves... - */ -abstract class NoticeListActorsItem extends NoticeListItem -{ - /** - * @return array of profile IDs - */ - abstract function getProfiles(); - - abstract function getListMessage($count, $you); - - function show() - { - $links = array(); - $you = false; - $cur = common_current_user(); - foreach ($this->getProfiles() as $id) { - if ($cur && $cur->id == $id) { - $you = true; - // TRANS: Reference to the logged in user in favourite list. - array_unshift($links, _m('FAVELIST', 'You')); - } else { - $profile = Profile::getKV('id', $id); - if ($profile instanceof Profile) { - $links[] = sprintf('%s', - htmlspecialchars($profile->getUrl()), - htmlspecialchars($profile->getBestName())); - } - } - } - - if ($links) { - $count = count($links); - $msg = $this->getListMessage($count, $you); - $out = sprintf($msg, $this->magicList($links)); - - $this->showStart(); - $this->out->raw($out); - $this->showEnd(); - return $count; - } else { - return 0; - } - } - - function magicList($items) - { - if (count($items) == 0) { - return ''; - } else if (count($items) == 1) { - return $items[0]; - } else { - $first = array_slice($items, 0, -1); - $last = array_slice($items, -1, 1); - // TRANS: Separator in list of user names like "Jim, Bob, Mary". - $separator = _(', '); - // TRANS: For building a list such as "Jim, Bob, Mary and 5 others like this". - // TRANS: %1$s is a list of users, separated by a separator (default: ", "), %2$s is the last user in the list. - return sprintf(_m('FAVELIST', '%1$s and %2$s'), implode($separator, $first), implode($separator, $last)); - } - } -} - -/** - * Placeholder for showing faves... - */ -class ThreadedNoticeListFavesItem extends NoticeListActorsItem -{ - function getProfiles() - { - $faves = $this->notice->getFaves(); - $profiles = array(); - foreach ($faves as $fave) { - $profiles[] = $fave->user_id; - } - return $profiles; - } - - function magicList($items) - { - if (count($items) > 4) { - return parent::magicList(array_slice($items, 0, 3)); - } else { - return parent::magicList($items); - } - } - - function getListMessage($count, $you) - { - if ($count == 1 && $you) { - // darn first person being different from third person! - // TRANS: List message for notice favoured by logged in user. - return _m('FAVELIST', 'You like this.'); - } else if ($count > 4) { - // TRANS: List message for when more than 4 people like something. - // TRANS: %%s is a list of users liking a notice, %d is the number over 4 that like the notice. - // TRANS: Plural is decided on the total number of users liking the notice (count of %%s + %d). - return sprintf(_m('%%s and %d others like this.', - '%%s and %d others like this.', - $count), - $count - 3); - } else { - // TRANS: List message for favoured notices. - // TRANS: %%s is a list of users liking a notice. - // TRANS: Plural is based on the number of of users that have favoured a notice. - return sprintf(_m('%%s likes this.', - '%%s like this.', - $count), - $count); - } - } - - function showStart() - { - $this->out->elementStart('li', array('class' => 'notice-data notice-faves')); - } - - function showEnd() - { - $this->out->elementEnd('li'); - } -} - -// @todo FIXME: needs documentation. -class ThreadedNoticeListInlineFavesItem extends ThreadedNoticeListFavesItem -{ - function showStart() - { - $this->out->elementStart('div', array('class' => 'notice-faves')); - } - - function showEnd() - { - $this->out->elementEnd('div'); - } -} - /** * Placeholder for showing repeats... */ diff --git a/lib/useractivitystream.php b/lib/useractivitystream.php index ce3e373724..21dfff1db5 100644 --- a/lib/useractivitystream.php +++ b/lib/useractivitystream.php @@ -20,7 +20,7 @@ /** * Class for activity streams * - * Includes faves, notices, and subscriptions. + * Includes objects like notices, subscriptions and from plugins. * * We extend atomusernoticefeed since it does some nice setup for us. * @@ -28,6 +28,7 @@ class UserActivityStream extends AtomUserNoticeFeed { public $activities = array(); + public $after = null; const OUTPUT_STRING = 1; const OUTPUT_RAW = 2; @@ -74,21 +75,20 @@ class UserActivityStream extends AtomUserNoticeFeed $subscriptions = $this->getSubscriptions(); $subscribers = $this->getSubscribers(); $groups = $this->getGroups(); - $faves = $this->getFaves(); $messagesFrom = $this->getMessagesFrom(); $messagesTo = $this->getMessagesTo(); - $objs = array_merge($subscriptions, $subscribers, $groups, $faves, $notices, $messagesFrom, $messagesTo); + $objs = array_merge($subscriptions, $subscribers, $groups, $notices, $messagesFrom, $messagesTo); + + Event::handle('AppendUserActivityStreamObjects', array($this, &$objs)); $subscriptions = null; $subscribers = null; $groups = null; - $faves = null; unset($subscriptions); unset($subscribers); unset($groups); - unset($faves); // Sort by create date @@ -101,7 +101,7 @@ class UserActivityStream extends AtomUserNoticeFeed } /** - * Interleave the pre-sorted subs/groups/faves with the user's + * Interleave the pre-sorted objects with the user's * notices, all in reverse chron order. */ function renderEntries($format=Feed::ATOM, $handle=null) @@ -278,27 +278,6 @@ class UserActivityStream extends AtomUserNoticeFeed return $subs; } - function getFaves() - { - $faves = array(); - - $fave = new Fave(); - - $fave->user_id = $this->user->id; - - if (!empty($this->after)) { - $fave->whereAdd("modified > '" . common_sql_date($this->after) . "'"); - } - - if ($fave->find()) { - while ($fave->fetch()) { - $faves[] = clone($fave); - } - } - - return $faves; - } - /** * * @param int $start unix timestamp for earliest diff --git a/plugins/Activity/lib/systemlistitem.php b/plugins/Activity/lib/systemlistitem.php index c6753e5ff1..958dfbc495 100644 --- a/plugins/Activity/lib/systemlistitem.php +++ b/plugins/Activity/lib/systemlistitem.php @@ -79,8 +79,10 @@ class SystemListItem extends NoticeListItemAdapter $user = common_current_user(); if (!empty($user)) { $this->nli->out->elementStart('div', 'notice-options'); - $this->showFaveForm(); - $this->showReplyLink(); + if (Event::handle('StartShowNoticeOptionItems', array($this))) { + $this->showReplyLink(); + Event::handle('EndShowNoticeOptionItems', array($this)); + } $this->nli->out->elementEnd('div'); } Event::handle('EndShowNoticeOptions', array($this)); diff --git a/plugins/ActivitySpam/ActivitySpamPlugin.php b/plugins/ActivitySpam/ActivitySpamPlugin.php index a18d52c5db..70d927900d 100644 --- a/plugins/ActivitySpam/ActivitySpamPlugin.php +++ b/plugins/ActivitySpam/ActivitySpamPlugin.php @@ -249,12 +249,9 @@ class ActivitySpamPlugin extends Plugin /** * Pre-cache our spam scores if needed. */ - function onEndNoticeListPrefill(&$notices, &$profiles, $avatarSize) { + function onEndNoticeListPrefill(array &$notices, array &$profiles, array $notice_ids, Profile $scoped=null) { if ($this->hideSpam) { - foreach ($notices as $notice) { - $ids[] = $notice->id; - } - Spam_score::multiGet('notice_id', $ids); + Spam_score::multiGet('notice_id', $notice_ids); } return true; } diff --git a/plugins/AnonymousFave/AnonymousFavePlugin.php b/plugins/AnonymousFave/AnonymousFavePlugin.php index 8e31468d55..c817578ec2 100644 --- a/plugins/AnonymousFave/AnonymousFavePlugin.php +++ b/plugins/AnonymousFave/AnonymousFavePlugin.php @@ -124,8 +124,8 @@ class AnonymousFavePlugin extends Plugin if (!common_logged_in() && $this->hasAnonFaving($item)) { $profile = AnonymousFavePlugin::getAnonProfile(); - if (!empty($profile)) { - if ($profile->hasFave($item->notice)) { + if ($profile instanceof Profile) { + if (Fave::existsForProfile($item->notice, $profile)) { $disfavor = new AnonDisFavorForm($item->out, $item->notice); $disfavor->show(); } else { diff --git a/plugins/AnonymousFave/actions/anonfavor.php b/plugins/AnonymousFave/actions/anonfavor.php index 2d35964c70..6af874679f 100644 --- a/plugins/AnonymousFave/actions/anonfavor.php +++ b/plugins/AnonymousFave/actions/anonfavor.php @@ -62,9 +62,9 @@ class AnonFavorAction extends RedirectingAction $notice = Notice::getKV($id); $token = $this->checkSessionToken(); - if ($profile->hasFave($notice)) { + if (Fave::existsForProfile($notice, $profile)) { // TRANS: Client error. - $this->clientError(_m('This notice is already a favorite!')); + throw new AlreadyFulfilledException(_m('This notice is already a favorite!')); } $fave = Fave::addNew($profile, $notice); diff --git a/plugins/Favorite/EVENTS.txt b/plugins/Favorite/EVENTS.txt index fb6bfbb91d..c15839c8a0 100644 --- a/plugins/Favorite/EVENTS.txt +++ b/plugins/Favorite/EVENTS.txt @@ -34,3 +34,10 @@ StartDisFavorNoticeForm: starting the data in the form for disfavoring a notice EndDisFavorNoticeForm: Ending the data in the form for disfavoring a notice - $DisfavorForm: the disfavor form being shown - $notice: notice being disfavored + +StartShowFaveForm: just before showing the fave form +- $item: the NoticeListItem object being shown + +EndShowFaveForm: just after showing the fave form +- $item: the NoticeListItem object being shown + diff --git a/plugins/Favorite/FavoritePlugin.php b/plugins/Favorite/FavoritePlugin.php index 9fbaf258c4..64b52ff75d 100644 --- a/plugins/Favorite/FavoritePlugin.php +++ b/plugins/Favorite/FavoritePlugin.php @@ -20,11 +20,62 @@ if (!defined('GNUSOCIAL')) { exit(1); } /** - * @package UI + * @package Activity * @maintainer Mikael Nordfeldth */ -class FavoritePlugin extends Plugin +class FavoritePlugin extends ActivityHandlerPlugin { + public function tag() + { + return 'favorite'; + } + + public function types() + { + return array(); + } + + public function verbs() + { + return array(ActivityVerb::FAVORITE); + } + + public function onCheckSchema() + { + $schema = Schema::get(); + $schema->ensureTable('fave', Fave::schemaDef()); + return true; + } + + public function onEndUpgrade() + { + printfnq("Ensuring all faves have a URI..."); + + $fave = new Fave(); + $fave->whereAdd('uri IS NULL'); + + if ($fave->find()) { + while ($fave->fetch()) { + try { + $fave->decache(); + $fave->query(sprintf('UPDATE fave '. + 'SET uri = "%s", '. + ' modified = "%s" '. + 'WHERE user_id = %d '. + 'AND notice_id = %d', + Fave::newURI($fave->user_id, $fave->notice_id, $fave->modified), + common_sql_date(strtotime($fave->modified)), + $fave->user_id, + $fave->notice_id)); + } catch (Exception $e) { + common_log(LOG_ERR, "Error updating fave URI: " . $e->getMessage()); + } + } + } + + printfnq("DONE.\n"); + } + public function onRouterInitialized(URLMapper $m) { // Web UI actions @@ -96,6 +147,160 @@ class FavoritePlugin extends Plugin 'format' => '(xml|json)')); } + /** + * Typically just used to fill out Twitter-compatible API status data. + * + * FIXME: Make all the calls before this end up with a Notice instead of ArrayWrapper please... + */ + public function onNoticeSimpleStatusArray($notice, array &$status, Profile $scoped=null, array $args=array()) + { + if ($scoped instanceof Profile) { + $status['favorited'] = Fave::existsForProfile($notice, $scoped); + } else { + $status['favorited'] = false; + } + return true; + } + + /** + * Typically just used to fill out StatusNet specific data in API calls in the referenced $info array. + */ + public function onStatusNetApiNoticeInfo(Notice $notice, array &$info, Profile $scoped=null, array $args=array()) + { + if ($scoped instanceof Profile) { + $info['favorite'] = Fave::existsForProfile($notice, $scoped) ? 'true' : 'false'; + } + return true; + } + + public function onNoticeDeleteRelated(Notice $notice) + { + $fave = new Fave(); + $fave->notice_id = $notice->id; + + if ($fave->find()) { + while ($fave->fetch()) { + Memcached_DataObject::blow('fave:ids_by_user_own:%d', $fave->user_id); + Memcached_DataObject::blow('fave:ids_by_user_own:%d;last', $fave->user_id); + Memcached_DataObject::blow('fave:ids_by_user:%d', $fave->user_id); + Memcached_DataObject::blow('fave:ids_by_user:%d;last', $fave->user_id); + $fave->delete(); + } + } + + $fave->free(); + } + + public function onStartNoticeListPrefill(array &$notices, array $notice_ids, Profile $scoped=null) + { + // prefill array of objects, before pluginfication it was Notice::fillFaves($notices) + Fave::fillFaves($notice_ids); + + // DB caching + if ($scoped instanceof Profile) { + Fave::pivotGet('notice_id', $notice_ids, array('user_id' => $scoped->id)); + } + } + + /** + * show the "favorite" form in the notice options element + * FIXME: Don't let a NoticeListItemAdapter slip in here (or extend that from NoticeListItem) + * + * @return void + */ + public function onStartShowNoticeOptionItems($nli) + { + if (Event::handle('StartShowFaveForm', array($nli))) { + $scoped = Profile::current(); + if ($scoped instanceof Profile) { + if (Fave::existsForProfile($nli->notice, $scoped)) { + $disfavor = new DisfavorForm($nli->out, $nli->notice); + $disfavor->show(); + } else { + $favor = new FavorForm($nli->out, $nli->notice); + $favor->show(); + } + } + Event::handle('EndShowFaveForm', array($nli)); + } + } + + public function onAppendUserActivityStreamObjects(UserActivityStream $uas, array &$objs) + { + $faves = array(); + $fave = new Fave(); + $fave->user_id = $uas->user->id; + + if (!empty($uas->after)) { + $fave->whereAdd("modified > '" . common_sql_date($uas->after) . "'"); + } + + if ($fave->find()) { + while ($fave->fetch()) { + $faves[] = clone($fave); + } + } + + return $faves; + } + + public function onStartShowThreadedNoticeTailItems(NoticeListItem $nli, Notice $notice, &$threadActive) + { + if ($nli instanceof ThreadedNoticeListSubItem) { + // The sub-items are replies to a conversation, thus we use different HTML elements etc. + $item = new ThreadedNoticeListInlineFavesItem($notice, $nli->out); + } else { + $item = new ThreadedNoticeListFavesItem($notice, $nli->out); + } + $threadActive = $item->show() || $threadActive; + return true; + } + + /** + * EndInterpretCommand for FavoritePlugin will handle the 'fav' command + * using the class FavCommand. + * + * @param string $cmd Command being run + * @param string $arg Rest of the message (including address) + * @param User $user User sending the message + * @param Command &$result The resulting command object to be run. + * + * @return boolean hook value + */ + public function onStartInterpretCommand($cmd, $arg, $user, &$result) + { + if ($result === false && $cmd == 'fav') { + if (empty($arg)) { + $result = null; + } else { + list($other, $extra) = $this->split_arg($arg); + if (!empty($extra)) { + $result = null; + } else { + $result = new FavCommand($user, $other); + } + } + return false; + } + return true; + } + + public function onHelpCommandMessages(HelpCommand $help, array &$commands) + { + // TRANS: Help message for IM/SMS command "fav ". + $commands['fav '] = _m('COMMANDHELP', "add user's last notice as a 'fave'"); + // TRANS: Help message for IM/SMS command "fav #". + $commands['fav #'] = _m('COMMANDHELP', "add notice with the given id as a 'fave'"); + } + + /** + * Are we allowed to perform a certain command over the API? + */ + public function onCommandSupportedAPI(Command $cmd, array &$supported) + { + $supported = $supported || $cmd instanceof FavCommand; + } + public function onPluginVersion(array &$versions) { $versions[] = array('name' => 'Favorite', diff --git a/plugins/Favorite/actions/apifavoritecreate.php b/plugins/Favorite/actions/apifavoritecreate.php index 607fb76edc..dd4fbd2795 100644 --- a/plugins/Favorite/actions/apifavoritecreate.php +++ b/plugins/Favorite/actions/apifavoritecreate.php @@ -118,7 +118,7 @@ class ApiFavoriteCreateAction extends ApiAuthAction // Note: Twitter lets you fave things repeatedly via API. - if ($this->user->hasFave($this->notice)) { + if (Fave::existsForProfile($this->notice, $this->scoped)) { $this->clientError( // TRANS: Client error displayed when trying to mark a notice favourite that already is a favourite. _('This status is already a favorite.'), diff --git a/plugins/Favorite/actions/favor.php b/plugins/Favorite/actions/favor.php index 8c19f9da5c..0e0ff9383c 100644 --- a/plugins/Favorite/actions/favor.php +++ b/plugins/Favorite/actions/favor.php @@ -55,12 +55,12 @@ class FavorAction extends FormAction if (!($notice instanceof Notice)) { $this->serverError(_('Notice not found')); } - if ($this->scoped->hasFave($notice)) { + if (Fave::existsForProfile($notice, $this->scoped)) { // TRANS: Client error displayed when trying to mark a notice as favorite that already is a favorite. - $this->clientError(_('This notice is already a favorite!')); + throw new AlreadyFulfilledException(_('This notice is already a favorite!')); } $fave = Fave::addNew($this->scoped, $notice); - if (!$fave) { + if (!$fave instanceof Fave) { // TRANS: Server error displayed when trying to mark a notice as favorite fails in the database. $this->serverError(_('Could not create favorite.')); } diff --git a/plugins/Favorite/classes/Fave.php b/plugins/Favorite/classes/Fave.php index 2076143296..130cf6aa78 100644 --- a/plugins/Favorite/classes/Fave.php +++ b/plugins/Favorite/classes/Fave.php @@ -157,6 +157,12 @@ class Fave extends Managed_DataObject return $act; } + static function existsForProfile($notice, Profile $scoped) { + $fave = self::pkeyGet(array('user_id'=>$scoped->id, 'notice_id'=>$notice->id)); + + return ($fave instanceof Fave); + } + /** * Fetch a stream of favorites by profile * @@ -201,4 +207,28 @@ class Fave extends Managed_DataObject $notice_id, common_date_iso8601($modified)); } + + + static protected $_faves = array(); + + /** + * All faves of this notice + * + * @param Notice $notice A notice we wish to get faves for (may still be ArrayWrapper) + * + * @return array Array of Fave objects + */ + static public function byNotice($notice) + { + if (!isset(self::$_faves[$notice->id])) { + self::fillFaves(array($notice->id)); + } + return self::$_faves[$notice->id]; + } + + static public function fillFaves(array $notice_ids) + { + $faveMap = Fave::listGet('notice_id', $notice_ids); + self::$_faves = array_replace(self::$_faves, $faveMap); + } } diff --git a/plugins/Favorite/lib/favcommand.php b/plugins/Favorite/lib/favcommand.php new file mode 100644 index 0000000000..c38463f4cd --- /dev/null +++ b/plugins/Favorite/lib/favcommand.php @@ -0,0 +1,52 @@ +other = $other; + } + + function handle($channel) + { + $notice = $this->getNotice($this->other); + + $fave = new Fave(); + $fave->user_id = $this->user->id; + $fave->notice_id = $notice->id; + $fave->find(); + + if ($fave->fetch()) { + // TRANS: Error message text shown when a favorite could not be set because it has already been favorited. + $channel->error($this->user, _('Could not create favorite: Already favorited.')); + return; + } + + $fave = Fave::addNew($this->user->getProfile(), $notice); + + if (!$fave) { + // TRANS: Error message text shown when a favorite could not be set. + $channel->error($this->user, _('Could not create favorite.')); + return; + } + + // @fixme favorite notification should be triggered + // at a lower level + + $other = User::getKV('id', $notice->profile_id); + + if ($other && $other->id != $this->user->id) { + if ($other->email && $other->emailnotifyfav) { + mail_notify_fave($other, $this->user, $notice); + } + } + + $this->user->blowFavesCache(); + + // TRANS: Text shown when a notice has been marked as favourite successfully. + $channel->output($this->user, _('Notice marked as fave.')); + } +} diff --git a/plugins/Favorite/lib/threadednoticelistfavesitem.php b/plugins/Favorite/lib/threadednoticelistfavesitem.php new file mode 100644 index 0000000000..fa68039ed5 --- /dev/null +++ b/plugins/Favorite/lib/threadednoticelistfavesitem.php @@ -0,0 +1,80 @@ +. + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Placeholder for showing faves... + */ +class ThreadedNoticeListFavesItem extends NoticeListActorsItem +{ + function getProfiles() + { + $faves = Fave::byNotice($this->notice); + $profiles = array(); + foreach ($faves as $fave) { + $profiles[] = $fave->user_id; + } + return $profiles; + } + + function magicList($items) + { + if (count($items) > 4) { + return parent::magicList(array_slice($items, 0, 3)); + } else { + return parent::magicList($items); + } + } + + function getListMessage($count, $you) + { + if ($count == 1 && $you) { + // darn first person being different from third person! + // TRANS: List message for notice favoured by logged in user. + return _m('FAVELIST', 'You like this.'); + } else if ($count > 4) { + // TRANS: List message for when more than 4 people like something. + // TRANS: %%s is a list of users liking a notice, %d is the number over 4 that like the notice. + // TRANS: Plural is decided on the total number of users liking the notice (count of %%s + %d). + return sprintf(_m('%%s and %d others like this.', + '%%s and %d others like this.', + $count), + $count - 3); + } else { + // TRANS: List message for favoured notices. + // TRANS: %%s is a list of users liking a notice. + // TRANS: Plural is based on the number of of users that have favoured a notice. + return sprintf(_m('%%s likes this.', + '%%s like this.', + $count), + $count); + } + } + + function showStart() + { + $this->out->elementStart('li', array('class' => 'notice-data notice-faves')); + } + + function showEnd() + { + $this->out->elementEnd('li'); + } +} diff --git a/plugins/Favorite/lib/threadednoticelistinlinefavesitem.php b/plugins/Favorite/lib/threadednoticelistinlinefavesitem.php new file mode 100644 index 0000000000..87690f1e6f --- /dev/null +++ b/plugins/Favorite/lib/threadednoticelistinlinefavesitem.php @@ -0,0 +1,34 @@ +. + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +// @todo FIXME: needs documentation. +class ThreadedNoticeListInlineFavesItem extends ThreadedNoticeListFavesItem +{ + function showStart() + { + $this->out->elementStart('div', array('class' => 'notice-faves')); + } + + function showEnd() + { + $this->out->elementEnd('div'); + } +} diff --git a/plugins/GroupPrivateMessage/GroupPrivateMessagePlugin.php b/plugins/GroupPrivateMessage/GroupPrivateMessagePlugin.php index fd9667cb99..28a1ab989e 100644 --- a/plugins/GroupPrivateMessage/GroupPrivateMessagePlugin.php +++ b/plugins/GroupPrivateMessage/GroupPrivateMessagePlugin.php @@ -230,7 +230,7 @@ class GroupPrivateMessagePlugin extends Plugin * * @return boolean hook value */ - function onStartIntepretCommand($cmd, $arg, $user, &$result) + function onStartInterpretCommand($cmd, $arg, $user, &$result) { if ($cmd == 'd' || $cmd == 'dm') { diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php index dfd3a9298f..b0b79fd1c6 100644 --- a/plugins/Realtime/RealtimePlugin.php +++ b/plugins/Realtime/RealtimePlugin.php @@ -37,6 +37,8 @@ if (!defined('GNUSOCIAL')) { exit(1); } * Based on experience with the Comet and Meteor plugins, * this superclass extracts out some of the common functionality * + * Currently depends on Favorite plugin. + * * @category Plugin * @package StatusNet * @author Evan Prodromou diff --git a/scripts/upgrade.php b/scripts/upgrade.php index 902a1aa85d..bd4a73d3af 100644 --- a/scripts/upgrade.php +++ b/scripts/upgrade.php @@ -51,7 +51,6 @@ function main() initLocalGroup(); initNoticeReshare(); - initFaveURI(); initSubscriptionURI(); initGroupMemberURI(); @@ -292,35 +291,6 @@ function initNoticeReshare() printfnq("DONE.\n"); } -function initFaveURI() -{ - printfnq("Ensuring all faves have a URI..."); - - $fave = new Fave(); - $fave->whereAdd('uri IS NULL'); - - if ($fave->find()) { - while ($fave->fetch()) { - try { - $fave->decache(); - $fave->query(sprintf('update fave '. - 'set uri = "%s", '. - ' modified = "%s" '. - 'where user_id = %d '. - 'and notice_id = %d', - Fave::newURI($fave->user_id, $fave->notice_id, $fave->modified), - common_sql_date(strtotime($fave->modified)), - $fave->user_id, - $fave->notice_id)); - } catch (Exception $e) { - common_log(LOG_ERR, "Error updated fave URI: " . $e->getMessage()); - } - } - } - - printfnq("DONE.\n"); -} - function initSubscriptionURI() { printfnq("Ensuring all subscriptions have a URI..."); diff --git a/tests/CommandInterperterTest.php b/tests/CommandInterperterTest.php index c97bfb1652..2d1824c69a 100644 --- a/tests/CommandInterperterTest.php +++ b/tests/CommandInterperterTest.php @@ -133,9 +133,9 @@ class CommandInterpreterTest extends PHPUnit_Framework_TestCase array('whois foo', 'WhoisCommand'), array('whois foo bar', null), - array('fav', null), +/* array('fav', null), array('fav foo', 'FavCommand'), - array('fav foo bar', null), + array('fav foo bar', null),*/ array('nudge', null), array('nudge foo', 'NudgeCommand'),