Commit b7178d21 authored by Siebrand Mazeland's avatar Siebrand Mazeland

Merge branch '1.0.x' of git://gitorious.org/statusnet/mainline into 1.0.x

parents 0de0ae2f 90a76315
<?php
/**
* Display a conversation in the browser
*
* PHP version 5
*
* @category Action
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2009, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
// XXX: not sure how to do paging yet,
// so set a 60-notice limit
require_once INSTALLDIR.'/lib/noticelist.php';
/**
* Conversation tree in the browser
*
* @category Action
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*/
class ConversationRepliesAction extends ConversationAction
{
function handle($args)
{
if ($this->boolean('ajax')) {
$this->showAjax();
} else {
parent::handle($args);
}
}
/**
* Show content.
*
* Display a hierarchical unordered list in the content area.
* Uses ConversationTree to do most of the heavy lifting.
*
* @return void
*/
function showContent()
{
$notices = Notice::conversationStream($this->id, null, null);
$ct = new FullThreadedNoticeList($notices, $this);
$cnt = $ct->show();
}
function showAjax()
{
header('Content-Type: text/xml;charset=utf-8');
$this->xw->startDocument('1.0', 'UTF-8');
$this->elementStart('html');
$this->elementStart('head');
$this->element('title', null, _('Notice'));
$this->elementEnd('head');
$this->elementStart('body');
$this->showContent();
$this->elementEnd('body');
$this->elementEnd('html');
}
}
class FullThreadedNoticeList extends ThreadedNoticeList
{
function newListItem($notice)
{
return new FullThreadedNoticeListItem($notice, $this->out);
}
}
class FullThreadedNoticeListItem extends ThreadedNoticeListItem
{
function initialItems()
{
return 1000; // @fixme
}
}
......@@ -44,6 +44,7 @@ class Fave extends Memcached_DataObject
common_log_db_error($fave, 'INSERT', __FILE__);
return false;
}
self::blow('fave:by_notice:%d', $fave->notice_id);
Event::handle('EndFavorNotice', array($profile, $notice));
}
......@@ -61,6 +62,7 @@ class Fave extends Memcached_DataObject
if (Event::handle('StartDisfavorNotice', array($profile, $notice, &$result))) {
$result = parent::delete();
self::blow('fave:by_notice:%d', $this->notice_id);
if ($result) {
Event::handle('EndDisfavorNotice', array($profile, $notice));
......@@ -208,4 +210,31 @@ class Fave extends Memcached_DataObject
return $fav;
}
/**
* Grab a list of profile who have favored this notice.
*
* @return ArrayWrapper masquerading as a Fave
*/
static function byNotice($noticeId)
{
$c = self::memcache();
$key = Cache::key('fave:by_notice:' . $noticeId);
$wrapper = $c->get($key);
if (!$wrapper) {
// @fixme caching & scalability!
$fave = new Fave();
$fave->notice_id = $noticeId;
$fave->find();
$list = array();
while ($fave->fetch()) {
$list[] = clone($fave);
}
$wrapper = new ArrayWrapper($list);
$c->set($key, $wrapper);
}
return $wrapper;
}
}
......@@ -496,6 +496,13 @@ class Notice extends Memcached_DataObject
if ($this->isPublic()) {
self::blow('public;last');
}
self::blow('fave:by_notice', $this->id);
if ($this->conversation) {
// In case we're the first, will need to calc a new root.
self::blow('notice:conversation_root:%d', $this->conversation);
}
}
/** save all urls in the notice to the db
......@@ -774,6 +781,35 @@ class Notice extends Memcached_DataObject
return false;
}
/**
* Grab the earliest notice from this conversation.
*
* @return Notice or null
*/
function conversationRoot()
{
if (!empty($this->conversation)) {
$c = self::memcache();
$key = Cache::key('notice:conversation_root:' . $this->conversation);
$notice = $c->get($key);
if ($notice) {
return $notice;
}
$notice = new Notice();
$notice->conversation = $this->conversation;
$notice->orderBy('CREATED');
$notice->limit(1);
$notice->find(true);
if ($notice->N) {
$c->set($key, $notice);
return $notice;
}
}
return null;
}
/**
* Pull up a full list of local recipients who will be getting
* this notice in their inbox. Results will be cached, so don't
......
......@@ -412,16 +412,20 @@ var SN = { // StatusNet
var replyItem = form.closest('li.notice-reply');
if (replyItem.length > 0) {
// If this is an inline reply, insert it in place.
// If this is an inline reply, remove the form...
var list = form.closest('.threaded-replies');
var placeholder = list.find('.notice-reply-placeholder');
replyItem.remove();
var id = $(notice).attr('id');
if ($("#"+id).length == 0) {
var parentNotice = replyItem.closest('li.notice');
replyItem.replaceWith(notice);
SN.U.NoticeInlineReplyPlaceholder(parentNotice);
$(notice).insertBefore(placeholder);
} else {
// Realtime came through before us...
replyItem.remove();
}
// ...and show the placeholder form.
placeholder.show();
} else if (notices.length > 0 && SN.U.belongsOnTimeline(notice)) {
// Not a reply. If on our timeline, show it at the top!
......@@ -604,8 +608,8 @@ var SN = { // StatusNet
// Update the existing form...
nextStep();
} else {
// Remove placeholder if any
list.find('li.notice-reply-placeholder').remove();
// Hide the placeholder...
var placeholder = list.find('li.notice-reply-placeholder').hide();
// Create the reply form entry at the end
var replyItem = $('li.notice-reply', list);
......@@ -615,7 +619,7 @@ var SN = { // StatusNet
var intermediateStep = function(formMaster) {
var formEl = document._importNode(formMaster, true);
replyItem.append(formEl);
list.append(replyItem);
list.append(replyItem); // *after* the placeholder
var form = replyForm = $(formEl);
SN.Init.NoticeFormSetup(form);
......@@ -662,6 +666,18 @@ var SN = { // StatusNet
SN.U.NoticeInlineReplyTrigger(notice);
return false;
});
$('li.notice-reply-comments a')
.live('click', function() {
var url = $(this).attr('href');
var area = $(this).closest('.threaded-replies');
$.get(url, {ajax: 1}, function(data, textStatus, xhr) {
var replies = $('.threaded-replies', data);
if (replies.length) {
area.replaceWith(document._importNode(replies[0], true));
}
});
return false;
});
},
/**
......@@ -1360,7 +1376,7 @@ var SN = { // StatusNet
if (cur == '' || cur == textarea.data('initialText')) {
var parentNotice = replyItem.closest('li.notice');
replyItem.remove();
SN.U.NoticeInlineReplyPlaceholder(parentNotice);
parentNotice.find('li.notice-reply-placeholder').show();
}
}
});
......
This diff is collapsed.
......@@ -100,10 +100,12 @@ class Action extends HTMLOutputter // lawsuit
{
if (Event::handle('StartShowHTML', array($this))) {
$this->startHTML();
$this->flush();
Event::handle('EndShowHTML', array($this));
}
if (Event::handle('StartShowHead', array($this))) {
$this->showHead();
$this->flush();
Event::handle('EndShowHead', array($this));
}
if (Event::handle('StartShowBody', array($this))) {
......@@ -471,11 +473,14 @@ class Action extends HTMLOutputter // lawsuit
$this->elementStart('div', array('id' => 'wrap'));
if (Event::handle('StartShowHeader', array($this))) {
$this->showHeader();
$this->flush();
Event::handle('EndShowHeader', array($this));
}
$this->showCore();
$this->flush();
if (Event::handle('StartShowFooter', array($this))) {
$this->showFooter();
$this->flush();
Event::handle('EndShowFooter', array($this));
}
$this->elementEnd('div');
......@@ -695,14 +700,17 @@ class Action extends HTMLOutputter // lawsuit
$this->elementStart('div', array('id' => 'site_nav_local_views_wrapper'));
if (Event::handle('StartShowLocalNavBlock', array($this))) {
$this->showLocalNavBlock();
$this->flush();
Event::handle('EndShowLocalNavBlock', array($this));
}
if (Event::handle('StartShowContentBlock', array($this))) {
$this->showContentBlock();
$this->flush();
Event::handle('EndShowContentBlock', array($this));
}
if (Event::handle('StartShowAside', array($this))) {
$this->showAside();
$this->flush();
Event::handle('EndShowAside', array($this));
}
$this->elementEnd('div');
......
......@@ -333,6 +333,9 @@ class Router
$m->connect('conversation/:id',
array('action' => 'conversation'),
array('id' => '[0-9]+'));
$m->connect('conversation/:id/replies',
array('action' => 'conversationreplies'),
array('id' => '[0-9]+'));
$m->connect('message/new', array('action' => 'newmessage'));
$m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => Nickname::DISPLAY_FMT));
......
......@@ -75,7 +75,15 @@ class ThreadedNoticeList extends NoticeList
break;
}
$convo = $this->notice->conversation;
// Collapse repeats into their originals...
$notice = $this->notice;
if ($notice->repeat_of) {
$orig = Notice::staticGet('id', $notice->repeat_of);
if ($orig) {
$notice = $orig;
}
}
$convo = $notice->conversation;
if (!empty($conversations[$convo])) {
// Seen this convo already -- skip!
continue;
......@@ -83,14 +91,10 @@ class ThreadedNoticeList extends NoticeList
$conversations[$convo] = true;
// Get the convo's root notice
// @fixme stream goes in wrong direction, this needs sane caching
//$notice = Notice::conversationStream($convo, 0, 1);
//$notice->fetch();
$notice = new Notice();
$notice->conversation = $this->notice->conversation;
$notice->orderBy('CREATED');
$notice->limit(1);
$notice->find(true);
$root = $notice->conversationRoot();
if ($root) {
$notice = $root;
}
try {
$item = $this->newListItem($notice);
......@@ -145,7 +149,10 @@ class ThreadedNoticeList extends NoticeList
class ThreadedNoticeListItem extends NoticeListItem
{
const INITIAL_ITEMS = 3;
function initialItems()
{
return 3;
}
function showContext()
{
......@@ -162,8 +169,9 @@ class ThreadedNoticeListItem extends NoticeListItem
function showEnd()
{
$max = $this->initialItems();
if (!$this->repeat) {
$notice = Notice::conversationStream($this->notice->conversation, 0, self::INITIAL_ITEMS + 2);
$notice = Notice::conversationStream($this->notice->conversation, 0, $max + 2);
$notices = array();
$cnt = 0;
$moreCutoff = null;
......@@ -173,7 +181,7 @@ class ThreadedNoticeListItem extends NoticeListItem
continue;
}
$cnt++;
if ($cnt > self::INITIAL_ITEMS) {
if ($cnt > $max) {
// boo-yah
$moreCutoff = clone($notice);
break;
......@@ -181,8 +189,15 @@ class ThreadedNoticeListItem extends NoticeListItem
$notices[] = clone($notice); // *grumble* inefficient as hell
}
$this->out->elementStart('ul', 'notices threaded-replies xoxo');
$item = new ThreadedNoticeListFavesItem($this->notice, $this->out);
$hasFaves = $item->show();
$item = new ThreadedNoticeListRepeatsItem($this->notice, $this->out);
$hasRepeats = $item->show();
if ($notices) {
$this->out->elementStart('ul', 'notices threaded-replies xoxo');
if ($moreCutoff) {
$item = new ThreadedNoticeListMoreItem($moreCutoff, $this->out);
$item->show();
......@@ -191,14 +206,16 @@ class ThreadedNoticeListItem extends NoticeListItem
$item = new ThreadedNoticeListSubItem($notice, $this->out);
$item->show();
}
}
if ($notices || $hasFaves || $hasRepeats) {
// @fixme do a proper can-post check that's consistent
// with the JS side
if (common_current_user()) {
$item = new ThreadedNoticeListReplyItem($notice, $this->out);
$item = new ThreadedNoticeListReplyItem($this->notice, $this->out);
$item->show();
}
$this->out->elementEnd('ul');
}
$this->out->elementEnd('ul');
}
parent::showEnd();
......@@ -227,6 +244,13 @@ class ThreadedNoticeListSubItem extends NoticeListItem
{
//
}
function showEnd()
{
$item = new ThreadedNoticeListInlineFavesItem($this->notice, $this->out);
$hasFaves = $item->show();
parent::showEnd();
}
}
/**
......@@ -265,7 +289,7 @@ class ThreadedNoticeListMoreItem extends NoticeListItem
function showMiniForm()
{
$id = $this->notice->conversation;
$url = common_local_url('conversation', array('id' => $id)) . '#notice-' . $this->notice->id;
$url = common_local_url('conversationreplies', array('id' => $id));
$notice = new Notice();
$notice->conversation = $id;
......@@ -316,4 +340,155 @@ class ThreadedNoticeListReplyItem extends NoticeListItem
$this->out->element('input', array('class' => 'placeholder',
'value' => _('Write a reply...')));
}
}
\ No newline at end of file
}
/**
* 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;
array_unshift($links, _m('FAVELIST', 'You'));
} else {
$profile = Profile::staticGet('id', $id);
if ($profile) {
$links[] = sprintf('<a href="%s" title="%s">%s</a>',
htmlspecialchars($profile->profileurl),
htmlspecialchars($profile->getBestName()),
htmlspecialchars($profile->nickname));
}
}
}
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 For building a list such as "You, bob, mary and 5 others have favored this notice".
return sprintf(_m('FAVELIST', '%1$s and %2$s'), implode(', ', $first), implode(', ', $last));
}
}
}
/**
* Placeholder for showing faves...
*/
class ThreadedNoticeListFavesItem extends NoticeListActorsItem
{
function getProfiles()
{
$fave = Fave::byNotice($this->notice->id);
$profiles = array();
while ($fave->fetch()) {
$profiles[] = $fave->user_id;
}
return $profiles;
}
function getListMessage($count, $you)
{
if ($count == 1 && $you) {
// darn first person being different from third person!
return _m('FAVELIST', 'You have favored this notice.');
} else {
// if 'you' is the first item,
return _m('FAVELIST', '%1$s has favored this notice.', '%1$s have favored this notice.', $count);
}
}
function showStart()
{
$this->out->elementStart('li', array('class' => 'notice-data notice-faves'));
}
function showEnd()
{
$this->out->elementEnd('li');
}
}
class ThreadedNoticeListInlineFavesItem extends ThreadedNoticeListFavesItem
{
function showStart()
{
$this->out->elementStart('div', array('class' => 'entry-content notice-faves'));
}
function showEnd()
{
$this->out->elementEnd('div');
}
}
/**
* Placeholder for showing faves...
*/
class ThreadedNoticeListRepeatsItem extends NoticeListActorsItem
{
function getProfiles()
{
$rep = $this->notice->repeatStream();
$profiles = array();
while ($rep->fetch()) {
$profiles[] = $rep->profile_id;
}
return $profiles;
}
function getListMessage($count, $you)
{
if ($count == 1 && $you) {
// darn first person being different from third person!
return _m('REPEATLIST', 'You have repeated this notice.');
} else {
// if 'you' is the first item,
return _m('REPEATLIST', '%1$s has repeated this notice.', '%1$s have repeated this notice.', $count);
}
}
function showStart()
{
$this->out->elementStart('li', array('class' => 'notice-data notice-repeats'));
}
function showEnd()
{
$this->out->elementEnd('li');
}
}
......@@ -242,4 +242,15 @@ class XMLOutputter
{
$this->xw->writeComment($txt);
}
/**
* Flush output buffers
*
* @return void
*/
function flush()
{
$this->xw->flush();
}
}
......@@ -2995,8 +2995,37 @@ X-OIM-Sequence-Num: 1
// no ticket found!
if (count($matches) == 0) {
$this->debug_message('*** Could not get passport ticket!');
return false;
// Since 2011/2/15, the return value will be Compact2, not PPToken2
// we need ticket and secret code
// RST1: messengerclear.live.com
// <wsse:BinarySecurityToken Id="Compact1">t=tick&p=</wsse:BinarySecurityToken>
// <wst:BinarySecret>binary secret</wst:BinarySecret>
// RST2: messenger.msn.com
// <wsse:BinarySecurityToken Id="PPToken2">t=tick</wsse:BinarySecurityToken>
// RST3: contacts.msn.com
// <wsse:BinarySecurityToken Id="Compact3">t=tick&p=</wsse:BinarySecurityToken>
// RST4: messengersecure.live.com
// <wsse:BinarySecurityToken Id="Compact4">t=tick&p=</wsse:BinarySecurityToken>
// RST5: spaces.live.com
// <wsse:BinarySecurityToken Id="Compact5">t=tick&p=</wsse:BinarySecurityToken>
// RST6: storage.msn.com
// <wsse:BinarySecurityToken Id="Compact6">t=tick&p=</wsse:BinarySecurityToken>
preg_match("#".
"<wsse\:BinarySecurityToken Id=\"Compact1\">(.*)</wsse\:BinarySecurityToken>(.*)".
"<wst\:BinarySecret>(.*)</wst\:BinarySecret>(.*)".
"<wsse\:BinarySecurityToken Id=\"Compact2\">(.*)</wsse\:BinarySecurityToken>(.*)".
"<wsse\:BinarySecurityToken Id=\"Compact3\">(.*)</wsse\:BinarySecurityToken>(.*)".
"<wsse\:BinarySecurityToken Id=\"Compact4\">(.*)</wsse\:BinarySecurityToken>(.*)".
"<wsse\:BinarySecurityToken Id=\"Compact5\">(.*)</wsse\:BinarySecurityToken>(.*)".
"<wsse\:BinarySecurityToken Id=\"Compact6\">(.*)</wsse\:BinarySecurityToken>(.*)".
"#",
$data, $matches);
// no ticket found!
if (count($matches) == 0) {
$this->debug_message("*** Can't get passport ticket!");
return false;
}
}
//$this->debug_message(var_export($matches, true));
......
......@@ -267,7 +267,7 @@ class PollPlugin extends MicroAppPlugin
{
$object = new ActivityObject();
$object->id = $notice->uri;
$object->type = self::POLL_OBJECT;
$object->type = self::POLL_RESPONSE_OBJECT;
$object->title = $notice->content;
$object->summary = $notice->content;
$object->link = $notice->bestUrl();
......@@ -290,7 +290,7 @@ class PollPlugin extends MicroAppPlugin
{
$object = new ActivityObject();
$object->id = $notice->uri;
$object->type = self::POLL_RESPONSE_OBJECT;
$object->type = self::POLL_OBJECT;