[Slashdotjp-dev 1045] [572] merged from upstream/2.5.0.200 branch

アーカイブの一覧に戻る

svnno****@sourc***** svnno****@sourc*****
2008年 4月 7日 (月) 09:26:30 JST


Revision: 572
          http://svn.sourceforge.jp/cgi-bin/viewcvs.cgi?root=slashdotjp&view=rev&rev=572
Author:   tach
Date:     2008-04-07 09:26:30 +0900 (Mon, 07 Apr 2008)

Log Message:
-----------
merged from upstream/2.5.0.200 branch

Modified Paths:
--------------
    slashjp/trunk/Slash/Apache/User/User.pm
    slashjp/trunk/Slash/DB/MySQL/MySQL.pm
    slashjp/trunk/Slash/Utility/Comments/Comments.pm
    slashjp/trunk/Slash/Utility/Environment/Environment.pm
    slashjp/trunk/debian/changelog
    slashjp/trunk/plugins/Ajax/PLUGIN
    slashjp/trunk/plugins/Ajax/htdocs/ajax.pl
    slashjp/trunk/plugins/Ajax/htdocs/images/admin.js
    slashjp/trunk/plugins/Ajax/htdocs/images/common.js
    slashjp/trunk/plugins/Ajax/htdocs/images/nodnix.js
    slashjp/trunk/plugins/Ajax/htdocs/images/sd_autocomplete.js
    slashjp/trunk/plugins/Ajax/htdocs/images/sd_calendar.js
    slashjp/trunk/plugins/Ajax/htdocs/images/slashbox.js
    slashjp/trunk/plugins/Ajax/templates/data;ajax;default
    slashjp/trunk/plugins/Ajax/templates/edit_comment;ajax;default
    slashjp/trunk/plugins/Console/console.pl
    slashjp/trunk/plugins/FireHose/FireHose.pm
    slashjp/trunk/plugins/FireHose/templates/data;firehose;default
    slashjp/trunk/plugins/FireHose/templates/dispTopicFireHose;misc;default
    slashjp/trunk/plugins/FireHose/templates/fireHoseForm;misc;default
    slashjp/trunk/plugins/FireHose/templates/firehose_pages;misc;default
    slashjp/trunk/plugins/FireHose/templates/list;firehose;default
    slashjp/trunk/plugins/HumanConf/HumanConf.pm
    slashjp/trunk/plugins/Journal/journal.pl
    slashjp/trunk/plugins/Moderation/Moderation.pm
    slashjp/trunk/plugins/ResKey/MANIFEST
    slashjp/trunk/plugins/ResKey/ResKey/Checks/Duration.pm
    slashjp/trunk/plugins/ResKey/ResKey/Key.pm
    slashjp/trunk/plugins/ResKey/example.plx
    slashjp/trunk/plugins/ResKey/mysql_dump.sql
    slashjp/trunk/plugins/ResKey/templates/data;reskey;default
    slashjp/trunk/plugins/TagDataView/TagDataView.pm
    slashjp/trunk/plugins/TagModeration/TagModeration.pm
    slashjp/trunk/plugins/Tags/Clout/Describe.pm
    slashjp/trunk/plugins/Tags/Clout/Vote.pm
    slashjp/trunk/plugins/Tags/PLUGIN
    slashjp/trunk/plugins/Tags/Tags.pm
    slashjp/trunk/plugins/Tags/mysql_dump.sql
    slashjp/trunk/plugins/Tags/tags.pl
    slashjp/trunk/plugins/Tags/templates/usertaghistory;users;default
    slashjp/trunk/plugins/Tags/templates/usertags;users;default
    slashjp/trunk/sql/mysql/defaults.sql
    slashjp/trunk/sql/mysql/upgrades
    slashjp/trunk/tagboxes/FHEditorPop/FHEditorPop.pm
    slashjp/trunk/tagboxes/FHEditorPop/mysql_dump.sql
    slashjp/trunk/tagboxes/Top/Top.pm
    slashjp/trunk/themes/slashcode/THEME
    slashjp/trunk/themes/slashcode/htdocs/comments.css
    slashjp/trunk/themes/slashcode/htdocs/comments.pl
    slashjp/trunk/themes/slashcode/htdocs/images/comments.js
    slashjp/trunk/themes/slashcode/htdocs/slashcode_lite.css
    slashjp/trunk/themes/slashcode/htdocs/users.pl
    slashjp/trunk/themes/slashcode/templates/dispComment;misc;default
    slashjp/trunk/themes/slashcode/templates/dispLinkComment;misc;default
    slashjp/trunk/themes/slashcode/templates/editComm;users;default
    slashjp/trunk/themes/slashcode/templates/printCommComments;misc;default
    slashjp/trunk/themes/slashcode/templates/printCommentsMain;misc;default
    slashjp/trunk/utils/createTestTags

Added Paths:
-----------
    slashjp/trunk/plugins/Ajax/templates/hc_comment;ajax;default
    slashjp/trunk/plugins/ResKey/ResKey/Checks/HumanConf.pm
    slashjp/trunk/plugins/Tags/templates/usertagnames;users;default
    slashjp/trunk/plugins/Tags/templates/usertagsforname;users;default
    slashjp/trunk/tagboxes/RecentTags/
    slashjp/trunk/themes/slashcode/htdocs/images/corner_w_bl.png
    slashjp/trunk/themes/slashcode/htdocs/images/corner_w_br.png
    slashjp/trunk/themes/slashcode/htdocs/images/corner_w_tl.png
    slashjp/trunk/themes/slashcode/htdocs/images/corner_w_tr.png
    slashjp/trunk/themes/slashcode/htdocs/users2.pl
    slashjp/trunk/themes/slashcode/templates/userInfo2;users;default
    slashjp/trunk/themes/slashcode/templates/userboxes2;misc;default


-------------- next part --------------
Modified: slashjp/trunk/Slash/Apache/User/User.pm
===================================================================
--- slashjp/trunk/Slash/Apache/User/User.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/Slash/Apache/User/User.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -597,7 +597,14 @@
                 $r->filename($constants->{basedir} . '/help.pl');
                 return OK;
         }
-        
+       
+        # This is a temporary addition!
+        if ($uri =~ m[^/(?:%5[eE]|\^)]) {
+                $r->uri('/users2.pl');
+                $r->filename($constants->{basedir} . '/users2.pl');
+                return OK;
+        }
+ 
 	# for self-references (/~/ and /my/)
 	if (($saveuri =~ m[^/(?:%7[eE]|~)] && $uri =~ m[^/~ (?: /(.*) | /? ) $]x)
 		# /my/ or /my can match, but not /mything
@@ -612,6 +619,7 @@
 		}
 
 		my($op, $extra) = split /\//, $string, 2;
+		$extra ||= '';
 
 		my $logged_in = $r->header_in('Cookie') =~ $USER_MATCH;
 		my $try_login = !$logged_in && $logtoken;
@@ -634,9 +642,9 @@
 				$found_the_op = 1;
 				if ($op eq 'journal') {
 					my $args;
-					if ($extra && $extra =~ /^\d+$/) {
+					if ($extra =~ /^\d+$/) {
 						$args = "id=$extra&op=edit";
-					} elsif ($extra && $extra eq 'friends') {
+					} elsif ($extra eq 'friends') {
 						$args = "op=friendview";
 					} else {
 						$args = "op=list";
@@ -703,7 +711,10 @@
 					$r->filename($constants->{basedir} . '/journal.pl');
 
 				} elsif ($op eq 'tags') {
-					$r->args("op=showtags");
+					my $args = 'op=showtags';
+					# XXX "!" is a 'reserved' char in URI, escape it here?
+					$args .= "&tagname=$extra" if $extra;
+					$r->args($args);
 					$r->uri('/users.pl');
 					$r->filename($constants->{basedir} . '/users.pl');
 
@@ -861,7 +872,10 @@
 			$r->filename($constants->{basedir} . '/journal.pl');
 
 		} elsif ($op eq 'tags') {
-			$r->args("op=showtags&nick=$nick&uid=$uid");
+			my $args = "op=showtags&nick=$nick&uid=$uid";
+			# XXX "!" is a 'reserved' char in URI, escape it here?
+			$args .= "&tagname=$extra" if $extra;
+			$r->args($args);
 			$r->uri('/users.pl');
 			$r->filename($constants->{basedir} . '/users.pl');
 

Modified: slashjp/trunk/Slash/DB/MySQL/MySQL.pm
===================================================================
--- slashjp/trunk/Slash/DB/MySQL/MySQL.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/Slash/DB/MySQL/MySQL.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -3491,6 +3491,7 @@
 	# Of course, markStoryClean and -Dirty work too
 
 	my($dirty_change, $dirty_newval);
+
 	if ($change_hr->{writestatus}) {
 		$dirty_change = 1;
 		$dirty_newval =	  $change_hr->{writestatus} eq 'dirty'	? 1 : 0;
@@ -3504,6 +3505,11 @@
 		delete $change_hr->{is_dirty}
 	}
 
+	if ($change_hr->{introtext} && $change_hr->{introtext} =~ /href=\"SELF\"/) {
+		my $link_url = $self->_getStorySelfLink($stoid, $change_hr);
+		$change_hr->{introtext} =~ s/href=\"SELF\"/href="$link_url"/;
+	}
+
 	$change_hr->{is_archived} = $change_hr->{is_archived} ? 'yes' : 'no'
 		if defined $change_hr->{is_archived};
 	$change_hr->{in_trash} = $change_hr->{in_trash} ? 'yes' : 'no'
@@ -7923,6 +7929,7 @@
 
 	$signoff_type ||= '';
 	$self->sqlInsert("signoff", { stoid => $stoid, uid => $uid, signoff_type => $signoff_type });
+	$self->setStory($stoid, { thumb_signoff_needed => 0 });
 
 	if ($constants->{plugin}{FireHose}) {
 		my $firehose = getObject("Slash::FireHose");
@@ -7993,6 +8000,19 @@
 	);
 }
 
+sub deleteSignoffsForStory {
+	my($self, $stoid) = @_;
+	my $constants = getCurrentStatic();
+	my $stoid_q = $self->sqlQuote($stoid);
+	$self->sqlDelete("signoff", "stoid=$stoid_q");
+	if ($constants->{plugin}{FireHose}) {
+		my $firehose = getObject("Slash::FireHose");
+		my ($id) = $self->sqlSelect("id", "firehose", "type='story' and srcid=$stoid_q");
+		$firehose->setFireHose($id, { signoffs => '' });
+
+	}
+}
+
 sub getSignoffsInLastMinutes {
 	my ($self, $mins) = @_;
 	$mins ||= getCurrentStatic("admin_timeout");
@@ -12629,6 +12649,23 @@
 	return $answer;
 }
 
+sub _getStorySelfLink {
+	my($self, $stoid, $change_hr) = @_; 
+	my $story = $self->getStory($stoid);
+	my $data = {};
+	my $link  = $change_hr->{title} || $story->{title};
+	my $tid   = $change_hr->{tid} || $story->{tid};
+	my $skin  = $change_hr->{primaryskid} || $story->{primary_skid};
+
+	my $story_link_ar = linkStory({
+		sid 	=> $story->{sid},
+		link 	=> $link,
+		tid	=> $tid,
+		skin	=> $skin
+	});
+	return $story_link_ar->[0];
+}
+
 ########################################################
 sub DESTROY {
 	my($self) = @_;

Modified: slashjp/trunk/Slash/Utility/Comments/Comments.pm
===================================================================
--- slashjp/trunk/Slash/Utility/Comments/Comments.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/Slash/Utility/Comments/Comments.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -38,7 +38,7 @@
 @EXPORT		= qw(
 	constrain_score dispComment displayThread printComments
 	jsSelectComments commentCountThreshold commentThresholds discussion2
-	tempUofmLinkGenerate tempUofmCipherObj selectComments preProcessReplyForm
+	selectComments preProcessReplyForm
 	getPoints preProcessComment postProcessComment prevComment saveComment
 );
 
@@ -156,8 +156,7 @@
 
 #slashProf("sC main sort", "sC setup");
 	my($oldComment, %old_comments);
-	# XXXd2 disable for sub-threads for now ($cid)
-	if ($discussion2 && !$cid && !$options->{no_d2}) {
+	if ($discussion2 && !$options->{no_d2}) {
 		my $limits = $slashdb->getDescriptions('d2_comment_limits');
 		my $max = $d2_comment_q ? $limits->{ $d2_comment_q } : 0;
 		$max = int($max/2) if $shtml;
@@ -165,6 +164,19 @@
 		$options->{existing} ||= {};
 		@$thisComment = sort { $a->{cid} <=> $b->{cid} } @$thisComment;
 
+		# we need to filter which comments are descendants of $cid
+		my %cid_seen;
+		if ($cid) {
+			# for display later
+			$user->{state}{d2_defaultclass}{$cid} = 'full';
+			# this only works because we are already in cid order
+			for my $C (@$thisComment) {
+				if ($cid == $C->{cid} || $cid_seen{$C->{pid}}) {
+					$cid_seen{$C->{cid}} = 1;
+				}
+			}
+		}
+
 		my $sort_comments;
 		if (!$user->{d2_comment_order}) { # score
 			$sort_comments = [ sort {
@@ -176,16 +188,21 @@
 			$sort_comments = $thisComment;
 		}
 
+
 		for my $C (@$sort_comments) {
 			next if $options->{existing}{$C->{cid}};
 
 			if ($max && @new_comments >= $max) {
 				if ($cid) {
+					# still include $cid even if it would
+					# otherwise be excluded (should only
+					# matter if not sorting by date
 					push @new_comments, $C if $cid == $C->{cid};
 				} else {
 					last;
 				}
 			} else {
+				next if $cid && !$cid_seen{$C->{cid}};
 				push @new_comments, $C;
 			}
 		}
@@ -193,9 +210,9 @@
 		my @seen;
 		my $lastcid = 0;
 		my %check = (%{$options->{existing}}, map { $_->{cid} => 1 } @new_comments);
-		for my $cid (sort { $a <=> $b } keys(%check)) {
-			push @seen, $lastcid ? $cid - $lastcid : $cid;
-			$lastcid = $cid;
+		for my $this_cid (sort { $a <=> $b } keys(%check)) {
+			push @seen, $lastcid ? $this_cid - $lastcid : $this_cid;
+			$lastcid = $this_cid;
 		}
 		$comments->{0}{d2_seen} = join ',', @seen;
 
@@ -285,32 +302,38 @@
 	}
 
 	# get the total visible kids for each comment --Pater
-	countTotalVisibleKids($comments);
+	countTotalVisibleKids($comments) unless $discussion2;
 
-	_print_cchp($discussion, $count, $comments->{0}{totals});
-
-##slashProf("sC reparenting", "sC counting");
-#slashProf("sC reparenting");
-	reparentComments($comments, $reader, $options);
-
-##slashProf("sC d2 fudging", "sC reparenting");
-#slashProf("", "sC reparenting");
+##slashProf("sC d2 fudging", "sC counting");
 	if ($oldComment) {
-		for my $cid (sort { $a <=> $b } keys %$comments) {
-			my $C = $comments->{$cid};
+		my @new_seen;
+		for my $this_cid (sort { $a <=> $b } keys %$comments) {
+			next unless $this_cid;
+			my $C = $comments->{$this_cid};
 
 			# && !$options->{existing}{ $C->{pid} }
 			while ($C->{pid}) {
 				my $parent = $comments->{ $C->{pid} } || {};
+
 				if (!$parent || !$parent->{kids} || !$parent->{cid} || !defined($parent->{pid}) || !defined($parent->{points})) {
-					$parent = $comments->{ $C->{pid} } = {
-						cid    => $C->{pid},
-						pid    => ($old_comments{ $C->{pid} } && $old_comments{ $C->{pid} }{ pid }) || 0,
-						kids   => [ ],
-						points => -2,
-						dummy  => 1,
-						%$parent,
-					};
+					# parents of our main cid, so spend time
+					# finding it ...
+					if ($cid && $C->{pid} < $cid) {
+						$user->{state}{d2_defaultclass}{$C->{pid}} = 'oneline';
+						$parent = $old_comments{ $C->{pid} };
+						push @new_seen, $C->{pid};
+						$count++;
+					} else {
+						$parent = {
+							cid    => $C->{pid},
+							pid    => ($old_comments{ $C->{pid} } && $old_comments{ $C->{pid} }{ pid }) || 0,
+							kids   => [ ],
+							points => -2,
+							dummy  => 1,
+							%$parent,
+						};
+					}
+					$comments->{ $C->{pid} } = $parent;
 				}
 
 				unless (grep { $_ == $C->{cid} } @{$parent->{kids}}) {
@@ -325,10 +348,29 @@
 				$C = $parent;
 			}
 		}
+
+		# fix d2_seen
+		my @seen;
+		my $lastcid = 0;
+		for my $this_cid (sort { $a <=> $b } @new_seen) {
+			push @seen, $lastcid ? $this_cid - $lastcid : $this_cid;
+			$lastcid = $this_cid;
+		}
+		my @old_seen = split /,/, $comments->{0}{d2_seen};
+		if (@seen && @old_seen) {
+			$old_seen[0] = $old_seen[0] - $lastcid;
+		}
+		$comments->{0}{d2_seen} = join ',', @seen, @old_seen;
 	}
 
 ##slashProf("", "sC d2 fudging");
 
+	_print_cchp($discussion, $count, $comments->{0}{totals});
+
+#slashProf("sC reparenting");
+	reparentComments($comments, $reader, $options);
+#slashProf("", "sC reparenting");
+
 	return($comments, $count);
 }
 
@@ -344,7 +386,6 @@
 	$gSkin     ||= getCurrentSkin();
 
 	my $id = $form->{sid};
-	my $pid = $form->{cid} || 0;
 	return unless $id;
 
 	my $threshold = defined $user->{d2_threshold} ? $user->{d2_threshold} : $user->{threshold};
@@ -360,12 +401,8 @@
 	my($comments) = $user->{state}{selectComments}{comments};
 
 	my $d2_seen_0 = $comments->{0}{d2_seen} || '';
-	#delete $comments->{0}; # non-comment data
-	if ($pid && exists $comments->{$pid}) {
-		$comments = _get_thread($comments, $pid);
-	}
 
-	my @roots = $pid ? $pid : @{$comments->{$pid}{kids}};
+	my @roots = @{$comments->{0}{kids} || []};
 	my %roots_hash = ( map { $_ => 1 } @roots );
 	my $thresh_totals;
 
@@ -392,7 +429,7 @@
 			}
 		}
 
-		$thresh_totals = commentCountThreshold($comments, $pid, \%roots_hash);
+		$thresh_totals = commentCountThreshold($comments, 0, \%roots_hash);
 		$comments = $comments_new;
 	}
 
@@ -408,6 +445,7 @@
 	$user->{is_anon}       ||= 0;
 	$user->{is_admin}      ||= 0;
 	$user->{is_subscriber} ||= 0;
+	my $root_comment = $user->{state}{selectComments}{cidorpid} || 0;
 
 	my $extra = '';
 	if ($d2_seen_0) {
@@ -435,7 +473,7 @@
 
 thresh_totals = $anon_thresh;
 
-root_comment = $pid;
+root_comment = $root_comment;
 root_comments = $anon_roots;
 root_comments_hash = $anon_rootsh;
 max_cid = $max_cid;
@@ -723,6 +761,7 @@
 
 	my $max_depth_allowed = $user->{state}{max_depth} || $constants->{max_depth} || 7;
 
+	# even if !reparent, we still want to be here so we can set comments at max depth
 	return if $user->{state}{noreparent} || (!$max_depth_allowed && !$user->{reparent});
 
 	# Adjust the max_depth_allowed for the root pid or cid.
@@ -731,7 +770,7 @@
 	# when $form->{cid|pid} is set.  And besides, max depth we
 	# display is for display, so it should be based on how much
 	# we're displaying, not on absolute depth of this thread.
-	my $root_cid_or_pid = $form->{cid} || $form->{pid} || 0;
+	my $root_cid_or_pid = discussion2($user) ? 0 : ($form->{cid} || $form->{pid} || 0);
 	if ($root_cid_or_pid) {
 		my $tmpcid = $root_cid_or_pid;
 		while ($tmpcid) {
@@ -965,8 +1004,9 @@
 #slashProf("", "selectComments");
 	if ($discussion2) {
 		$user->{state}{selectComments} = {
-			comments	=> $comments,
-			count		=> $count
+			cidorpid	=> $cidorpid,
+			comments        => $comments,
+			count           => $count
 		};
 	}
 
@@ -1026,7 +1066,7 @@
 	return if $user->{state}{nocomment} || $user->{mode} eq 'nocomment';
 
 	my($comment, $next, $previous);
-	if ($cid) {
+	if ($cid && !$discussion2) {
 		my($next, $previous);
 		$comment = $comments->{$cid};
 		if (my $sibs = $comments->{$comment->{pid}}{kids}) {
@@ -1241,6 +1281,9 @@
 
 		my $highlight = ($comment->{points} >= $highlightthresh && $class ne 'hidden') ? 1 : 0;
 		$class = 'full' if $highlight;
+		if ($discussion2 && $user->{state}{d2_defaultclass}{$cid}) {
+			$class = $user->{state}{d2_defaultclass}{$cid};
+		}
 		$comment->{class} = $class;
 
 		$user->{state}{comments}{totals}{$class}++ unless $comment->{dummy};
@@ -1902,8 +1945,10 @@
 		$score_to_display .= "Score:";
 		if (length $comment->{points}) {
 			$score_to_display .= $comment->{points};
-			$score_to_display = qq[<a href="#" onclick="getModalPrefs('modcommentlog', 'Moderation Comment Log', $comment->{cid}); return false">$score_to_display</a>]
-				if $constants->{modal_prefs_active} && !$user->{is_anon};
+			if ($constants->{modal_prefs_active}) {
+				my $func = "getModalPrefs('modcommentlog', 'Moderation Comment Log', $comment->{cid})";
+				$score_to_display = qq[<a href="#" onclick="$func; return false">$score_to_display</a>];
+			}
 		} else {
 			$score_to_display .= '?';
 		}
@@ -1991,25 +2036,23 @@
 		&& $comment->{nickname} ne "-") { # this last test probably useless
 		my @link = ( );
 
-		push @link, (qq'<span id="reply_link_$comment->{cid}">' . linkComment({
+		push @link, (qq'<span id="reply_link_$comment->{cid}" class="nbutton"><p><b>' . linkComment({
 			sid	=> $comment->{sid},
 			pid	=> $comment->{cid},
 			op	=> 'Reply',
 			subject	=> 'Reply to This',
 			subject_only => 1,
-			onclick	=> (($discussion2 && (!$constants->{subscribe} || $user->{is_subscriber})) ? "replyTo($comment->{cid}); return false;" : '')
-		}) . '</span>') unless $user->{state}{discussion_archived};
+			onclick	=> (($discussion2 && !$user->{is_anon}) ? "replyTo($comment->{cid}); return false;" : '')
+		}) . '</b></p></span>') unless $user->{state}{discussion_archived};
 
-		push @link, linkComment({
+		push @link, (qq'<span class="nbutton"><p><b>' . linkComment({
 			sid	=> $comment->{sid},
 			cid	=> $comment->{original_pid},
 			pid	=> $comment->{original_pid},
 			subject	=> 'Parent',
 			subject_only => 1,
 			onclick	=> ($discussion2 ? "return selectParent($comment->{original_pid})" : '')
-		}, 1) if $comment->{original_pid};# && !($discussion2 &&
-#			(!$form->{cid} || $form->{cid} != $comment->{cid})
-#		);
+		}, 1) . '</b></p></span>') if $comment->{original_pid};
 
 #use Data::Dumper; print STDERR "_hard_dispComment createSelect can_mod='$can_mod' disc_arch='$user->{state}{discussion_archived}' modd_arch='$constants->{comments_moddable_archived}' cid='$comment->{cid}' reasons: " . Dumper($reasons);
 
@@ -2025,10 +2068,10 @@
 		push @link, qq|<input type="checkbox" name="del_$comment->{cid}">|
 			if $user->{is_admin};
 
-		my $link = join(" | ", @link);
+		my $link = join(" ", @link);
 
 		if (@link) {
-			$commentsub = "[ $link ]";
+			$commentsub = $link;
 		}
 
 	}
@@ -2084,6 +2127,7 @@
 
 	my $class = $comment->{class}; 
 	my $classattr = $discussion2 ? qq[ class="$class"] : '';
+	my $contain = $class eq 'full' ? ' contain' : '';
 
 	my $head = $discussion2 ? <<EOT1 : <<EOT2;
 			<h4><a id="comment_link_$comment->{cid}" name="comment_link_$comment->{cid}" href="$gSkin->{rootdir}/comments.pl?sid=$comment->{sid}&amp;cid=$comment->{cid}" onclick="return setFocusComment($comment->{cid})">$comment->{subject}</a>
@@ -2093,7 +2137,7 @@
 
 	my $return = '';
 	$return = <<EOT if !$options->{noshow_show};
-<li id="tree_$comment->{cid}" class="comment">
+<li id="tree_$comment->{cid}" class="comment$contain">
 <div id="comment_status_$comment->{cid}" class="commentstatus"></div>
 <div id="comment_$comment->{cid}"$classattr>
 EOT
@@ -2506,43 +2550,11 @@
 	if (getCurrentStatic('no_d2')) {
 		return 0;
 	}
-	return $user->{discussion2} =~ /^(?:slashdot|uofm)$/
+	return $user->{discussion2} eq 'slashdot'
 		? $user->{discussion2} : 0;
 }
 
 
-sub tempUofmLinkGenerate {
-	require URI::Escape;
-
-	my $constants = getCurrentStatic();
-	my $user = getCurrentUser();
-
-	my $cipher = tempUofmCipherObj() or return;
-
-	my $encrypted = $cipher->encrypt($user->{uid} . '|' . $user->{nickname});
-	return sprintf($constants->{uofm_address}, URI::Escape::uri_escape($encrypted));
-}
-
-sub tempUofmCipherObj {
-	my $constants = getCurrentStatic();
-	return unless $constants->{uofm_key} && $constants->{uofm_iv};
-
-	require Crypt::CBC;
-
-	my $cipher = Crypt::CBC->new({
-		key		=> $constants->{uofm_key},
-		iv		=> $constants->{uofm_iv},
-		cipher		=> 'Blowfish',
-		regenerate_key	=> 0,
-		padding		=> 'null',
-		prepend_iv	=> 0
-	});
-
-	return $cipher;
-}
-
-
-
 1;
 
 __END__

Modified: slashjp/trunk/Slash/Utility/Environment/Environment.pm
===================================================================
--- slashjp/trunk/Slash/Utility/Environment/Environment.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/Slash/Utility/Environment/Environment.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -1884,10 +1884,12 @@
 		colorblock	=> sub { $_[0] =~ s|[^\w#,]+||g				},
 # What I actually want to do for userfield is allow it to match
 # [\w.]+, or pass emailValid(), or be changed to the return value
-# from nickFix().  For technical reasons I'm putting that off
-# until probably next week.  Until then this breaks some very
-# minor functionality. - Jamie 2008-01-09
+# from nickFix().  But nickFix() uses constants, which might not
+# be set up at this point. - Jamie 2008-01-09
 		userfield	=> sub { $_[0] =~ s|[^\w.@ -]||g			},
+# Ditto here, really - Jamie 2008-03-24
+		tagname		=> sub { $_[0] = '' unless
+					 $_[0] =~ /^\!?[a-z][a-z0-9]{0,62}$/		},
 	);
 
 

Modified: slashjp/trunk/debian/changelog
===================================================================
--- slashjp/trunk/debian/changelog	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/debian/changelog	2008-04-07 00:26:30 UTC (rev 572)
@@ -1,3 +1,9 @@
+slash (2.5.0.200-1) unstable; urgency=low
+
+  * New upstream CVS release
+
+ -- Taku YASUI <tach****@osdn*****>  Mon, 07 Apr 2008 09:26:13 +0900
+
 slash (2.5.0.198-1) unstable; urgency=low
 
   * New upstream CVS release

Modified: slashjp/trunk/plugins/Ajax/PLUGIN
===================================================================
--- slashjp/trunk/plugins/Ajax/PLUGIN	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Ajax/PLUGIN	2008-04-07 00:26:30 UTC (rev 572)
@@ -36,6 +36,7 @@
 template=templates/data;ajax;default
 template=templates/datewidget;misc;default
 template=templates/edit_comment;ajax;default
+template=templates/hc_comment;ajax;default
 template=templates/modal_footer;misc;default
 template=templates/prefs_d2;ajax;default
 template=templates/prefs_d2_posting;ajax;default

Modified: slashjp/trunk/plugins/Ajax/htdocs/ajax.pl
===================================================================
--- slashjp/trunk/plugins/Ajax/htdocs/ajax.pl	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Ajax/htdocs/ajax.pl	2008-04-07 00:26:30 UTC (rev 572)
@@ -40,8 +40,6 @@
 	);
 #	print STDERR "AJAX3 $$: $user->{uid}, $op\n";
 
-#$Slash::ResKey::DEBUG = 2;
-
 	$ops->{$op}{function} ||= loadCoderef($ops->{$op}{class}, $ops->{$op}{subroutine});
 	$op = 'default' unless $ops->{$op}{function};
 
@@ -58,7 +56,7 @@
 
 	if ($reskey_name ne 'NA') {
 		my $reskey = getObject('Slash::ResKey');
-		my $rkey = $reskey->key($reskey_name);
+		my $rkey = $reskey->key($reskey_name); #, { debug => 1 });
 		if (!$rkey) {
 			print STDERR scalar(localtime) . " ajax.pl main no rkey for op='$op' name='$reskey_name'\n";
 			return;
@@ -72,17 +70,17 @@
 			$rkey->use;
 		}
 		if (!$rkey->success) {
+			# feel free to send msgdiv => 'thisdivhere' to the ajax call,
+			# and any reskey error messages will be sent to it
 			if ($form->{msgdiv}) {
 				header_ajax({ content_type => 'application/json' });
 				(my $msgdiv = $form->{msgdiv}) =~ s/[^\w-]+//g;
 				print Data::JavaScript::Anon->anon_dump({
-					html	=> { $msgdiv => $rkey->errstr },
+					html	  => { $msgdiv => $rkey->errstr },
+					eval_last => "\$('#$msgdiv').show()"
 				});
 			}
-			printf STDERR "AJAXE %d: UID:%d, op:%s: %s (%s:%s:%s:%s:%s:%s:%s)\n",
-				$$, $user->{uid}, $op, $rkey->errstr, $rkey->reskey,
-				$rkey->type, $rkey->resname, $rkey->rkrid, $rkey->code, $rkey->static,
-				$user->{srcids}{ 24 };
+			$rkey->ERROR($op);
 			return;
 		}
 	}
@@ -281,12 +279,21 @@
 	my $discussion = $slashdb->getDiscussion($sid);
 	my $comment = preProcessComment($form, $user, $discussion, \$error_message);
 	if (!$error_message) {
-		$options->{rkey}->use or $error_message = $options->{rkey}->errstr;
+		unless ($options->{rkey}->use) {
+			$error_message = $options->{rkey}->errstr;
+		}
 	}
 	$saved_comment = saveComment($form, $comment, $user, $discussion, \$error_message)
 		unless $error_message;
 	my $cid = $saved_comment && $saved_comment ne '-1' ? $saved_comment->{cid} : 0;
 
+	if ($error_message) {
+		$error_message = getData('inline preview warning') . $error_message
+			unless $options->{rkey}->death;
+		# go back to HumanConf if we still have errors left to display
+		$error_message .= slashDisplay('hc_comment', { pid => $pid }, { Return => 1 });
+	}
+
 	$options->{content_type} = 'application/json';
 	my %to_dump = ( cid => $cid, error => $error_message );
 #use Data::Dumper; print STDERR Dumper \%to_dump;
@@ -301,15 +308,18 @@
 
 	$user->{state}{ajax_accesslog_op} = 'comments_preview_reply';
 
-	my($error_message, $preview, $html);
+	my $html = my $error_message = '';
 	my $discussion = $slashdb->getDiscussion($sid);
 	my $comment = preProcessComment($form, $user, $discussion, \$error_message);
 	if ($comment && $comment ne '-1') {
-		$preview = postProcessComment({ %$comment, %$form, %$user }, 0, $discussion);
+		my $preview = postProcessComment({ %$comment, %$form, %$user }, 0, $discussion);
 		$html = prevComment($preview, $user);
 	}
 
-	$error_message ||= 'This comment will not be saved until you click the Submit button below.';
+	if ($html) {
+		$error_message = getData('inline preview warning') . $error_message;
+		$error_message .= slashDisplay('hc_comment', { pid => $pid }, { Return => 1 });
+	}
 	$options->{content_type} = 'application/json';
 	my %to_dump = (
 		error => $error_message,
@@ -339,7 +349,7 @@
 	preProcessReplyForm($form, $reply);
 
 	my $reskey = getObject('Slash::ResKey');
-	my $rkey = $reskey->key('comments', { nostate => 1 });
+	my $rkey = $reskey->key('comments', { nostate => 1 }); #, debug => 1 });
 	$rkey->create;
 
 	my %to_dump;
@@ -386,15 +396,14 @@
 sub fetchComments {
 	my($slashdb, $constants, $user, $form, $options) = @_;
 
-	my $cids         = [ grep /^\d+$/, split /,/, ($form->{cids} || '') ];
+	my $cids         = [ grep { defined && /^\d+$/ } ($form->{_multi}{cids} ? @{$form->{_multi}{cids}} : $form->{cids}) ];
 	my $id           = $form->{discussion_id} || 0;
 	my $cid          = $form->{cid} || 0; # root id
 	my $d2_seen      = $form->{d2_seen};
-	my $placeholders = $form->{placeholders};
-	my @placeholders;
+	my $placeholders = [ grep { defined && /^\d+$/ } ($form->{_multi}{placeholders} ? @{$form->{_multi}{placeholders}} : $form->{placeholders}) ];
 
 	$user->{state}{ajax_accesslog_op} = "ajax_comments_fetch";
-#use Data::Dumper; print STDERR Dumper [ $cids, $id, $cid, $d2_seen ];
+#use Data::Dumper; print STDERR Dumper [ $form, $cids, $id, $cid, $d2_seen ];
 	# XXX error?
 	return unless $id && (@$cids || $d2_seen);
 
@@ -439,17 +448,16 @@
 	#delete $comments->{0}; # non-comment data
 
 	my %data;
-	if ($d2_seen || $placeholders) {
+	if ($d2_seen || @$placeholders) {
 		my $special_cids;
 		if ($d2_seen) {
 			$special_cids = $cids = [ sort { $a <=> $b } grep { $_ && !$seen{$_} } keys %$comments ];
-		} elsif ($placeholders) {
-			@placeholders = split /[,;]/, $placeholders;
-			$special_cids = [ sort { $a <=> $b } @placeholders ];
+		} elsif (@$placeholders) {
+			$special_cids = [ sort { $a <=> $b } @$placeholders ];
 			if ($form->{d2_seen_ex}) {
 				my @seen;
 				my $lastcid = 0;
-				my %check = (%seen, map { $_ => 1 } @placeholders);
+				my %check = (%seen, map { $_ => 1 } @$placeholders);
 				for my $cid (sort { $a <=> $b } keys(%check)) {
 					push @seen, $lastcid ? $cid - $lastcid : $cid;
 					$lastcid = $cid;
@@ -587,9 +595,9 @@
 		$to_dump{eval_first} ||= '';
 		$to_dump{eval_first} .= "d2_seen = '$d2_seen_0'; updateMoreNum($total);";
 	}
-	if ($placeholders) {
+	if (@$placeholders) {
 		$to_dump{eval_first} ||= '';
-		$to_dump{eval_first} .= "placeholder_no_update = " . Data::JavaScript::Anon->anon_dump({ map { $_ => 1 } @placeholders }) . ';';
+		$to_dump{eval_first} .= "placeholder_no_update = " . Data::JavaScript::Anon->anon_dump({ map { $_ => 1 } @$placeholders }) . ';';
 	}
 	writeLog($id);
 	return Data::JavaScript::Anon->anon_dump(\%to_dump);
@@ -746,7 +754,7 @@
 		my $moddb = getObject("Slash::$constants->{m1_pluginname}");
 		if ($moddb) {
 			# we hijack "tabbed" as our cid -- pudge
-			return $moddb->dispModCommentLog('cid', $form->{'tabbed'}, {
+			my $return = $moddb->dispModCommentLog('cid', $form->{'tabbed'}, {
 				show_m2s        => ($constants->{m2}
 					? (defined($form->{show_m2s})
 						? $form->{show_m2s}
@@ -756,6 +764,8 @@
 				need_m2_button  => $constants->{m2},
 				title           => " "
 			});
+			$return ||= getData('no modcommentlog');
+			return $return;
 		}
 
 	} else {

Modified: slashjp/trunk/plugins/Ajax/htdocs/images/admin.js
===================================================================
--- slashjp/trunk/plugins/Ajax/htdocs/images/admin.js	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Ajax/htdocs/images/admin.js	2008-04-07 00:26:30 UTC (rev 572)
@@ -200,7 +200,7 @@
 	if(use_fh_interval) {
 		interval = getFirehoseUpdateInterval(); 
 	}
-	setTimeout("console_update(" + use_fh_interval + "," + fh_is_timed_out +")", interval);
+	setTimeout("console_update(" + use_fh_interval + "," + fh_is_timed_out +")", interval * 2);
 }
 
 function firehose_usage() {

Modified: slashjp/trunk/plugins/Ajax/htdocs/images/common.js
===================================================================
--- slashjp/trunk/plugins/Ajax/htdocs/images/common.js	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Ajax/htdocs/images/common.js	2008-04-07 00:26:30 UTC (rev 572)
@@ -5,6 +5,42 @@
 	return document.getElementById(id);
 }
 
+jQuery.fn.extend({
+
+	mapClass: function( map ) {
+		map['?'] = map['?'] || [];
+		return this.each(function() {
+			var unique = {};
+			var cl = [];
+			$.each($.map(this.className.split(/\s+/), function(k){
+				return k in map ? map[k] : ('*' in map ? map['*'] : k)
+			}).concat(map['+']), function(i, k) {
+				if ( k && !(k in unique) ) {
+					unique[k] = true;
+					cl.push(k);
+				}
+			});
+			this.className = (cl.length ? cl : map['?']).join(' ');
+		});
+	},
+
+	setClass: function( c1 ) {
+		return this.each(function() {
+			this.className = c1
+		});
+	},
+
+	toggleClasses: function( c1, c2, force ) {
+		var map = { '?': force };
+		map[c1]=c2;
+		map[c2]=c1;
+		return this.mapClass(map);
+	}
+
+});
+
+var reskey_static = '';
+
 // global settings, but a firehose might use a local settings object instead
 var firehose_settings = {};
   firehose_settings.startdate = '';
@@ -18,8 +54,10 @@
   firehose_settings.is_embedded = 0;
   firehose_settings.not_id = 0;
   firehose_settings.section = 0;
+  firehose_settings.more_num = 0;
 
 // Settings to port out of settings object
+  firehose_item_count = 0;
   firehose_updates = Array(0);
   firehose_updates_size = 0;
   firehose_ordered = Array(0);
@@ -78,23 +116,11 @@
 }
 
 function createPopupButtons() {
-	var buttons = "";
-	if (arguments.length > 0) {
-		buttons = '<span class="buttons">';
-	}
-	for (var i=0; i<arguments.length; i++) {
-		buttons =  buttons + "<span>" + arguments[i] + "</span>";
-	}
-
-	buttons = buttons + "</span>";
-	return buttons;
+	return '<span class="buttons"><span>' + $.makeArray(arguments).join('</span><span>') + '</span></span>';
 }
 
 function closePopup(id, refresh) {
-	var el = $dom(id);
-	if (el) {
-		el.parentNode.removeChild(el);
-	}
+	$('#'+id).remove();
 	if (refresh) {
 		window.location.reload();
 	}
@@ -134,87 +160,44 @@
 }
 
 function getXYForId(id, addWidth, addHeight) {
-	var div = $dom(id);
-	var offset = jQuery(div).offset();
-	var xy = [ offset.left, offset.top ];
-	if (addWidth) {
-		xy[0] = xy[0] + div.offsetWidth;
-	}
-	if (addHeight) {
-		xy[1] = xy[1] + div.offsetHeight;
-	}
-	return xy;
+	var div = $('#'+id);
+	var offset = div.offset();
+	if (addWidth) offset.left += div.attr('offsetWidth');
+	if (addHeight) offset.top += div.attr('offsetHeight');
+	return [ offset.left, offset.top ];
 }
 
 function firehose_toggle_advpref() {
-	var obj = $dom('fh_advprefs');
-	if (obj.className == 'hide') {
-		obj.className = "";
-	} else {
-		obj.className = "hide";
-	}
+	$('#fh_advprefs').toggleClass('hide');
 }
 
 function firehose_open_prefs() {
-	var obj = $dom('fh_advprefs');
-	obj.className = "";
+	$('#fh_advprefs').removeClass();
 }
 
-function toggleId(id, first, second) {
-	var obj = $dom(id);
-	if (obj.className == first) {
-		obj.className = second;
-	} else if (obj.className == second) {
-		obj.className = first;
-	} else {
-		obj.className = first;
-	}
+function toggleId(id, c1, c2) {
+	$('#'+id).toggleClasses(c1, c2, c1);
 }
 
 function toggleIntro(id, toggleid) {
-	var obj = $dom(id);
-	var toggle = $dom(toggleid);
-	if (obj.className == 'introhide') {
-		obj.className = "intro"
-		toggle.innerHTML = "[-]";
-		toggle.className = "expanded";
-	} else {
-		obj.className = "introhide"
-		toggle.innerHTML = "[+]";
-		toggle.className = "condensed";
+	var new_class = 'condensed';
+	var new_html = '[+]';
+	if ( $('#'+id).toggleClasses('introhide', 'intro').hasClass('intro') ) {
+		new_class = 'expanded';
+		new_html = '[-]';
 	}
+	$('#'+toggleid).setClass(new_class).html(new_html);
 }
 
 function tagsToggleStoryDiv(id, is_admin, type) {
-	var bodyid = 'toggletags-body-' + id;
-	var tagsbody = $dom(bodyid);
-	if (tagsbody.className == 'tagshide') {
-		tagsShowBody(id, is_admin, '', type);
-	} else {
-		tagsHideBody(id);
-	}
+	($('#toggletags-body-'+id).hasClass('tagshide') ? tagsShowBody : tagsHideBody)(id, is_admin, '', type);
 }
 
 function tagsHideBody(id) {
-	// Make the body of the tagbox vanish
-	var tagsbodyid = 'toggletags-body-' + id;
-	var tagsbody = $dom(tagsbodyid);
-	tagsbody.className = "tagshide"
-
-	// Make the title of the tagbox change back to regular
-	var titleid = 'tagbox-title-' + id;
-	var title = $dom(titleid);
-	title.className = "tagtitleclosed";
-
-	// Make the tagbox change back to regular.
-	var tagboxid = 'tagbox-' + id;
-	var tagbox = $dom(tagboxid);
-	tagbox.className = "tags";
-
-	// Toggle the button back.
-	var tagsbuttonid = 'toggletags-button-' + id;
-	var tagsbutton = $dom(tagsbuttonid);
-	tagsbutton.innerHTML = "[+]";
+	$('#toggletags-body-'+id).setClass('tagshide');		// Make the body of the tagbox vanish
+	$('#tagbox-title-'+id).setClass('tagtitleclosed');	// Make the title of the tagbox change back to regular
+	$('#tagbox-'+id).setClass('tags');			// Make the tagbox change back to regular.
+	$('#toggletags-button-'+id).html('[+]');		// Toggle the button back.
 }
 
 function tagsShowBody(id, is_admin, newtagspreloadtext, type) {
@@ -229,37 +212,19 @@
 	}
 
 	//alert("Tags show body / Type: " + type );
+	$('#toggletags-button-'+id).html("[-]");		// Toggle the button to show the click was received
+	$('#tagbox-'+id).setClass("tags");			// Make the tagbox change to the slashbox class
+	$('#tagbox-title-'+id).setClass("tagtitleopen");	// Make the title of the tagbox change to white-on-green
+	$('#toggletags-body-'+id).setClass("tagbody");		// Make the body of the tagbox visible
 	
-	// Toggle the button to show the click was received
-	var tagsbuttonid = 'toggletags-button-' + id;
-	var tagsbutton = $dom(tagsbuttonid);
-	tagsbutton.innerHTML = "[-]";
-
-	// Make the tagbox change to the slashbox class
-	var tagboxid = 'tagbox-' + id;
-	var tagbox = $dom(tagboxid);
-	tagbox.className = "tags";
-
-	// Make the title of the tagbox change to white-on-green
-	var titleid = 'tagbox-title-' + id;
-	var title = $dom(titleid);
-	title.className = "tagtitleopen";
-
-	// Make the body of the tagbox visible
-	var tagsbodyid = 'toggletags-body-' + id;
-	var tagsbody = $dom(tagsbodyid);
-	
-	tagsbody.className = "tagbody";
-	
 	// If the tags-user div hasn't been filled, fill it.
-	var tagsuserid = 'tags-user-' + id;
-	var tagsuser = $dom(tagsuserid);
-	if (tagsuser.innerHTML == "") {
+	var tagsuser = $('#tags-user-' + id);
+	if (tagsuser.html() == "") {
 		// The tags-user-123 div is empty, and needs to be
 		// filled with the tags this user has already
 		// specified for this story, and a reskey to allow
 		// the user to enter more tags.
-		tagsuser.innerHTML = "Retrieving...";
+		tagsuser.html("Retrieving...");
 		var params = {};
 		if (type == "stories") {
 			params['op'] = 'tags_get_user_story';
@@ -275,12 +240,10 @@
 		params['newtagspreloadtext'] = newtagspreloadtext;
 		var handlers = {
 			onComplete: function() { 
-				var textid = 'newtags-' + id;
-				var input = $dom(textid);
-				input.focus();
+				$dom('newtags-'+id).focus();
 			}
 		}
-		ajax_update(params, tagsuserid, handlers);
+		ajax_update(params, 'tags-user-' + id, handlers);
 		//alert('after ajax_update ' + tagsuserid);
 
 		// Also fill the admin div.  Note that if the user
@@ -311,9 +274,8 @@
 			// that we append some text to the user text.
 			// We can't do that by passing it in, so do it
 			// manually now.
-			var textinputid = 'newtags-' + id;
-			var textinput = $dom(textinputid);
-			textinput.value = textinput.value + ' ' + newtagspreloadtext;
+			var textinput = $dom('newtags-'+id);
+			textinput.value += ' ' + newtagspreloadtext;
 			textinput.focus();
 		}
 	}
@@ -391,7 +353,7 @@
 	params['type'] = type;
 	if ( fh_is_admin && ("_#)^*".indexOf(tag[0]) != -1) ) {
 	  params['op'] = 'tags_admin_commands';
-	  params['reskey'] = $dom('admin_commands-reskey-' + id).value;
+	  params['reskey'] = $('#admin_commands-reskey-' + id).val();
 	  params['command'] = tag;
 	} else {
 	  params['op'] = 'tags_create_tag';
@@ -405,68 +367,53 @@
 }
 
 function tagsCreateForStory(id) {
-	var toggletags_message_id = 'toggletags-message-' + id;
-	var toggletags_message_el = $dom(toggletags_message_id);
-	toggletags_message_el.innerHTML = 'Saving tags...';
+	var status = $('#toggletags-message-'+id).html('Saving tags...');
 
-	var params = {};
-	params['op'] = 'tags_create_for_story';
-	params['sidenc'] = id;
-	var newtagsel = $dom('newtags-' + id);
-	params['tags'] = newtagsel.value;
-	var reskeyel = $dom('newtags-reskey-' + id);
-	params['reskey'] = reskeyel.value;
+	ajax_update({
+		op: 'tags_create_for_story',
+		sidenc: id,
+		tags: $('#newtags-'+id).val(),
+		reskey: $('#newtags-reskey-'+id).val()
+	}, 'tags-user-' + id);
 
-	ajax_update(params, 'tags-user-' + id);
-
 	// XXX How to determine failure here?
-	toggletags_message_el.innerHTML = 'Tags saved.';
+	status.html('Tags saved.');
 }
 
 function tagsCreateForUrl(id) {
-	var toggletags_message_id = 'toggletags-message-' + id;
-	var toggletags_message_el = $dom(toggletags_message_id);
-	toggletags_message_el.innerHTML = 'Saving tags...';
+	var status = $('#toggletags-message-'+id).html('Saving tags...');
 
-	var params = {};
-	params['op'] = 'tags_create_for_url';
-	params['id'] = id;
-	var newtagsel = $dom('newtags-' + id);
-	params['tags'] = newtagsel.value;
-	var reskeyel = $dom('newtags-reskey-' + id);
-	params['reskey'] = reskeyel.value;
+	ajax_update({
+		op:	'tags_create_for_url',
+		id:	id,
+		tags:	$('#newtags-'+id).val(),
+		reskey:	$('#newtags-reskey-'+id).val()
+	}, 'tags-user-' + id);
 
-	ajax_update(params, 'tags-user-' + id);
-
 	// XXX How to determine failure here?
-	toggletags_message_el.innerHTML = 'Tags saved.';
+	status.html('Tags saved.');
 }
 
 //Firehose functions begin
 function setOneTopTagForFirehose(id, newtag) {
-	var params = {};
-	params['op'] = 'firehose_update_one_tag';
-	params['id'] = id;
-	params['tags'] = newtag;
-	// params['reskey'] = reskeyel.value;
-	ajax_update(params, '');
+	ajax_update({
+		op: 'firehose_update_one_tag',
+		id: id,
+		tags: newtag
+	});
 }
 
 function tagsCreateForFirehose(id) {
-	var toggletags_message_id = 'toggletags-message-' + id;
-	var toggletags_message_el = $dom(toggletags_message_id);
-	toggletags_message_el.innerHTML = 'Saving tags...';
+	var status = $('#toggletags-message-'+id).html('Saving tags...');
 	
-	var params = {};
-	params['op'] = 'tags_create_for_firehose';
-	params['id'] = id;
-	var newtagsel = $dom('newtags-' + id);
-	params['tags'] = newtagsel.value; 
-	var reskeyel = $dom('newtags-reskey-' + id);
-	params['reskey'] = reskeyel.value;
+	ajax_update({
+		op:	'tags_create_for_firehose',
+		id:	id,
+		tags:	$('#newtags-'+id).val(),
+		reskey:	$('#newtags-reskey-'+id).val()
+	}, 'tags-user-'+id);
 
-	ajax_update(params, 'tags-user-' + id);
-	toggletags_message_el.innerHTML = 'Tags saved.';
+	status.html('Tags saved.');
 }
 
 function toggle_firehose_body(id, is_admin) {
@@ -487,11 +434,7 @@
 			}
 		};
 		params['reskey'] = reskey_static;
-		if (is_admin) {
-			ajax_update(params, 'fhbody-'+id, handlers);
-		} else {
-			ajax_update(params, 'fhbody-'+id);
-		}
+		ajax_update(params, 'fhbody-'+id, is_admin ? handlers : null);
 		fhbody.className = "body";
 		fh.className = "article" + usertype;
 		if (is_admin)
@@ -510,12 +453,7 @@
 }
 
 function toggleFirehoseTagbox(id) {
-	var fhtb = $dom('fhtagbox-'+id);
-	if (fhtb.className == "hide") {
-		fhtb.className = "tagbox";
-	} else {
-		fhtb.className = "hide";
-	}
+	$('#fhtagbox-'+id).toggleClasses('tagbox', 'hide');
 }
 
 function firehose_set_options(name, value) {
@@ -555,14 +493,7 @@
 		}
 
 		if (classname) {
-			var els = document.getElementsByClassName(classname, $dom('firehoselist'));
-			var classval = classname;
-			if (value) {
-				classval = classval + " hide";
-			}
-			for (i = 0; i< els.length; i++) {
-				els[i].className = classval;
-			}
+			$('#firehoselist .'+classname).setClass(classname + value ? ' hide' : '');
 		}
 	}
 
@@ -573,6 +504,7 @@
 			}
 		}
 		firehose_settings.page = 0;
+		firehose_settings.more_num = 0;
 	}
 	if (name != "color") {
 	for (i=0; i< pairs.length; i++) {
@@ -628,20 +560,19 @@
 			firehose_settings.startdate = value;
 			firehose_settings.duration = 1;
 			firehose_settings.page = 0;
+			firehose_settings.more_num = 0;
 			var issuedate = firehose_settings.issue.substr(5,2) + "/" + firehose_settings.issue.substr(8,2) + "/" + firehose_settings.issue.substr(10,2);
 
-			if ($dom('fhcalendar')) {
-				$dom('fhcalendar')._widget.setDate(issuedate, "day");
-			}
-			if ($dom('fhcalendar_pag')) {
-				$dom('fhcalendar_pag')._widget.setDate(issuedate, "day");
-			}
+			$('#fhcalendar, #fhcalendar_pag').each(function(){
+				this._widget.setDate(issuedate, "day");
+			});
 		}
 		if (name == "color") {
 			firehose_settings.color = value;
 		}
 		if (name == "pagesize") {
 			firehose_settings.page = 0;
+			firehose_settings.more_num = 0;
 		}
 	}
 
@@ -669,14 +600,7 @@
 }
 
 function firehose_remove_all_items() {
-	var fhl = $dom('firehoselist');
-	var children = fhl.childNodes;
-	for (var i = children.length -1 ; i >= 0; i--) {
-		var el = children[i];
-		if (el.id) {
-			el.parentNode.removeChild(el);
-		}
-	}
+	$('#firehoselist').children().remove();
 }
 
 
@@ -684,24 +608,15 @@
 	if (!check_logged_in()) return;
 
 	setFirehoseAction();
-	var params = {};
-	var handlers = {
-		onComplete: json_handler
-	};
-	params['op'] = 'firehose_up_down';
-	params['id'] = id;
-	params['reskey'] = reskey_static;
-	params['dir'] = dir;
-	var updown = $dom('updown-' + id);
-	ajax_update(params, '', handlers);
-	if (updown) {
-		if (dir == "+") {
-			updown.className = "votedup";	
-		} else if (dir == "-") {
-			updown.className = "voteddown";	
-		}
-	}
+	ajax_update({
+		op:	'firehose_up_down',
+		id:	id,
+		reskey:	reskey_static,
+		dir:	dir
+	}, '', { onComplete: json_handler });
 
+	$('#updown-'+id).setClass(dir=='+' ? 'votedup' : 'voteddown');
+
 	if (dir == "-" && fh_is_admin) {
 		firehose_collapse_entry(id);
 	}
@@ -709,15 +624,12 @@
 
 function firehose_remove_tab(tabid) {
 	setFirehoseAction();
-	var params = {};
-	var handlers = {
-		onComplete:  json_handler
-	};
-	params['op'] = 'firehose_remove_tab';
-	params['tabid'] = tabid;
-	params['reskey'] = reskey_static;
-	params['section'] = firehose_settings.section;
-	ajax_update(params, '', handlers);
+	ajax_update({
+		op:		'firehose_remove_tab',
+		tabid:		tabid,
+		reskey:		reskey_static,
+		section:	firehose_settings.section
+	}, '', { onComplete: json_handler });
 
 }
 
@@ -733,12 +645,12 @@
 		url: request_url || '/ajax.pl',
 		data: request_params,
 		type: 'POST',
-		contentType: 'application/x-www-form-urlencoded',
+		contentType: 'application/x-www-form-urlencoded'
 	};
 
 	if ( id ) {
 		opts['success'] = function(html){
-			jQuery('#'+id).html(html);
+			$('#'+id).html(html);
 		}
 	}
 
@@ -782,37 +694,34 @@
 
 	if (response.html) {
 		for (el in response.html) {
-			if ($dom(el))
-				$dom(el).innerHTML = response.html[el];
+			$('#'+el).html(response.html[el]);
 		}
 		
 	} 
 
 	if (response.value) {
 		for (el in response.value) {
-			if ($dom(el))
-				$dom(el).value = response.value[el];
+			$('#'+el).val(response.value[el]);
 		}
 	}
 
 	if (response.html_append) {
 		for (el in response.html_append) {
-			if ($dom(el))
-				$dom(el).innerHTML = $dom(el).innerHTML + response.html_append[el];
+			$('#'+el).each(function(){
+				this.innerHTML += response.html_append[el];
+			});
 		}
 	}
 
 	if (response.html_append_substr) {
 		for (el in response.html_append_substr) {
-			if ($dom(el)) {
-				var this_html = $dom(el).innerHTML;
-				var i = $dom(el).innerHTML.search(/<span class="?substr"?> ?<\/span>[\s\S]*$/i);
-				if (i == -1) {
-					$dom(el).innerHTML += response.html_append_substr[el];
-				} else {
-					$dom(el).innerHTML = $dom(el).innerHTML.substr(0, i) +
-						response.html_append_substr[el];
-				}
+			var found = $('#'+el);
+			if (found.size()) {
+				var this_html = found.html();
+				var pos = this_html.search(/<span class="?substr"?> ?<\/span>[\s\S]*$/i);
+				if ( pos != -1 )
+					this_html = this_html.substr(0, pos);
+				found.html(this_html + response.html_append_substr[el]);
 			}
 		}
 	}		
@@ -833,14 +742,14 @@
 		var fh = 'firehose-' + el[1];
 		var wait_interval = 800;
 		if(el[0] == "add") {
-			if (firehose_before[el[1]] && jQuery('#firehose-' + firehose_before[el[1]]).size()) {
-				jQuery('#firehose-' + firehose_before[el[1]]).after(el[2]);
-			} else if (firehose_after[el[1]] && jQuery('#firehose-' + firehose_after[el[1]]).size()) {
-				jQuery('#firehose-' + firehose_after[el[1]]).before(el[2]);
+			if (firehose_before[el[1]] && $('#firehose-' + firehose_before[el[1]]).size()) {
+				$('#firehose-' + firehose_before[el[1]]).after(el[2]);
+			} else if (firehose_after[el[1]] && $('#firehose-' + firehose_after[el[1]]).size()) {
+				$('#firehose-' + firehose_after[el[1]]).before(el[2]);
 			} else if (insert_new_at == "bottom") {
-				jQuery('#firehoselist').append(el[2]);
+				$('#firehoselist').append(el[2]);
 			} else {
-				jQuery('#firehoselist').prepend(el[2]);
+				$('#firehoselist').prepend(el[2]);
 			}
 		
 			var toheight = 50;
@@ -937,28 +846,21 @@
 
 function firehose_reorder() {
 	if (firehose_ordered) {
-		var fhlist = $dom('firehoselist');
+		var fhlist = $('#firehoselist');
 		if (fhlist) {
-			var item_count = 0;
-			for (i = 0; i < firehose_ordered.length; i++) {
-				if (/^\d+$/.test(firehose_ordered[i])) {
-					item_count++;
+			firehose_item_count = firehose_ordered.length;
+			for (i = 0; i < firehose_ordered.length; ++i) {
+				if (!/^\d+$/.test(firehose_ordered[i])) {
+					--firehose_item_count;
 				}
-				var fhel = $dom('firehose-' + firehose_ordered[i]);
-				if (fhlist && fhel) {
-					fhlist.appendChild(fhel);
-				}
+				$('#firehose-'+firehose_ordered[i]).appendTo(fhlist);
 				if ( firehose_future[firehose_ordered[i]] ) {
-					if ($dom("ttype-" + firehose_ordered[i])) {
-						$dom("ttype-" + firehose_ordered[i]).className = "future";
-					}
+					$('#ttype-'+firehose_ordered[i]).setClass('future');
 				} else {
-					if ($dom("ttype-" + firehose_ordered[i]) && $dom("ttype-" + firehose_ordered[i]).className == "future") {
-						$dom("ttype-" + firehose_ordered[i]).className = "story";
-					}
+					$('#ttype-'+firehose_ordered[i]+'.future').setClass('story');
 				}
 			}
-			document.title = "[% sitename %] - " + (console_updating ? "Console" : "Firehose") + " (" + item_count + ")";
+			document.title = "[% sitename %] - " + (console_updating ? "Console" : "Firehose") + " (" + firehose_item_count + ")";
 		}
 	}
 
@@ -973,9 +875,7 @@
 
 
 function firehose_get_updates_handler(transport) {
-	if ($dom('busy')) {
-		$dom('busy').className = "hide";
-	}
+	$('#busy').setClass('hide');
 	var response = eval_response(transport);
 	var processed = 0;
 	firehose_removals = response.update_data.removals;
@@ -1005,24 +905,9 @@
 }
 
 function firehose_get_item_idstring() {
-	var fhl = $dom('firehoselist');
-	var str = "";
-	var children;
-	if (fhl) {
-		var id;
-		children = fhl.childNodes;
-		if (children) {
-			for (var i = 0; i < children.length; i++) {
-				if (children[i].id) {
-					id = children[i].id;
-					id = id.replace(/^firehose-/g, "");
-					id = id.replace(/^\s+|\s+$/g, "");
-					str = str + id + ",";
-				}
-			}
-		}
-	}
-	return str;
+	return $('#firehoselist > [id]').map(function(){
+		return this.id.replace(/firehose-(\S+)/, '$1');
+	}).get().join(',');
 }
 
 
@@ -1039,26 +924,20 @@
 		while(id = fh_update_timerids.pop()) { clearTimeout(id) };
 	}
 	fh_is_updating = 1
-	var params = {};
-	var handlers = {
-		onComplete: firehose_get_updates_handler
+	var params = {
+		op:		'firehose_get_updates',
+		ids:		firehose_get_item_idstring(),
+		updatetime:	update_time,
+		fh_pageval:	firehose_settings.pageval,
+		embed:		firehose_settings.is_embedded
 	};
-	params['op'] = 'firehose_get_updates';
-	params['ids'] = firehose_get_item_idstring();
-	params['updatetime'] = update_time;
 
 	for (i in firehose_settings) {
 		params[i] = firehose_settings[i];
 	}
 
-	if ( firehose_settings.is_embedded ) {
-		params['embed'] = 1;
-	}
-	params['fh_pageval'] = firehose_settings.pageval;
-	if ($dom('busy')) {
-		$dom('busy').className = "";
-	}
-	ajax_update(params, '', handlers);
+	$('#busy').removeClass();
+	ajax_update(params, '', { onComplete: firehose_get_updates_handler });
 }
 
 
@@ -1100,8 +979,7 @@
 	var secs = getSecsSinceLastFirehoseAction();
 	if (secs > inactivity_timeout) {
 		fh_is_timed_out = 1;
-		if ($dom('message_area'))
-			$dom('message_area').innerHTML = "サーバの反応が良くないので自動更新は遅れます";
+		$('#message_area').html("Automatic updates have been slowed due to inactivity")
 		//firehose_pause();
 	}
 }
@@ -1110,36 +988,21 @@
 	fh_play = 1;
 	setFirehoseAction();
 	firehose_set_options('pause', '0');
-	var pausepanel = $dom('pauseorplay');
-	if ($dom('message_area'))
-		$dom('message_area').innerHTML = "";
-	if (pausepanel) {
-		pausepanel.innerHTML = "更新";
-	}
-	var pause = $dom('pause');
-	
-	var play_div = $dom('play');
-	if (play_div) {
-		play_div.className = "hide";
-	}
-	if (pause) {
-		pause.className = "show";
-	}
+	$('#message_area').html('');
+	$('#pauseorplay').html('Updated');
+	$('#play').setClass('hide');
+	$('#pause').setClass('show');
 }
 
 function is_firehose_playing() {
-  return YAHOO.util.Dom.hasClass('play', 'hide');
+  return fh_play==1;
 }
 
 function firehose_pause() {
 	fh_play = 0;
-	var pause = $dom('pause');
-	var play_div = $dom('play');
-	pause.className = "hide";
-	play_div.className = "show";
-	if ($dom('pauseorplay')) {
-		$dom('pauseorplay').innerHTML = "停止";
-	}
+	$('#pause').setClass('hide');
+	$('#play').setClass('show');
+	$('#pauseorplay').html('Paused');
 	firehose_set_options('pause', '1');
 }
 
@@ -1148,14 +1011,8 @@
 }
 
 function firehose_collapse_entry(id) {
-	var fhbody = $dom('fhbody-'+id);
-	var fh = $dom('firehose-'+id);
-	if (fhbody && fhbody.className == "body") {
-		fhbody.className = "hide";
-	}
-	if (fh) {	
-		fh.className = "briefarticle";
-	}
+	$('#fhbody-'+id+'.body').setClass('hide');
+	$('#firehose-'+id).setClass('briefarticle');
 	tagsHideBody(id)
 
 }
@@ -1275,38 +1132,25 @@
 }
 
 function logToDiv(id, message) {
-	var div = $dom(id);
-	if (div) {
-	div.innerHTML = div.innerHTML + message + "<br>";
-	}
+	$('#'+id).append(message + '<br>');
 }
 
 
 function firehose_open_tab(id) {
-	var tf = $dom('tab-form-'+id);
-	var tt = $dom('tab-text-'+id);
-	var ti = $dom('tab-input-'+id);
-	tf.className="";
-	ti.focus();
-	tt.className="hide";
+	$('#tab-form-'+id).removeClass();
+	$dom('tab-input-'+id).focus();
+	$('#tab-text-'+id).setClass('hide');
 }
 
 function firehose_save_tab(id) {
-	var tf = $dom('tab-form-'+id);
-	var tt = $dom('tab-text-'+id);
-	var ti = $dom('tab-input-'+id);
-	var params = {};
-	var handlers = {
-		onComplete: json_handler 
-	};
-	params['op'] = 'firehose_save_tab';
-	params['tabname'] = ti.value;
-	params['section'] = firehose_settings.section;
-
-	params['tabid'] = id;
-	ajax_update(params, '',  handlers);
-	tf.className = "hide";
-	tt.className = "";
+	ajax_update({
+		op:		'firehose_save_tab',
+		tabname:	$('#tab-input-'+id).val(),
+		section:	firehose_settings.section,
+		tabid:		id
+	}, '',  { onComplete: json_handler });
+	$('#tab-form-'+id).setClass('hide');
+	$('#tab-text-'+id).removeClass();
 }
 
 
@@ -1424,6 +1268,8 @@
 }
 
 function getModalPrefs(section, title, tabbed) {
+	if (!reskey_static)
+		return show_login_box();
 	document.getElementById('preference_title').innerHTML = title;
 	var params = {};
 	params['op'] = 'getModalPrefs';
@@ -1464,28 +1310,17 @@
 }
 
 function ajaxSaveSlashboxes() {
-	var wrapper = document.getElementById('slashboxes');
-	var titles = YAHOO.util.Dom.getElementsByClassName('title', 'div', wrapper);
-	var sep = "";
-	var all = "";
-	for ( i=0; i<titles.length; ++i) {
-		var bid = titles[i].id.slice(0,-6);
-		all += sep + bid;
-		sep = ",";
-	}
-
-	var params = {};
-	params['op'] = 'page_save_user_boxes';
-	params['reskey'] = reskey_static;
-	params['bids'] = all;
-	ajax_update(params, '');
+	ajax_update({
+		op:	'page_save_user_boxes',
+		reskey:	reskey_static,
+		bids:	$('#slashboxes div.title').map(function(){
+				return this.id.slice(0,-6);
+			}).get().join(',')
+	});
 }
 
 function ajaxRemoveSlashbox( id ) {
-	var slashboxes = document.getElementById('slashboxes');
-	var box = document.getElementById(id);
-	if ( box.parentNode === slashboxes ) {
-		slashboxes.removeChild(box);
+	if ( $('#slashboxes > #'+id).remove().size() ) {
 		ajaxSaveSlashboxes();
 	}
 }
@@ -1517,7 +1352,7 @@
 }
 
 function admin_signoff(stoid, type, id) {
-	var params = [];
+	var params = {};
 	params['op'] = 'admin_signoff';
 	params['stoid'] = stoid;
 	params['reskey'] = reskey_static;
@@ -1530,7 +1365,6 @@
 
 function scrollWindowToFirehose(fhid) {
 	var firehose_y = getOffsetTop($('firehose-' + fhid));
-	console.log(firehose_y);
 	scroll(viewWindowLeft(), firehose_y);
 }
 
@@ -1621,6 +1455,7 @@
 	var pos = firehose_get_pos_of_id(cur);
 	if (pos < (firehose_ordered.length - 1)) {
 		pos++;
+	} else {
 	}
 	firehose_set_cur(firehose_ordered[pos]);
 	scrollWindowToFirehose(firehose_cur);
@@ -1637,4 +1472,14 @@
 
 }
 
+function firehose_more() {
+	var increment_by = 10;
+	firehose_settings.more_num = firehose_settings.more_num + increment_by;
+	
+	if (((firehose_item_count + increment_by) >= 200) && !fh_is_admin) {
+		$('#firehose_more').hide();
+	}
+	firehose_set_options('more_num', firehose_settings.more_num);
+}
 
+

Modified: slashjp/trunk/plugins/Ajax/htdocs/images/nodnix.js
===================================================================
--- slashjp/trunk/plugins/Ajax/htdocs/images/nodnix.js	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Ajax/htdocs/images/nodnix.js	2008-04-07 00:26:30 UTC (rev 572)
@@ -17,16 +17,17 @@
 }
 
 function get_predefined_nodnix_tags() {
-  var tags = [];
-  var query = _get_nodnix('input').getAttribute("updown");
-  var listEl = query=="+" ? document.getElementById('static-nod-completions')
-                          : document.getElementById('static-nix-completions');
-  if ( listEl ) {
-    var itemEls = listEl.getElementsByTagName('li');
-    for ( var i=0; i<itemEls.length; ++i )
-      tags.push([itemEls[i].textContent]);
-  }
-  return tags;
+	var tags = [];
+	var query = _get_nodnix('input').getAttribute("updown");
+	var listEl = query=="+" ? document.getElementById('static-nod-completions')
+		: document.getElementById('static-nix-completions');
+
+	if ( listEl ) {
+		var itemEls = listEl.getElementsByTagName('li');
+		for ( var i=0; i<itemEls.length; ++i )
+			tags.push([itemEls[i].textContent]);
+	}
+	return tags;
 }
 var predefinedDS = new YAHOO.widget.DS_JSFunction(get_predefined_nodnix_tags);
 
@@ -34,54 +35,55 @@
 var proxyDS = new Object();
 proxyDS.__proto__ = YAHOO.slashdot.dataSources[0];
 proxyDS.doQuery = function( oCallbackFn, sQuery, oParent ) {
-  if ( sQuery && sQuery.length )
-    this.__proto__.doQuery(oCallbackFn, sQuery, oParent)
-  else
-    predefinedDS.doQuery(oCallbackFn, sQuery, oParent);
+	if ( sQuery && sQuery.length )
+		this.__proto__.doQuery(oCallbackFn, sQuery, oParent)
+	else
+		predefinedDS.doQuery(oCallbackFn, sQuery, oParent);
 }
 
 function get_nodnix_listener() {
-  if ( !get_nodnix_listener.nodnix_listener ) {
-    var keylist = new Array(); // must be an actual Array(), not just [], for YUI to do-the-right-thing
-    keylist.push(YAHOO.util.KeyListener.KEY.ESCAPE);
+	if ( !get_nodnix_listener.nodnix_listener ) {
+		var keylist = new Array(); // must be an actual Array(), not just [], for YUI to do-the-right-thing
+		keylist.push(YAHOO.util.KeyListener.KEY.ESCAPE);
 
-    var a='A'.charCodeAt(0), z='Z'.charCodeAt(0);
-    for ( var kc = a; kc <= z; ++kc )
-      keylist.push(kc);
-    var extras = "!_#)^*";
-    for ( var i=0; i<extras.length; ++i )
-      keylist.push(extras.charCodeAt(i));
+		var a='A'.charCodeAt(0), z='Z'.charCodeAt(0);
+		for ( var kc = a; kc <= z; ++kc )
+			keylist.push(kc);
+		var extras = "!_#)^*";
+		for ( var i=0; i<extras.length; ++i )
+			keylist.push(extras.charCodeAt(i));
 
-    get_nodnix_listener.nodnix_listener = new YAHOO.util.KeyListener(document, {keys:keylist},
-                                                           {fn:handle_nodnix_key});
+		get_nodnix_listener.nodnix_listener = new YAHOO.util.KeyListener(
+			document, {keys:keylist}, {fn:handle_nodnix_key}
+		);
 
 
-    var keylist2 = new Array();
-    keylist2.push(YAHOO.util.KeyListener.KEY.SPACE);
-    keylist2.push(YAHOO.util.KeyListener.KEY.ESCAPE);
-    keylist2.push(YAHOO.util.KeyListener.KEY.ENTER);
+		var keylist2 = new Array();
+		keylist2.push(YAHOO.util.KeyListener.KEY.SPACE);
+		keylist2.push(YAHOO.util.KeyListener.KEY.ESCAPE);
+		keylist2.push(YAHOO.util.KeyListener.KEY.ENTER);
 
-    var setupCompleter = function(inputEl, containerEl) {
-      var ac = new YAHOO.widget.AutoComplete(inputEl, containerEl, proxyDS);
-      //ac.typeAhead = true;
-      ac.allowBrowserAutocomplete = false;
-      ac.highlightClassName = "selected";
-      ac.minQueryLength = 0;
+		var setupCompleter = function(inputEl, containerEl) {
+			var ac = new YAHOO.widget.AutoComplete(inputEl, containerEl, proxyDS);
+			//ac.typeAhead = true;
+			ac.allowBrowserAutocomplete = false;
+			ac.highlightClassName = "selected";
+			ac.minQueryLength = 0;
 
-      ac.textboxBlurEvent.subscribe(handle_nodnix_blur);
-      ac.itemSelectEvent.subscribe(handle_nodnix_select);
-      ac.unmatchedItemSelectEvent.subscribe(handle_nodnix_select);
+			ac.textboxBlurEvent.subscribe(handle_nodnix_blur);
+			ac.itemSelectEvent.subscribe(handle_nodnix_select);
+			ac.unmatchedItemSelectEvent.subscribe(handle_nodnix_select);
 
-      var listener = new YAHOO.util.KeyListener(inputEl, {keys:keylist2}, {fn:handle_completer_key});
-      listener.enable();
+			var listener = new YAHOO.util.KeyListener(inputEl, {keys:keylist2}, {fn:handle_completer_key});
+			listener.enable();
 
-      return ac;
-    }
+			return ac;
+		}
 
-    nod_completer = setupCompleter("nod-input", "nod-completions");
-    nix_completer = setupCompleter("nix-input", "nix-completions");
-  }
-  return get_nodnix_listener.nodnix_listener;
+		nod_completer = setupCompleter("nod-input", "nod-completions");
+		nix_completer = setupCompleter("nix-input", "nix-completions");
+	}
+	return get_nodnix_listener.nodnix_listener;
 }
 
 
@@ -101,10 +103,10 @@
 }
 
 function nodnix_not_tag( old_tag ) {
-  var new_tag = old_tag[0]=='!' ? old_tag.slice(1) : '!'+old_tag;
+	var new_tag = old_tag[0]=='!' ? old_tag.slice(1) : '!'+old_tag;
 	createTag(new_tag, g_nodnix_item_id, "firehose");
 	var tag_list = _get_nodnix('ol');
-	  // XXX not a good idea if the tag happens to be 'span' or 'li', etc
+	// XXX not a good idea if the tag happens to be 'span' or 'li', etc
 	tag_list.innerHTML = tag_list.innerHTML.replace(old_tag, new_tag, "g");
 }
 
@@ -178,122 +180,122 @@
 }
 
 function _get_nodnix( tag ) {
-  var menu;
-     ((menu=get_nod_menu()).style.display != 'none')
-  || ((menu=get_nix_menu()).style.display != 'none')
-  ||  (menu=null);
+	var menu;
+	   ((menu=get_nod_menu()).style.display != 'none')
+	|| ((menu=get_nix_menu()).style.display != 'none')
+	||  (menu=null);
 
-  if ( ! YAHOO.util.Dom.hasClass(menu, 'editing') )
-    return;
+	if ( ! YAHOO.util.Dom.hasClass(menu, 'editing') )
+		return;
 
-  return menu.getElementsByTagName(tag)[0];
+	return menu.getElementsByTagName(tag)[0];
 }
 
 function handle_nodnix_key( type, args, obj ) {
-  if ( args ) {
-    var event = args[1];
-    if ( event ) {
-        // space key initiates editing, but _doesn't_ go into the text field (swallow it)
-        // escape key hides the menu before we even start editing (and we swallow it)
-        // any other key initiates editing and goes into the text field (don't swallow it)
-      var isSPACE = event.keyCode == YAHOO.util.KeyListener.KEY.SPACE;
-      var isESCAPE = event.keyCode == YAHOO.util.KeyListener.KEY.ESCAPE;
+	if ( args ) {
+		var event = args[1];
+		if ( event ) {
+			// space key initiates editing, but _doesn't_ go into the text field (swallow it)
+			// escape key hides the menu before we even start editing (and we swallow it)
+			// any other key initiates editing and goes into the text field (don't swallow it)
+			var isSPACE = event.keyCode == YAHOO.util.KeyListener.KEY.SPACE;
+			var isESCAPE = event.keyCode == YAHOO.util.KeyListener.KEY.ESCAPE;
 
-      if ( isSPACE || isESCAPE )
-        YAHOO.util.Event.stopEvent(event);
+			if ( isSPACE || isESCAPE )
+				YAHOO.util.Event.stopEvent(event);
 
-      if ( isESCAPE )
-        hide_nodnix_menu();
-      else
-        begin_nodnix_editing();
-    }
-  }
+			if ( isESCAPE )
+				hide_nodnix_menu();
+			else
+				begin_nodnix_editing();
+		}
+	}
 }
 
 function soon_is_now() {
-  YAHOO.util.Dom.removeClass(get_nod_menu(), 'soon');
-  YAHOO.util.Dom.removeClass(get_nix_menu(), 'soon');
+	YAHOO.util.Dom.removeClass(get_nod_menu(), 'soon');
+	YAHOO.util.Dom.removeClass(get_nix_menu(), 'soon');
 }
 
 function refresh_tag_bar( tag_list ) {
-  // ajax request to fill the user tags list
-  var params = {};
-  params['op'] = 'tags_get_user_firehose';
-  params['id'] = g_nodnix_item_id;
-  params['nodnix'] = 1;
-  ajax_update(params, tag_list, {});
+	// ajax request to fill the user tags list
+	var params = {};
+	params['op'] = 'tags_get_user_firehose';
+	params['id'] = g_nodnix_item_id;
+	params['nodnix'] = 1;
+	ajax_update(params, tag_list, {});
 }
 
 function begin_nodnix_editing() {
-  if ( is_firehose_playing() ) {
-    firehose_pause();
-    end_nodnix_editing.restore_firehose_state = firehose_play;
-  }
+	if ( is_firehose_playing() ) {
+		firehose_pause();
+		end_nodnix_editing.restore_firehose_state = firehose_play;
+	}
 
-  get_nodnix_listener().disable();
-  YAHOO.util.Dom.addClass(get_nod_menu(), 'soon');
-  YAHOO.util.Dom.addClass(get_nix_menu(), 'soon');
-  YAHOO.util.Dom.addClass(get_nod_menu(), 'editing');
-  YAHOO.util.Dom.addClass(get_nix_menu(), 'editing');
-  dont_hide_nodnix_menu();
+	get_nodnix_listener().disable();
+	YAHOO.util.Dom.addClass(get_nod_menu(), 'soon');
+	YAHOO.util.Dom.addClass(get_nix_menu(), 'soon');
+	YAHOO.util.Dom.addClass(get_nod_menu(), 'editing');
+	YAHOO.util.Dom.addClass(get_nix_menu(), 'editing');
+	dont_hide_nodnix_menu();
 
-  var input = _get_nodnix('input');
-  input.value = "";
-  input.focus();
+	var input = _get_nodnix('input');
+	input.value = "";
+	input.focus();
 
-  var tag_list = _get_nodnix('ol');
-  tag_list.innerHTML = "";
-  refresh_tag_bar(tag_list);
+	var tag_list = _get_nodnix('ol');
+	tag_list.innerHTML = "";
+	refresh_tag_bar(tag_list);
 
-  (input.getAttribute("updown")=="+" ? nod_completer : nix_completer).sendQuery();
-  setTimeout(soon_is_now, 225);
+	(input.getAttribute("updown")=="+" ? nod_completer : nix_completer).sendQuery();
+	setTimeout(soon_is_now, 225);
 }
 
 function end_nodnix_editing() {
-  YAHOO.util.Dom.removeClass(get_nod_menu(), 'editing');
-  YAHOO.util.Dom.removeClass(get_nix_menu(), 'editing');
-  end_nodnix_editing.restore_firehose_state();
-  end_nodnix_editing.restore_firehose_state = function(){}
+	YAHOO.util.Dom.removeClass(get_nod_menu(), 'editing');
+	YAHOO.util.Dom.removeClass(get_nix_menu(), 'editing');
+	end_nodnix_editing.restore_firehose_state();
+	end_nodnix_editing.restore_firehose_state = function(){}
 }
 end_nodnix_editing.restore_firehose_state = function(){}
 
 function handle_nodnix_blur( type, args ) {
-  hide_nodnix_menu();
+	hide_nodnix_menu();
 }
 
 function handle_nodnix_select( type, args, stay_open ) {
-  var tagname = args[2];
-  if ( tagname !== undefined && tagname !== null ) {
-    if ( typeof tagname != 'string' )
-      tagname = tagname[0];
-    nodnix_tag(tagname);
-      // now 'harden' the tag
-    var list = _get_nodnix('ol');
-    list.innerHTML = handle_nodnix_select.template_string.split('$').join(tagname) + list.innerHTML;
-    _get_nodnix('input').value = "";
-  }
-  if ( !stay_open )
-    hide_nodnix_menu();
+	var tagname = args[2];
+	if ( tagname !== undefined && tagname !== null ) {
+		if ( typeof tagname != 'string' )
+			tagname = tagname[0];
+		nodnix_tag(tagname);
+			// now 'harden' the tag
+		var list = _get_nodnix('ol');
+		list.innerHTML = handle_nodnix_select.template_string.split('$').join(tagname) + list.innerHTML;
+		_get_nodnix('input').value = "";
+	}
+	if ( !stay_open )
+		hide_nodnix_menu();
 }
 
-  // WARNING: keep this string in sync with tagsnodnixuser;misc;default
+// WARNING: keep this string in sync with tagsnodnixuser;misc;default
 handle_nodnix_select.template_string = '<li>$<span class="tag-actions"><a class="not-tag" onmousedown="nodnix_not_tag(\'$\'); return false" href="#">!</a> <a class="del-tag" onmousedown="nodnix_del_tag(\'$\'); return false" href="#">x</a></span></li>';
 
 function handle_completer_key( type, args ) {
-  var key = args[0];
-  var event = args[1];
-  var stay_open = false;
-  switch ( key ) {
-    case YAHOO.util.KeyListener.KEY.ESCAPE:
-      hide_nodnix_menu();
-      break;
-    case YAHOO.util.KeyListener.KEY.SPACE:
-      YAHOO.util.Event.stopEvent(event);
-      stay_open = true;
-      // fall through
-    case YAHOO.util.KeyListener.KEY.ENTER:
-      handle_nodnix_select("", [null, null, _get_nodnix('input').value], stay_open);
-      break;
-  }
+	var key = args[0];
+	var event = args[1];
+	var stay_open = false;
+	switch ( key ) {
+		case YAHOO.util.KeyListener.KEY.ESCAPE:
+			hide_nodnix_menu();
+			break;
+		case YAHOO.util.KeyListener.KEY.SPACE:
+			YAHOO.util.Event.stopEvent(event);
+			stay_open = true;
+			// fall through
+		case YAHOO.util.KeyListener.KEY.ENTER:
+			handle_nodnix_select("", [null, null, _get_nodnix('input').value], stay_open);
+			break;
+	}
 }
 

Modified: slashjp/trunk/plugins/Ajax/htdocs/images/sd_autocomplete.js
===================================================================
--- slashjp/trunk/plugins/Ajax/htdocs/images/sd_autocomplete.js	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Ajax/htdocs/images/sd_autocomplete.js	2008-04-07 00:26:30 UTC (rev 572)
@@ -3,67 +3,66 @@
 
 YAHOO.namespace("slashdot");
 
-YAHOO.slashdot.DS_JSArray = function(aData, oConfigs)
-  {
-    if ( typeof oConfigs == "object" )
-      for ( var sConfig in oConfigs )
-        this[sConfig] = oConfigs[sConfig];
+YAHOO.slashdot.DS_JSArray = function(aData, oConfigs) {
+	if ( typeof oConfigs == "object" )
+		for ( var sConfig in oConfigs )
+			this[sConfig] = oConfigs[sConfig];
 
-    if ( !aData || (aData.constructor != Array) )
-      return
+	if ( !aData || (aData.constructor != Array) )
+		return
 
-    this.data = aData;
-    this._init();
-  }
+	this.data = aData;
+	this._init();
+}
 
 YAHOO.slashdot.DS_JSArray.prototype = new YAHOO.widget.DataSource();
 
 YAHOO.slashdot.DS_JSArray.prototype.data = null;
 
 YAHOO.slashdot.DS_JSArray.prototype.doQuery = function(oCallbackFn, sQuery, oParent) {
-    var aData = this.data; // the array
-    var aResults = []; // container for results
-    var bMatchFound = false;
-    var bMatchContains = this.queryMatchContains;
+	var aData = this.data; // the array
+	var aResults = []; // container for results
+	var bMatchFound = false;
+	var bMatchContains = this.queryMatchContains;
 
-    if(sQuery && !this.queryMatchCase) {
-        sQuery = sQuery.toLowerCase();
-    }
+	if(sQuery && !this.queryMatchCase) {
+		sQuery = sQuery.toLowerCase();
+	}
 
-    // Loop through each element of the array...
-    // which can be a string or an array of strings
-    for(var i = aData.length-1; i >= 0; i--) {
-        var aDataset = [];
+	// Loop through each element of the array...
+	// which can be a string or an array of strings
+	for(var i = aData.length-1; i >= 0; i--) {
+		var aDataset = [];
 
-        if(aData[i]) {
-            if(aData[i].constructor == String) {
-                aDataset[0] = aData[i];
-            }
-            else if(aData[i].constructor == Array) {
-                aDataset = aData[i];
-            }
-        }
+		if(aData[i]) {
+			if(aData[i].constructor == String) {
+				aDataset[0] = aData[i];
+			}
+			else if(aData[i].constructor == Array) {
+				aDataset = aData[i];
+			}
+		}
 
-        if(aDataset[0] && (aDataset[0].constructor == String)) {
-            var sKeyIndex = 0;
-            if (sQuery) {
-              sKeyIndex = (this.queryMatchCase) ?
-                encodeURIComponent(aDataset[0]).indexOf(sQuery):
-                encodeURIComponent(aDataset[0]).toLowerCase().indexOf(sQuery);
-            }
+		if(aDataset[0] && (aDataset[0].constructor == String)) {
+			var sKeyIndex = 0;
+			if (sQuery) {
+				sKeyIndex = (this.queryMatchCase) ?
+					encodeURIComponent(aDataset[0]).indexOf(sQuery):
+					encodeURIComponent(aDataset[0]).toLowerCase().indexOf(sQuery);
+			}
 
-            // A STARTSWITH match is when the query is found at the beginning of the key string...
-            if((!bMatchContains && (sKeyIndex === 0)) ||
-            // A CONTAINS match is when the query is found anywhere within the key string...
-            (bMatchContains && (sKeyIndex > -1))) {
-                // Stash a match into aResults[].
-                aResults.unshift(aDataset);
-            }
-        }
-    }
+			// A STARTSWITH match is when the query is found at the beginning of the key string...
+			if((!bMatchContains && (sKeyIndex === 0)) ||
+				// A CONTAINS match is when the query is found anywhere within the key string...
+				(bMatchContains && (sKeyIndex > -1))) {
+					// Stash a match into aResults[].
+					aResults.unshift(aDataset);
+			}
+		}
+	}
 
-    this.getResultsEvent.fire(this, oParent, sQuery, aResults);
-    oCallbackFn(sQuery, aResults, oParent);
+	this.getResultsEvent.fire(this, oParent, sQuery, aResults);
+	oCallbackFn(sQuery, aResults, oParent);
 };
 
 
@@ -282,242 +281,219 @@
 "neverdisplay"
 ];
 
-    var feedbackDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.feedbackTags);
-    var actionsDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.actionTags);
-    var sectionsDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.sectionTags);
-    var topicsDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.topicTags);
-    var fhitemDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.fhitemOpts);
-    var storyDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.storyOpts);
+var feedbackDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.feedbackTags);
+var actionsDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.actionTags);
+var sectionsDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.sectionTags);
+var topicsDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.topicTags);
+var fhitemDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.fhitemOpts);
+var storyDS = new YAHOO.slashdot.DS_JSArray(YAHOO.slashdot.storyOpts);
 
-    var tagsDS = new YAHOO.widget.DS_XHR("./ajax.pl", ["\n", "\t"]);
-    // tagsDS.maxCacheEntries = 0; // turn off local cacheing, because Jamie says the query is fast
-    tagsDS.queryMatchSubset = false;
-    tagsDS.responseType = YAHOO.widget.DS_XHR.TYPE_FLAT;
-    tagsDS.scriptQueryParam = "prefix";
-    tagsDS.scriptQueryAppend = "op=tags_list_tagnames";
-    tagsDS.queryMethod = "POST";
+var tagsDS = new YAHOO.widget.DS_XHR("./ajax.pl", ["\n", "\t"]);
+// tagsDS.maxCacheEntries = 0; // turn off local cacheing, because Jamie says the query is fast
+tagsDS.queryMatchSubset = false;
+tagsDS.responseType = YAHOO.widget.DS_XHR.TYPE_FLAT;
+tagsDS.scriptQueryParam = "prefix";
+tagsDS.scriptQueryAppend = "op=tags_list_tagnames";
+tagsDS.queryMethod = "POST";
 
-    var fhtabsDS = new YAHOO.widget.DS_XHR("./ajax.pl", ["\n", "\t"]);
-    fhtabsDS.queryMatchSubset = false;
-    fhtabsDS.responseType = YAHOO.widget.DS_XHR.TYPE_FLAT;
-    fhtabsDS.scriptQueryParam = "prefix";
-    fhtabsDS.scriptQueryAppend = "op=firehose_list_tabs";
-    fhtabsDS.queryMethod = "POST";
+var fhtabsDS = new YAHOO.widget.DS_XHR("./ajax.pl", ["\n", "\t"]);
+fhtabsDS.queryMatchSubset = false;
+fhtabsDS.responseType = YAHOO.widget.DS_XHR.TYPE_FLAT;
+fhtabsDS.scriptQueryParam = "prefix";
+fhtabsDS.scriptQueryAppend = "op=firehose_list_tabs";
+fhtabsDS.queryMethod = "POST";
 
 YAHOO.slashdot.dataSources = [tagsDS, actionsDS, sectionsDS, topicsDS, feedbackDS, storyDS, fhitemDS, fhtabsDS ];
 
-YAHOO.slashdot.AutoCompleteWidget = function()
-  {
-    this._widget = document.getElementById("ac-select-widget");
-    this._spareInput = document.getElementById("ac-select-input");
+YAHOO.slashdot.AutoCompleteWidget = function() {
+	this._widget = document.getElementById("ac-select-widget");
+	this._spareInput = document.getElementById("ac-select-input");
 
-    this._sourceEl = null;
-    this._denyNextAttachTo = null;
+	this._sourceEl = null;
+	this._denyNextAttachTo = null;
 
-    YAHOO.util.Event.addListener(document.body, "click", this._onSdClick, this, true);
-    // add body/window blur to detect changing windows?
-  }
+	YAHOO.util.Event.addListener(document.body, "click", this._onSdClick, this, true);
+	// add body/window blur to detect changing windows?
+}
 
-YAHOO.slashdot.AutoCompleteWidget.prototype._textField = function()
-  {
-    if ( this._sourceEl==null || this._sourceEl.type=='text' || this._sourceEl.type=='textarea' )
-      return this._sourceEl;
+YAHOO.slashdot.AutoCompleteWidget.prototype._textField = function() {
+	if ( this._sourceEl==null || this._sourceEl.type=='text' || this._sourceEl.type=='textarea' )
+		return this._sourceEl;
 
-    return this._spareInput;
-  }
+	return this._spareInput;
+}
 
-YAHOO.slashdot.AutoCompleteWidget.prototype._needsSpareInput = function()
-  {
-    // return this._textField() == this._spareInput;
-    return this._sourceEl && (this._sourceEl.type != "text") && (this._sourceEl.type != "textarea");
-  }
+YAHOO.slashdot.AutoCompleteWidget.prototype._needsSpareInput = function() {
+	// return this._textField() == this._spareInput;
+	return this._sourceEl && (this._sourceEl.type != "text") && (this._sourceEl.type != "textarea");
+}
 
-YAHOO.slashdot.AutoCompleteWidget.prototype._newCompleter = function( tagDomain )
-  {
-    var c = null;
-    if ( this._needsSpareInput() )
-      {
-        c = new YAHOO.widget.AutoComplete("ac-select-input", "ac-choices", YAHOO.slashdot.dataSources[tagDomain]);
-        c.minQueryLength = 0;
+YAHOO.slashdot.AutoCompleteWidget.prototype._newCompleter = function( tagDomain ) {
+	var c = null;
+	if ( this._needsSpareInput() ) {
+		c = new YAHOO.widget.AutoComplete("ac-select-input", "ac-choices", YAHOO.slashdot.dataSources[tagDomain]);
+		c.minQueryLength = 0;
 
-          // hack? -- override YUI's private member function so that for top tags auto-complete, right arrow means select
-        c._jumpSelection = function() { if ( this._oCurItem ) this._selectItem(this._oCurItem); };
-      }
-    else
-      {
-        c = new YAHOO.widget.AutoComplete(this._sourceEl, "ac-choices", YAHOO.slashdot.dataSources[tagDomain]);
-        c.delimChar = " ";
-        c.minQueryLength = 3;
-      }
-    c.typeAhead = false;
-    c.forceSelection = false;
-    c.allowBrowserAutocomplete = false;
-    c.maxResultsDisplayed = 25;
-    c.animVert = false;
+		// hack? -- override YUI's private member function so that for top tags auto-complete, right arrow means select
+		c._jumpSelection = function() { if ( this._oCurItem ) this._selectItem(this._oCurItem); };
+	} else {
+		c = new YAHOO.widget.AutoComplete(this._sourceEl, "ac-choices", YAHOO.slashdot.dataSources[tagDomain]);
+		c.delimChar = " ";
+		c.minQueryLength = 3;
+	}
+	c.typeAhead = false;
+	c.forceSelection = false;
+	c.allowBrowserAutocomplete = false;
+	c.maxResultsDisplayed = 25;
+	c.animVert = false;
 
-    return c;
-  }
+	return c;
+}
 
-YAHOO.slashdot.AutoCompleteWidget.prototype._show = function( obj, callbackParams, tagDomain )
-  {
-      // onTextboxBlur should have already hidden the previous instance (if any), but if events
-      //  come out of order, we must hide now to prevent broken listeners
-    if ( this._sourceEl )
-      this._hide();
+YAHOO.slashdot.AutoCompleteWidget.prototype._show = function( obj, callbackParams, tagDomain ) {
+	// onTextboxBlur should have already hidden the previous instance (if any), but if events
+	// come out of order, we must hide now to prevent broken listeners
+	if ( this._sourceEl )
+		this._hide();
 
-    this._sourceEl = obj;
+	this._sourceEl = obj;
 
-    if ( this._sourceEl )
-      {
-        this._callbackParams = callbackParams;
-        this._callbackParams._tagDomain = tagDomain;
-        this._completer = this._newCompleter(tagDomain);
-        
-        if ( typeof callbackParams.yui == "object" )
-          for ( var field in callbackParams.yui )
-            this._completer[field] = callbackParams.yui[field];
+	if ( this._sourceEl ) {
+		this._callbackParams = callbackParams;
+		this._callbackParams._tagDomain = tagDomain;
+		this._completer = this._newCompleter(tagDomain);
 
-        if ( callbackParams.delayAutoHighlight )
-          this._completer.autoHighlight = false;
-          
+		if ( typeof callbackParams.yui == "object" )
+			for ( var field in callbackParams.yui )
+				this._completer[field] = callbackParams.yui[field];
 
-    // widget must be visible to move
-        YAHOO.util.Dom.removeClass(this._widget, "hidden");
-    // move widget to be near the 'source'
-        var pos = YAHOO.util.Dom.getXY(this._sourceEl);
-        pos[1] += this._sourceEl.offsetHeight;
-        YAHOO.util.Dom.setXY(this._widget, pos);
+		if ( callbackParams.delayAutoHighlight )
+			this._completer.autoHighlight = false;
 
-        YAHOO.util.Dom.addClass(this._sourceEl, "ac-source");
+		// widget must be visible to move
+		YAHOO.util.Dom.removeClass(this._widget, "hidden");
+		// move widget to be near the 'source'
+		var pos = YAHOO.util.Dom.getXY(this._sourceEl);
+		pos[1] += this._sourceEl.offsetHeight;
+		YAHOO.util.Dom.setXY(this._widget, pos);
 
-        if ( this._needsSpareInput() )
-          {
-            YAHOO.util.Dom.removeClass(this._spareInput, "hidden");
-            this._spareInput.value = "";
-            this._spareInput.focus();
-            this._pending_hide = setTimeout(YAHOO.slashdot.gCompleterWidget._hide, 15000);
-          }
-        else
-          YAHOO.util.Dom.addClass(this._spareInput, "hidden");
+		YAHOO.util.Dom.addClass(this._sourceEl, "ac-source");
 
-        this._completer.itemSelectEvent.subscribe(this._onSdItemSelectEvent, this);
-        this._completer.unmatchedItemSelectEvent.subscribe(this._onSdItemSelectEvent, this);
-        this._completer.textboxBlurEvent.subscribe(this._onSdTextboxBlurEvent, this);
+		if ( this._needsSpareInput() ) {
+			YAHOO.util.Dom.removeClass(this._spareInput, "hidden");
+			this._spareInput.value = "";
+			this._spareInput.focus();
+			this._pending_hide = setTimeout(YAHOO.slashdot.gCompleterWidget._hide, 15000);
+		} else
+			YAHOO.util.Dom.addClass(this._spareInput, "hidden");
 
-        YAHOO.util.Event.addListener(this._textField(), "keydown", this._onSdTextboxKeyDown, this, true);
-      }
-  }
+		this._completer.itemSelectEvent.subscribe(this._onSdItemSelectEvent, this);
+		this._completer.unmatchedItemSelectEvent.subscribe(this._onSdItemSelectEvent, this);
+		this._completer.textboxBlurEvent.subscribe(this._onSdTextboxBlurEvent, this);
 
-YAHOO.slashdot.AutoCompleteWidget.prototype._hide = function()
-  {
-    if ( this._pending_hide )
-      {
-        clearTimeout(this._pending_hide);
-        this._pending_hide = null;
-      }
+		YAHOO.util.Event.addListener(this._textField(), "keydown", this._onSdTextboxKeyDown, this, true);
+	}
+}
 
-    YAHOO.util.Dom.addClass(this._widget, "hidden");
-    YAHOO.util.Dom.addClass(this._spareInput, "hidden");
-    if ( this._sourceEl )
-      {
-        YAHOO.util.Dom.removeClass(this._sourceEl, "ac-source");
+YAHOO.slashdot.AutoCompleteWidget.prototype._hide = function() {
+	if ( this._pending_hide ) {
+		clearTimeout(this._pending_hide);
+		this._pending_hide = null;
+	}
 
-        YAHOO.util.Event.removeListener(this._textField(), "keydown", this._onSdTextboxKeyDown, this, true);
-        this._completer.itemSelectEvent.unsubscribe(this._onSdItemSelectEvent, this);
-        this._completer.unmatchedItemSelectEvent.unsubscribe(this._onSdItemSelectEvent, this);
-        this._completer.textboxBlurEvent.unsubscribe(this._onSdTextboxBlurEvent, this);
+	YAHOO.util.Dom.addClass(this._widget, "hidden");
+	YAHOO.util.Dom.addClass(this._spareInput, "hidden");
+	if ( this._sourceEl ) {
+		YAHOO.util.Dom.removeClass(this._sourceEl, "ac-source");
 
-        this._sourceEl = null;
-        this._callbackParams = null;
-        this._completer = null;
-      }
+		YAHOO.util.Event.removeListener(this._textField(), "keydown", this._onSdTextboxKeyDown, this, true);
+		this._completer.itemSelectEvent.unsubscribe(this._onSdItemSelectEvent, this);
+		this._completer.unmatchedItemSelectEvent.unsubscribe(this._onSdItemSelectEvent, this);
+		this._completer.textboxBlurEvent.unsubscribe(this._onSdTextboxBlurEvent, this);
 
-    this._denyNextAttachTo = null;
-  }
+		this._sourceEl = null;
+		this._callbackParams = null;
+		this._completer = null;
+	}
 
-YAHOO.slashdot.AutoCompleteWidget.prototype.attach = function( obj, callbackParams, tagDomain )
-  {
-    var newSourceEl = obj;
-    if ( typeof obj == "string" )
-      newSourceEl = document.getElementById(obj);
+	this._denyNextAttachTo = null;
+}
 
-      // act like a menu: if we click on the same trigger while visible, hide
-    var denyThisAttach = this._denyNextAttachTo == newSourceEl;
-    this._denyNextAttachTo = null;
-    if ( denyThisAttach )
-      return;
+YAHOO.slashdot.AutoCompleteWidget.prototype.attach = function( obj, callbackParams, tagDomain ) {
+	var newSourceEl = obj;
+	if ( typeof obj == "string" )
+		newSourceEl = document.getElementById(obj);
 
-    if ( newSourceEl && newSourceEl !== this._sourceEl )
-      {
-        callbackParams._sourceEl = newSourceEl;
-        this._show(newSourceEl, callbackParams, tagDomain);
+		// act like a menu: if we click on the same trigger while visible, hide
+	var denyThisAttach = this._denyNextAttachTo == newSourceEl;
+	this._denyNextAttachTo = null;
+	if ( denyThisAttach )
+		return;
 
-        var q = callbackParams.queryOnAttach;
-        if ( q )
-          this._completer.sendQuery((typeof q == "string") ? q : "");
-      }
-  }
+	if ( newSourceEl && newSourceEl !== this._sourceEl ) {
+		callbackParams._sourceEl = newSourceEl;
+		this._show(newSourceEl, callbackParams, tagDomain);
 
-YAHOO.slashdot.AutoCompleteWidget.prototype._onSdClick = function( e, me )
-  {
-      // if the user re-clicked the item to which I'm attached, then they mean to hide me
-      //  I'm going to hide automatically, because a click outside the text will blur, and that makes me go away
-      //  but I need to remember _not_ to let the current click re-show me
-    var reclicked = me._sourceEl && YAHOO.util.Event.getTarget(e, true) == me._sourceEl;
-    me._denyNextAttachTo = reclicked ? me._sourceEl : null;
-  }
+		var q = callbackParams.queryOnAttach;
+		if ( q )
+			this._completer.sendQuery((typeof q == "string") ? q : "");
+	}
+}
 
-YAHOO.slashdot.AutoCompleteWidget.prototype._onSdItemSelectEvent = function( type, args, me )
-  {
-    var tagname = args[2];
-    if ( tagname !== undefined && tagname !== null ) {
-      if ( typeof tagname != 'string' )
-        tagname = tagname[0];
+YAHOO.slashdot.AutoCompleteWidget.prototype._onSdClick = function( e, me ) {
+		// if the user re-clicked the item to which I'm attached, then they mean to hide me
+		// I'm going to hide automatically, because a click outside the text will blur, and that makes me go away
+		// but I need to remember _not_ to let the current click re-show me
+	var reclicked = me._sourceEl && YAHOO.util.Event.getTarget(e, true) == me._sourceEl;
+	me._denyNextAttachTo = reclicked ? me._sourceEl : null;
+}
 
-      var p = me._callbackParams;
-      if ( p.action0 !== undefined )
-        p.action0(tagname, p);
-      me._hide();
-      if ( p.action1 !== undefined )
-        p.action1(tagname, p);
+YAHOO.slashdot.AutoCompleteWidget.prototype._onSdItemSelectEvent = function( type, args, me ) {
+	var tagname = args[2];
+	if ( tagname !== undefined && tagname !== null ) {
+		if ( typeof tagname != 'string' )
+			tagname = tagname[0];
 
-    } else {
-      me._hide();
-    }
-  }
+		var p = me._callbackParams;
+		if ( p.action0 !== undefined )
+			p.action0(tagname, p);
+		me._hide();
+		if ( p.action1 !== undefined )
+			p.action1(tagname, p);
 
-YAHOO.slashdot.AutoCompleteWidget.prototype._onSdTextboxBlurEvent = function( type, args, me )
-  {
-    var o = me._denyNextAttachTo;
-    me._hide();
-    me._denyNextAttachTo = o;
-  }
+	} else {
+		me._hide();
+	}
+}
 
-YAHOO.slashdot.AutoCompleteWidget.prototype._onSdTextboxKeyDown = function( e, me )
-  {
-    if ( me._callbackParams && me._callbackParams.delayAutoHighlight )
-      {
-        me._callbackParams.delayAutoHighlight = false;
-        me._completer.autoHighlight = true;
-      }
+YAHOO.slashdot.AutoCompleteWidget.prototype._onSdTextboxBlurEvent = function( type, args, me ) {
+	var o = me._denyNextAttachTo;
+	me._hide();
+	me._denyNextAttachTo = o;
+}
 
-    switch ( e.keyCode )
-      {
-        case 27: // esc
-        // any other keys?...
-          me._hide();
-          break;
-        case 13:
-          // I'm sorry to say we have to test first, something somehow somewhere can still
-          //  leave this listener dangling; want to look deeper into this, as this would _still_
-          //  leave the listener dangling
-          if ( me._completer )
-            me._completer.unmatchedItemSelectEvent.fire(me._completer, me, me._completer._sCurQuery);
-          break;
-        default:
-          if ( me._pending_hide )
-            clearTimeout(me._pending_hide);
-          if ( me._needsSpareInput() )
-          me._pending_hide = setTimeout(YAHOO.slashdot.gCompleterWidget._hide, 15000);
-      }
-  }
+YAHOO.slashdot.AutoCompleteWidget.prototype._onSdTextboxKeyDown = function( e, me ) {
+	if ( me._callbackParams && me._callbackParams.delayAutoHighlight ) {
+		me._callbackParams.delayAutoHighlight = false;
+		me._completer.autoHighlight = true;
+	}
+
+	switch ( e.keyCode ) {
+		case 27: // esc
+		// any other keys?...
+			me._hide();
+			break;
+		case 13:
+			// I'm sorry to say we have to test first, something somehow somewhere can still
+			// leave this listener dangling; want to look deeper into this, as this would _still_
+			// leave the listener dangling
+			if ( me._completer )
+				me._completer.unmatchedItemSelectEvent.fire(me._completer, me, me._completer._sCurQuery);
+			break;
+		default:
+			if ( me._pending_hide )
+				clearTimeout(me._pending_hide);
+			if ( me._needsSpareInput() )
+			me._pending_hide = setTimeout(YAHOO.slashdot.gCompleterWidget._hide, 15000);
+	}
+}

Modified: slashjp/trunk/plugins/Ajax/htdocs/images/sd_calendar.js
===================================================================
--- slashjp/trunk/plugins/Ajax/htdocs/images/sd_calendar.js	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Ajax/htdocs/images/sd_calendar.js	2008-04-07 00:26:30 UTC (rev 572)
@@ -1,275 +1,275 @@
 // _*_ Mode: JavaScript; tab-width: 8; indent-tabs-mode: true _*_
-// $Id: sd_calendar.js,v 1.7 2007/06/05 19:04:12 scc Exp $
+// $Id: sd_calendar.js,v 1.9 2008/03/28 20:53:43 pudge Exp $
 
 YAHOO.namespace("slashdot");
 
 function _datesToSelector( selectorFormat, dates ) {
-  function format( d ) {
-    return selectorFormat(d.getFullYear(), d.getMonth()+1, d.getDate(), d.getDay());
-  }
+	function format( d ) {
+		return selectorFormat(d.getFullYear(), d.getMonth()+1, d.getDate(), d.getDay());
+	}
 
-  var s = format(dates[0]);
-  if ( dates[1] !== undefined )
-    s += "-" + format(dates[1]);
-  return s;
+	var s = format(dates[0]);
+	if ( dates[1] !== undefined )
+		s += "-" + format(dates[1]);
+	return s;
 }
 
 function _bundleDates( date1, date2 ) {
-  if ( date1 instanceof Array )
-    return date1;
-  else if ( date2 === undefined )
-    return [ date1 ];
-  else
-    return [ date1, date2 ];
+	if ( date1 instanceof Array )
+		return date1;
+	else if ( date2 === undefined )
+		return [ date1 ];
+	else
+		return [ date1, date2 ];
 }
 
 function datesToHumanReadable( date1, date2 ) {
-  var day_name = ["日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"];
+	var day_name = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
 
-  function day_ordinal( d ) {
-    switch ( d ) {
-      case 1: case 21: case 31: return d+"st";
-      case 2: case 22:          return d+"nd";
-      case 3: case 23:          return d+"rd";
-      default:                  return d+"th";
-    }
-  }
+	function day_ordinal( d ) {
+		switch ( d ) {
+			case 1: case 21: case 31: return d+"st";
+			case 2: case 22:          return d+"nd";
+			case 3: case 23:          return d+"rd";
+			default:                  return d+"th";
+		}
+	}
 
 
-  var month_name = ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"];
+	var month_name = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
 
-  function minimalHumanReadable( y, m, d, wd ) {
-    var now = new Date();
-    if ( now.getFullYear() == y ) {
-      if ( now.getMonth()+1 == m ) {
-      }
-    }
-  }
+	function minimalHumanReadable( y, m, d, wd ) {
+		var now = new Date();
+		if ( now.getFullYear() == y ) {
+			if ( now.getMonth()+1 == m ) {
+			}
+		}
+	}
 
-  
 
-  return _datesToSelector(function(y,m,d){return ""+y+"年"+month_name[m-1]+d+"日";}, _bundleDates(date1, date2));
+
+	return _datesToSelector(function(y,m,d){return ""+d+" "+month_name[m-1]+" "+y;}, _bundleDates(date1, date2));
 }
 
 function datesToYUISelector( date1, date2 ) {
-  return _datesToSelector(function(y,m,d){return ""+m+"/"+d+"/"+y;}, _bundleDates(date1, date2));
+	return _datesToSelector(function(y,m,d){return ""+m+"/"+d+"/"+y;}, _bundleDates(date1, date2));
 }
 
 function datesToKinoSelector( date1, date2 ) {
-  function formatter( y, m, d ) {
-    if ( m < 10 ) m = "0" + m;
-    if ( d < 10 ) d = "0" + d;
-    return "" + y + m + d;
-  }
-  return _datesToSelector(formatter, _bundleDates(date1, date2));
+	function formatter( y, m, d ) {
+		if ( m < 10 ) m = "0" + m;
+		if ( d < 10 ) d = "0" + d;
+		return "" + y + m + d;
+	}
+	return _datesToSelector(formatter, _bundleDates(date1, date2));
 }
 
 // Kino format to YUI format: "20070428".replace(/(....)(..)(..)/, "$2/$3/$1")
 
 function weekOf( date ) {
-  var dayStart = new Date(datesToYUISelector(date));
-  var dayCount = Math.round(dayStart.getTime() / 86400000);
-  var weekStart = dayCount - dayStart.getDay() + 1;
-  var weekStop = weekStart + 6;
-  var startDate = new Date(weekStart * 86400000);
-  var endDate = new Date(weekStop * 86400000);
-  return [startDate, endDate];
+	var dayStart = new Date(datesToYUISelector(date));
+	var dayCount = Math.round(dayStart.getTime() / 86400000);
+	var weekStart = dayCount - dayStart.getDay() + 1;
+	var weekStop = weekStart + 6;
+	var startDate = new Date(weekStart * 86400000);
+	var endDate = new Date(weekStop * 86400000);
+	return [startDate, endDate];
 }
 
 var gOpenCalendarPane = null;
 
 
 YAHOO.slashdot.DateWidget = function( params ) {
-  this.init(params);
+	this.init(params);
 }
 
 YAHOO.slashdot.DateWidget.prototype.init = function( params ) { // id, mode, date, initCallback
-  var peer = null;
-  if ( params.peer !== undefined ) {
-    peer = document.getElementById(params.peer);
-    params.mode = peer._widget._mode;
-    params.date = peer._widget.getDate();
+	var peer = null;
+	if ( params.peer !== undefined ) {
+		peer = document.getElementById(params.peer);
+		params.mode = peer._widget._mode;
+		params.date = peer._widget.getDate();
 
-    this.subscribeToPeer(peer);
-    this._peer = peer;
-  }
+		this.subscribeToPeer(peer);
+		this._peer = peer;
+	}
 
-  this._mode = (params.mode !== undefined) ? params.mode : "now";
+	this._mode = (params.mode !== undefined) ? params.mode : "now";
 
-  var root = document.getElementById(params.id);
-  var find1st = function(name, kind) {
-    return YAHOO.util.Dom.getElementsByClassName(name, kind, root)[0];
-  }
+	var root = document.getElementById(params.id);
+	var find1st = function(name, kind) {
+		return YAHOO.util.Dom.getElementsByClassName(name, kind, root)[0];
+	}
 
-  var widget = this;
+	var widget = this;
 
-  this._element = root;
-  this._dateTab = find1st('date-tab', 'span');
-    this._dateTab._widget = this;
-  this._label = find1st('day-label', 'option');
-  this._calendarPane = find1st('calendar-pane', 'div');
-  this.toggleCalendarPane(false);
+	this._element = root;
+	this._dateTab = find1st('date-tab', 'span');
+		this._dateTab._widget = this;
+	this._label = find1st('day-label', 'option');
+	this._calendarPane = find1st('calendar-pane', 'div');
+	this.toggleCalendarPane(false);
 
-  this._popup = find1st('date-span-popup', 'select');
-    this._popup._widget = this;
+	this._popup = find1st('date-span-popup', 'select');
+		this._popup._widget = this;
 
-  this._calendar = new YAHOO.widget.Calendar(params.id+'-calendar-table', this._calendarPane.id, {maxdate:datesToYUISelector(new Date())});
-  this._calendar.selectEvent.subscribe(this.handleCalendarSelect, this, true);
+	this._calendar = new YAHOO.widget.Calendar(params.id+'-calendar-table', this._calendarPane.id, {maxdate:datesToYUISelector(new Date())});
+	this._calendar.selectEvent.subscribe(this.handleCalendarSelect, this, true);
 
-  root._widget = this;
-  root.setDate = function(d, m) { widget.setDate(d, m); }
-  root.getDateRange = function() { return widget.getDateRange(); }
-  root.changeEvent = new YAHOO.util.CustomEvent("change");
+	root._widget = this;
+	root.setDate = function(d, m) { widget.setDate(d, m); }
+	root.getDateRange = function() { return widget.getDateRange(); }
+	root.changeEvent = new YAHOO.util.CustomEvent("change");
 
-  this._muteEvents = 0;
+	this._muteEvents = 0;
 
-  this.setDate(params.date);
+	this.setDate(params.date);
 
-  if ( peer )
-    peer._widget.subscribeToPeer(this._element);
+	if ( peer )
+		peer._widget.subscribeToPeer(this._element);
 
-  if ( params.init !== undefined )
-    params.init(root);
+	if ( params.init !== undefined )
+		params.init(root);
 }
 
 function attachDateWidgetTo( params ) {
-  return new YAHOO.slashdot.DateWidget(params);
+	return new YAHOO.slashdot.DateWidget(params);
 }
 
 YAHOO.slashdot.DateWidget.prototype.muteEvents = function() {
-  ++this._muteEvents;
+	++this._muteEvents;
 }
 
 YAHOO.slashdot.DateWidget.prototype.unmuteEvents = function() {
-  --this._muteEvents;
+	--this._muteEvents;
 }
 
 YAHOO.slashdot.DateWidget.prototype._reportChanged = function() {
-  if ( ! this._muteEvents )
-    this._element.changeEvent.fire(this.getDateRange(), this._mode, this.getDate());
+	if ( ! this._muteEvents )
+		this._element.changeEvent.fire(this.getDateRange(), this._mode, this.getDate());
 }
 
 YAHOO.slashdot.DateWidget.prototype.severPeer = function() {
-  if ( this._peer !== undefined ) {
-    this._peer._widget.unsubscribeFromPeer(this);
-    this.unsubscribeFromPeer(this._peer);
-    delete this._peer;
-  }
+	if ( this._peer !== undefined ) {
+		this._peer._widget.unsubscribeFromPeer(this);
+		this.unsubscribeFromPeer(this._peer);
+		delete this._peer;
+	}
 }
 
 YAHOO.slashdot.DateWidget.prototype.subscribeToPeer = function( peer ) {
-  peer.changeEvent.subscribe(this.handlePeerChange, this, true);
+	peer.changeEvent.subscribe(this.handlePeerChange, this, true);
 }
 
 YAHOO.slashdot.DateWidget.prototype.unsubscribeFromPeer = function( peer ) {
-  peer.changeEvent.unsubscribe(this.handlePeerChange, this);
+	peer.changeEvent.unsubscribe(this.handlePeerChange, this);
 }
 
 YAHOO.slashdot.DateWidget.prototype.setMode = function( newMode ) {
-  var oldMode = this._mode;
-  var modeChanged = (newMode !== undefined) && (newMode != oldMode);
-  if ( modeChanged ) {
-    if ( newMode == "all" )
-      this.toggleCalendarPane(false);
-    YAHOO.util.Dom.replaceClass(this._element, oldMode, newMode);
-    this._mode = newMode;
-    this._popup.value = newMode;
-  }
+	var oldMode = this._mode;
+	var modeChanged = (newMode !== undefined) && (newMode != oldMode);
+	if ( modeChanged ) {
+		if ( newMode == "all" )
+			this.toggleCalendarPane(false);
+		YAHOO.util.Dom.replaceClass(this._element, oldMode, newMode);
+		this._mode = newMode;
+		this._popup.value = newMode;
+	}
 
-  if ( modeChanged )
-    this._reportChanged();
+	if ( modeChanged )
+		this._reportChanged();
 
-  return modeChanged;
+	return modeChanged;
 }
 
 YAHOO.slashdot.DateWidget.prototype.setDate = function( date, mode ) {
-  this.muteEvents();
-    if ( date === undefined )
-      date = new Date();
-    this._calendar.select(date);
-    this._calendar.render();
-    var dateChanged = this._setDateFromSelection(date);
+	this.muteEvents();
+		if ( date === undefined )
+			date = new Date();
+		this._calendar.select(date);
+		this._calendar.render();
+		var dateChanged = this._setDateFromSelection(date);
 
-    var modeChanged = false;
-    if ( mode !== undefined )
-      modeChanged = this.setMode(mode);
-  this.unmuteEvents();
+		var modeChanged = false;
+		if ( mode !== undefined )
+			modeChanged = this.setMode(mode);
+	this.unmuteEvents();
 
-  if ( dateChanged || modeChanged )
-    this._reportChanged();
+	if ( dateChanged || modeChanged )
+		this._reportChanged();
 }
 
 YAHOO.slashdot.DateWidget.prototype._setDateFromSelection = function( date, allowModeChange ) {
-  var oldLabel = this._label.innerHTML;
-  var newLabel = "" + datesToHumanReadable(date);
-  var labelChanged = oldLabel != newLabel;
-  if ( labelChanged )
-    this._label.innerHTML = newLabel;
+	var oldLabel = this._label.innerHTML;
+	var newLabel = "Day of " + datesToHumanReadable(date);
+	var labelChanged = oldLabel != newLabel;
+	if ( labelChanged )
+		this._label.innerHTML = newLabel;
 
-  var modeChanged = false;
-  if ( allowModeChange==true ) {
-    var today = new Date();
-    var newMode = ( date.getFullYear() == today.getFullYear()
-                 && date.getMonth()    == today.getMonth()
-                 && date.getDate()     == today.getDate() ) ? "now" : "day";
+	var modeChanged = false;
+	if ( allowModeChange==true ) {
+		var today = new Date();
+		var newMode = ( date.getFullYear() == today.getFullYear()
+		             && date.getMonth()    == today.getMonth()
+		             && date.getDate()     == today.getDate() ) ? "now" : "day";
 
-    this.muteEvents();
-    modeChanged = this.setMode(newMode);
-    this.unmuteEvents();
-  }
+		this.muteEvents();
+		modeChanged = this.setMode(newMode);
+		this.unmuteEvents();
+	}
 
-  if ( labelChanged || modeChanged )
-    this._reportChanged();
+	if ( labelChanged || modeChanged )
+		this._reportChanged();
 
-  return labelChanged || modeChanged;
+	return labelChanged || modeChanged;
 }
 
 YAHOO.slashdot.DateWidget.prototype.getDate = function() {
-  return this._calendar.getSelectedDates()[0];
+	return this._calendar.getSelectedDates()[0];
 }
 
 YAHOO.slashdot.DateWidget.prototype.getDateRange = function() {
-  var range = { duration: -1 };
+	var range = { duration: -1 };
 
-  var start = null;
-  if ( this._mode == "day" ) {
-    start = this.getDate();
-    range.duration = 1;
-  } else if ( this._mode == "now" ) {
-    range.duration = 7;
-  }
+	var start = null;
+	if ( this._mode == "day" ) {
+		start = this.getDate();
+		range.duration = 1;
+	} else if ( this._mode == "now" ) {
+		range.duration = 7;
+	}
 
-  if ( start !== null )
-    range.startdate = datesToKinoSelector(start);
+	if ( start !== null )
+		range.startdate = datesToKinoSelector(start);
 
-  return range;
+	return range;
 }
 
 YAHOO.slashdot.DateWidget.prototype.toggleCalendarPane = function( show ) {
-  if ( gOpenCalendarPane !== null && gOpenCalendarPane !== this ) {
-    gOpenCalendarPane.toggleCalendarPane(false);
-  }
-  this._calendarPane.style.display = show ? 'block' : 'none';
-  YAHOO.util.Dom[ show ? 'addClass' : 'removeClass' ](this._dateTab, 'active');
-  gOpenCalendarPane = show ? this : null;
+	if ( gOpenCalendarPane !== null && gOpenCalendarPane !== this ) {
+		gOpenCalendarPane.toggleCalendarPane(false);
+	}
+	this._calendarPane.style.display = show ? 'block' : 'none';
+	YAHOO.util.Dom[ show ? 'addClass' : 'removeClass' ](this._dateTab, 'active');
+	gOpenCalendarPane = show ? this : null;
 }
 
 YAHOO.slashdot.DateWidget.prototype.handleDateTabClick = function() {
-  this.toggleCalendarPane( ! YAHOO.util.Dom.hasClass(this._dateTab, 'active') );
+	this.toggleCalendarPane( ! YAHOO.util.Dom.hasClass(this._dateTab, 'active') );
 }
 
 YAHOO.slashdot.DateWidget.prototype.handleCalendarSelect = function( type, args, obj ) {
-  this._setDateFromSelection(this._calendar._toDate(args[0][0]), true);
-  this.toggleCalendarPane(false);
+	this._setDateFromSelection(this._calendar._toDate(args[0][0]), true);
+	this.toggleCalendarPane(false);
 }
 
 YAHOO.slashdot.DateWidget.prototype.handleRangePopupSelect = function( obj ) {
-  this.setMode(obj.value);
+	this.setMode(obj.value);
 }
 
 YAHOO.slashdot.DateWidget.prototype.handlePeerChange = function( type, args, obj ) {
-  this.muteEvents();
-    this.setDate(args[2], args[1]);
-  this.unmuteEvents();
+	this.muteEvents();
+		this.setDate(args[2], args[1]);
+	this.unmuteEvents();
 }
 

Modified: slashjp/trunk/plugins/Ajax/htdocs/images/slashbox.js
===================================================================
--- slashjp/trunk/plugins/Ajax/htdocs/images/slashbox.js	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Ajax/htdocs/images/slashbox.js	2008-04-07 00:26:30 UTC (rev 572)
@@ -1,101 +1,90 @@
 YAHOO.namespace("slashdot");
 
-YAHOO.slashdot.SlashBox = function( id, sGroup, config )
-  {
-    if ( id )
-      {
-        this.init(id, sGroup, config);
-        this.initFrame();
-        this.logger = this.logger || YAHOO;
-      }
+YAHOO.slashdot.SlashBox = function( id, sGroup, config ) {
+	if ( id ) {
+		this.init(id, sGroup, config);
+		this.initFrame();
+		this.logger = this.logger || YAHOO;
+	}
 
-    this.deleteBoundaryId = sGroup;
-  }
+	this.deleteBoundaryId = sGroup;
+}
 
 YAHOO.extend(YAHOO.slashdot.SlashBox, YAHOO.util.DDProxy);
 
 
-YAHOO.slashdot.SlashBox.prototype.createFrame()
-  {
-    // ...
-  }
+YAHOO.slashdot.SlashBox.prototype.createFrame = function() {
+	// ...
+}
 
 
-YAHOO.slashdot.SlashBox.prototype.startDrag = function(x, y)
-  {
-    var dragEl = this.getDragEl();
-    var clickEl = this.getEl();
+YAHOO.slashdot.SlashBox.prototype.startDrag = function(x, y) {
+	var dragEl = this.getDragEl();
+	var clickEl = this.getEl();
 
-    dragEl.innerHTML = clickEl.innerHTML;
-    dragEl.className = clickEl.className;
+	dragEl.innerHTML = clickEl.innerHTML;
+	dragEl.className = clickEl.className;
 
-    YAHOO.util.Dom.addClass(clickEl, "to-be-moved");
-      // so we can style the object-to-be-moved in CSS
-  }
+	YAHOO.util.Dom.addClass(clickEl, "to-be-moved");
+		// so we can style the object-to-be-moved in CSS
+}
 
-YAHOO.slashdot.SlashBox.prototype.endDrag = function(e)
-  {
-    YAHOO.util.Dom.removeClass(this.getEl(), "to-be-moved");
-      // done moving, back to your regularly scheduled CSS (see this.startDrag)
-  }
+YAHOO.slashdot.SlashBox.prototype.endDrag = function(e) {
+	YAHOO.util.Dom.removeClass(this.getEl(), "to-be-moved");
+		// done moving, back to your regularly scheduled CSS (see this.startDrag)
+}
 
-YAHOO.slashdot.SlashBox.prototype.onDragOver = function(e, id)
-  {
-    if ( id == this.deleteBoundaryId )
-      return;
+YAHOO.slashdot.SlashBox.prototype.onDragOver = function(e, id) {
+	if ( id == this.deleteBoundaryId )
+		return;
 
-    var pointer_y = YAHOO.util.Event.getPageY(e);
-    var dragged_box = this.getEl();
-    var fixed_box;
-    
-    if ("string" == typeof id)
-      fixed_box = YAHOO.util.DDM.getElement(id);
-    else
-      fixed_box = YAHOO.util.DDM.getBestMatch(id).getEl();
+	var pointer_y = YAHOO.util.Event.getPageY(e);
+	var dragged_box = this.getEl();
+	var fixed_box;
 
-    var parent = fixed_box.parentNode;
+	if ("string" == typeof id)
+		fixed_box = YAHOO.util.DDM.getElement(id);
+	else
+		fixed_box = YAHOO.util.DDM.getBestMatch(id).getEl();
 
-    var dragged_top = YAHOO.util.DDM.getPosY(dragged_box);
-    var fixed_top = YAHOO.util.DDM.getPosY(fixed_box);
-    
-    var fixed_mid = fixed_top + ( Math.floor(fixed_box.offsetHeight / 2));
+	var parent = fixed_box.parentNode;
 
-    var dragging_down = dragged_top < fixed_top;
+	var dragged_top = YAHOO.util.DDM.getPosY(dragged_box);
+	var fixed_top = YAHOO.util.DDM.getPosY(fixed_box);
+	
+	var fixed_mid = fixed_top + ( Math.floor(fixed_box.offsetHeight / 2));
 
+	var dragging_down = dragged_top < fixed_top;
 
-    if ( dragging_down && pointer_y > fixed_mid )
-      parent.insertBefore(fixed_box, dragged_box);
-    else if ( !dragging_down && pointer_y < fixed_mid )
-      parent.insertBefore(dragged_box, fixed_box);
-    else
-      return;
-  }
 
-YAHOO.slashdot.SlashBox.prototype.onDragEnter = function(e, id)
-  {
-    if ( id == this.deleteBoundaryId )
-      {
-        var dragEl = this.getDragEl();
-        var clickEl = this.getEl();
-        YAHOO.util.Dom.removeClass(dragEl, "to-be-deleted");
-        YAHOO.util.Dom.removeClass(clickEl, "to-be-deleted");
-          // so we can style the object-to-be-moved in CSS
-      }
-  }
+	if ( dragging_down && pointer_y > fixed_mid )
+		parent.insertBefore(fixed_box, dragged_box);
+	else if ( !dragging_down && pointer_y < fixed_mid )
+		parent.insertBefore(dragged_box, fixed_box);
+	else
+		return;
+}
 
-YAHOO.slashdot.SlashBox.prototype.onDragOut = function(e, id)
-  {
-    if ( id == this.deleteBoundaryId )
-      {
-        var dragEl = this.getDragEl();
-        var clickEl = this.getEl();
-        YAHOO.util.Dom.addClass(dragEl, "to-be-deleted");
-        YAHOO.util.Dom.addClass(clickEl, "to-be-deleted");
-          // so we can style the object-to-be-moved in CSS
-      }
-  }
+YAHOO.slashdot.SlashBox.prototype.onDragEnter = function(e, id) {
+	if ( id == this.deleteBoundaryId ) {
+		var dragEl = this.getDragEl();
+		var clickEl = this.getEl();
+		YAHOO.util.Dom.removeClass(dragEl, "to-be-deleted");
+		YAHOO.util.Dom.removeClass(clickEl, "to-be-deleted");
+			// so we can style the object-to-be-moved in CSS
+	}
+}
 
-YAHOO.slashdot.SlashBox.prototype.onDragDrop = function(e, id)
-  {
-    ajaxSaveSlashboxes();
-  }
+YAHOO.slashdot.SlashBox.prototype.onDragOut = function(e, id) {
+	if ( id == this.deleteBoundaryId ) {
+		var dragEl = this.getDragEl();
+		var clickEl = this.getEl();
+		YAHOO.util.Dom.addClass(dragEl, "to-be-deleted");
+		YAHOO.util.Dom.addClass(clickEl, "to-be-deleted");
+			// so we can style the object-to-be-moved in CSS
+	}
+}
+
+YAHOO.slashdot.SlashBox.prototype.onDragDrop = function(e, id) {
+	ajaxSaveSlashboxes();
+}

Modified: slashjp/trunk/plugins/Ajax/templates/data;ajax;default
===================================================================
--- slashjp/trunk/plugins/Ajax/templates/data;ajax;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Ajax/templates/data;ajax;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -10,11 +10,20 @@
 en_US
 __name__
 data
-__seclev__
-10000
 __template__
 [% SWITCH value %]
 	
 [% CASE 'set_section_prefs_success_msg' %]
 	<a href="#" onclick="window.location.reload(); return false" style="color:#fff;">Close</a>
+
+[% CASE 'inline preview warning' %]
+	<p>This comment will not be saved until you click the Submit button below.</p>
+
+[% CASE 'no modcommentlog' %]
+	<p>No comment history available.</p>
+
 [% END %]
+__seclev__
+1000
+__version__
+$Id$

Modified: slashjp/trunk/plugins/Ajax/templates/edit_comment;ajax;default
===================================================================
--- slashjp/trunk/plugins/Ajax/templates/edit_comment;ajax;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Ajax/templates/edit_comment;ajax;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -19,57 +19,67 @@
 __name__
 edit_comment
 __template__
-<div id="wide">
-[% this_title = pid ? reply.subject : discussion.title;
-   this_title = this_title | strip_html;
-   PROCESS titlebar title="Reply to: $this_title" %]
+<div class="inline_comment">
+[% IF user.is_anon %]
+	<p class="warning">
+		[% IF constants.allow_anonymous %]
+	You are not logged in.  You can <a href=\"${gSkin.rootdir}/login.pl\">log
+	in now</a>, or <a href="[% gSkin.rootdir %]/users.pl">Create an Account</a>.
+		[% ELSE %]
+	You are not logged in.  You can <a href=\"${gSkin.rootdir}/login.pl\">log
+	in now</a>, <a href="[% gSkin.rootdir %]/users.pl">Create an Account</a>,
+	or post as <b>[% user.nickname | strip_literal %]</b>.
+		[% END %]
+	</p>
+[% END %]
 
-<p>If you have difficulty with this form, please use the <a href="[% gSkin.rootdir %]/comments.pl?op=Reply&amp;sid=[% sid %]&amp;pid=[% pid %]">old form</a>.</p>
-
-[% IF user.is_anon %]<p>
-	[% IF constants.allow_anonymous %]
-You are not logged in.  You can <a href=\"${gSkin.rootdir}/login.pl\">log
-in now</a>, or <a href="[% gSkin.rootdir %]/users.pl">Create an Account</a>.
-	[% ELSE %]
-You are not logged in.  You can <a href=\"${gSkin.rootdir}/login.pl\">log
-in now</a>, <a href="[% gSkin.rootdir %]/users.pl">Create an Account</a>,
-or post as <b>[% user.nickname | strip_literal %]</b>.
-	[% END %]
-</p>[% END %]
-
 [% IF !user.is_anon || constants.allow_anonymous %]
-<form action="[% gSkin.rootdir %]/comments.pl" method="post">
-	<div id="replyto_preview_[% pid %]" class="replyto_reply" onclick="editReply([% pid %])"></div>
-	<div id="replyto_reply_[% pid %]" class="replyto_reply">
-<input type="hidden" name="sid" value="[% sid %]">
-[% IF pid %]<input type="hidden" name="pid" value="[% pid %]">[% END %]
-<input type="hidden" name="gotmodwarning_[% pid %]" id="gotmodwarning_[% pid %]" value="0">
-[% reskey_label = "reskey_reply_$pid"; PROCESS reskey_tag %]
-<p><input type="text" name="postersubj_[% pid %]" id="postersubj_[% pid %]" value="[% form.postersubj | strip_attribute %]" size="50" maxlength="50">
-[% UNLESS user.is_anon %][<a href="[% gSkin.rootdir %]/my/comments" onclick="getModalPrefs('d2_posting', 'Discussion 2'); return false;">Options</a>]
-[%- IF constants.allow_anonymous && user.karma > -1 && (discussion.commentstatus == 'enabled' || discussion.commentstatus == 'logged_in') -%]
-	<input type="checkbox" name="postanon_[% pid %]" id="postanon_[% pid %]"> Post Anonymously
-[%- END %]</p>[% END %]
-<p><textarea wrap="virtual" name="postercomment_[% pid %]" id="postercomment_[% pid %]" rows="[% user.textarea_rows || constants.textarea_rows %]" cols="[% user.textarea_cols || constants.textarea_cols %]"></textarea></p>
-
-	</div>
-	<div id="replyto_msg_[% pid %]" class="replyto_msg"></div>
-	<div class="replyto_buttons">
-		<span id="replyto_buttons_1_[% pid %]">
-<input type="button" name="preview_[% pid %]"    id="preview_[% pid %]"    value="Preview"          class="button" onclick="previewReply([% pid %]); return false;">
-[%- IF pid # not for root-level reply %]
-<input type="button" name="quotereply_[% pid %]" id="quotereply_[% pid %]" value="Quote Parent"     class="button" onclick="quoteReply([% pid %]);   return false;">[% END %]
-		</span><span id="replyto_buttons_2_[% pid %]" style="display: none">
-<input type="button" name="submit_[% pid %]"     id="submit_[% pid %]"     value="Submit"           class="button" onclick="submitReply([% pid %]);  return false;">
-<input type="button" name="edit_[% pid %]"       id="edit_[% pid %]"       value="Continue Editing" class="button" onclick="editReply([% pid %]);    return false;">
-		</span><span id="replyto_buttons_3_[% pid %]">
-<input type="button" name="cancel_[% pid %]"     id="cancel_[% pid %]"     value="Cancel"           class="button" onclick="cancelReply([% pid %]);  return false;">
-		</span>
-	</div>
-</form>
+	<form action="[% gSkin.rootdir %]/comments.pl" method="post">
+		<div id="replyto_preview_[% pid %]" class="replyto_reply" ondblclick="editReply([% pid %])"></div>
+		<div id="replyto_reply_[% pid %]" class="replyto_reply">
+			<input type="hidden" name="sid" value="[% sid %]">
+			[% IF pid %]<input type="hidden" name="pid" value="[% pid %]">[% END %]
+			<input type="hidden" name="gotmodwarning_[% pid %]" id="gotmodwarning_[% pid %]" value="0">
+			[% reskey_label = "reskey_reply_$pid"; PROCESS reskey_tag %]
+			<div class="generaltitle">
+				<div class="title">
+					<h3>
+						<input type="text" name="postersubj_[% pid %]" id="postersubj_[% pid %]" value="[% form.postersubj | strip_attribute %]" size="50" maxlength="50">
+						[% UNLESS user.is_anon %]
+							[%- IF constants.allow_anonymous && user.karma > -1 && (discussion.commentstatus == 'enabled' || discussion.commentstatus == 'logged_in') -%]
+									<input type="checkbox" name="postanon_[% pid %]" id="postanon_[% pid %]"> Post Anonymously
+							[%- END %]
+						[% END %]
+					</h3>
+					[% UNLESS user.is_anon %]<span class="pref"><a href="[% gSkin.rootdir %]/my/comments" onclick="getModalPrefs('d2_posting', 'Discussion 2'); return false;">Preferences</a></span>[% END %]
+				</div>
+			</div>
+			<div class="generalbody">
+				<p>If you have difficulty with this form, please use the <a href="[% gSkin.rootdir %]/comments.pl?op=Reply&amp;sid=[% sid %]&amp;pid=[% pid %]">old form</a>.</p>
+				<textarea wrap="virtual" name="postercomment_[% pid %]" id="postercomment_[% pid %]" rows="[% user.textarea_rows || constants.textarea_rows %]" cols="[% user.textarea_cols || constants.textarea_cols %]"></textarea>	
+			</div>
+		</div>
+		<div id="replyto_msg_[% pid %]" class="replyto_msg" style="display: none"></div>
+		<div class="replyto_buttons">
+			<span id="replyto_buttons_1_[% pid %]">
+				<input type="button" name="preview_[% pid %]" id="preview_[% pid %]" value="Preview" class="button" onclick="previewReply([% pid %]); return false;">
+				[%- IF pid # not for root-level reply %]
+				<input type="button" name="quotereply_[% pid %]" id="quotereply_[% pid %]" value="Quote Parent" class="button" onclick="quoteReply([% pid %]); return false;">[% END %]
+				[%- UNLESS user.is_anon %]
+				<input type="button" name="prefs_[% pid %]" id="prefs_[% pid %]" value="Options" class="button" onclick="getModalPrefs('d2_posting', 'Discussion 2'); return false;">[% END %]
+			</span>
+			<span id="replyto_buttons_2_[% pid %]" style="display: none">
+				<input type="button" name="submit_[% pid %]" id="submit_[% pid %]" value="Submit" class="button" onclick="submitReply([% pid %]); return false;">
+				<input type="button" name="edit_[% pid %]" id="edit_[% pid %]" value="Continue Editing" class="button" onclick="editReply([% pid %]); return false;">
+				<span class="state">Preview</span>
+			</span>
+			<span id="replyto_buttons_3_[% pid %]">
+				<input type="button" name="cancel_[% pid %]" id="cancel_[% pid %]" value="Cancel" class="button" onclick="cancelReply([% pid %]); return false;">
+			</span>
+		</div>
+	</form>
 [% END # IF !user.is_anon || constants.allow_anonymous %]
 </div>
-
 __seclev__
 1000
 __version__

Copied: slashjp/trunk/plugins/Ajax/templates/hc_comment;ajax;default (from rev 571, slashjp/branches/upstream/current/plugins/Ajax/templates/hc_comment;ajax;default)
===================================================================
--- slashjp/trunk/plugins/Ajax/templates/hc_comment;ajax;default	                        (rev 0)
+++ slashjp/trunk/plugins/Ajax/templates/hc_comment;ajax;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -0,0 +1,29 @@
+__section__
+default
+__description__
+Template which renders the comment editor.
+
+* error_message = error message if there is an error
+* preview = preview of comment, if applicable
+* reply = hashref of comment replying to
+* hide_name = hide name / log out link
+* hide_email = hide email display
+* extras = array of any extras associated with this comment
+ 
+__title__
+
+__page__
+ajax
+__lang__
+en_US
+__name__
+hc_comment
+__template__
+[% IF user.state.hc && !user.state.hcinvalid %]
+<p>[% user.state.hcquestion; user.state.hchtml %]
+<input type="text" id="hcanswer_[% pid %]" value="[% form.hcanswer | strip_attribute %]" size="8" maxlength="8">
+[% END %]
+__seclev__
+1000
+__version__
+$Id: hc_comment;ajax;default,v 1.1 2008/03/25 18:46:24 pudge Exp $

Modified: slashjp/trunk/plugins/Console/console.pl
===================================================================
--- slashjp/trunk/plugins/Console/console.pl	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Console/console.pl	2008-04-07 00:26:30 UTC (rev 572)
@@ -65,8 +65,7 @@
 	my $tagnamesbox = '';
 	my $tags = getObject('Slash::Tags');
 	if ($tags) {
-		my $rtoi_ar = $tags->getRecentTagnamesOfInterest();
-		$tagnamesbox = $tags->showRecentTagnamesBox();
+		$tagnamesbox = $tags->showRecentTagnamesBox({ box_only => 1});
 	}
 
 	slashDisplay('display', {

Modified: slashjp/trunk/plugins/FireHose/FireHose.pm
===================================================================
--- slashjp/trunk/plugins/FireHose/FireHose.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/FireHose/FireHose.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -380,6 +380,9 @@
 
 	$options ||= {};
 	$options->{limit} ||= 50;
+	my $ps = $options->{limit};
+	
+	$options->{limit} += $options->{more_num} if $options->{more_num};
 
 	my $pop;
 	$pop = $self->getMinPopularityForColorLevel($colors->{$options->{color}})
@@ -609,9 +612,10 @@
 	$other = 'GROUP BY firehose.id' if $options->{tagged_by_uid};
 
 	my $count_other = $other;
+	my $offset;
 
 	if (1 || !$doublecheck) { # do always for now
-		my $offset = defined $options->{offset} ? $options->{offset} : '';
+		$offset = defined $options->{offset} ? $options->{offset} : '';
 		$offset = '' if $offset !~ /^\d+$/;
 		$offset = "$offset, " if length $offset;
 		$limit_str = "LIMIT $offset $options->{limit}" unless $options->{nolimit};
@@ -630,10 +634,11 @@
 	}
 
 
-	my $page_size = $options->{limit} || 1;
+	my $page_size = $ps || 1;
 	$results->{records_pages} ||= ceil($count / $page_size);
 	$results->{records_page}  ||= (int(($options->{offset} || 0) / $options->{limit}) + 1) || 1;
 
+	my $future_count = $count - $options->{limit} - ($options->{offset} || 0);
 
 	if (keys %$filter_globjids) {
 		for my $i (0 .. $#{$hr_ar}) {
@@ -664,7 +669,7 @@
 
 		$items = $hr_ar;
 	}
-	return($items, $results, $count);
+	return($items, $results, $count, $future_count);
 }
 
 # A single-globjid wrapper around getUserFireHoseVotesForGlobjs.
@@ -1017,7 +1022,7 @@
 	}
 
 	my $eval_first = "";
-	for my $o (qw(startdate mode fhfilter orderdir orderby startdate duration color)) {
+	for my $o (qw(startdate mode fhfilter orderdir orderby startdate duration color more_num)) {
 		my $value = $opts->{$o};
 		if ($o eq 'orderby' && $value eq 'editorpop') {
 			$value = 'popularity';
@@ -1025,6 +1030,9 @@
 		if ($o eq 'startdate') {
 			$value =~ s/-//g;
 		}
+		if ($o eq 'more_num') {
+			$value ||= 0;
+		}
 		$eval_first .= "firehose_settings.$o = " . Data::JavaScript::Anon->anon_dump("$value") . "; ";
 	}
 
@@ -1162,7 +1170,7 @@
 	my %ids = map { $_ => 1 } @ids;
 	my %ids_orig = ( %ids ) ;
 	my $opts = $firehose->getAndSetOptions({ no_set => 1 });
-	my($items, $results) = $firehose_reader->getFireHoseEssentials($opts);
+	my($items, $results, $count, $future_count) = $firehose_reader->getFireHoseEssentials($opts);
 	my $num_items = scalar @$items;
 	my $future = {};
 	my $globjs = [];
@@ -1304,6 +1312,7 @@
 	$html->{filter_text} = "Filtered to ".strip_literal($opts->{color})." '".strip_literal($opts->{fhfilter})."'";
 	$html->{gmt_update_time} = " (".timeCalc($slashdb->getTime(), "%H:%M", 0)." GMT) " if $user->{is_admin};
 	$html->{itemsreturned} = $num_items == 0 ?  getData("noitems", { options => $opts }, 'firehose') : "";
+	$html->{firehose_more} = getData("firehose_more_link", { options => $opts, future_count => $future_count, contentsonly => 1}, 'firehose');
 
 	my $data_dump =  Data::JavaScript::Anon->anon_dump({
 		html		=> $html,
@@ -2153,6 +2162,15 @@
 	if ($form->{not_id} && $form->{not_id} =~ /^\d+$/) {
 		$options->{not_id} = $form->{not_id};
 	}
+
+
+	if ($form->{more_num} && $form->{more_num} =~ /^\d+$/) {
+		$options->{more_num} = $form->{more_num};
+		if (!$user->{is_admin} && (($options->{limit} + $options->{more_num}) > 200)) {
+			$options->{more_num} = 200 - $options->{limit} ;
+		}
+	}
+
 	return $options;
 }
 
@@ -2277,7 +2295,7 @@
 	if ($featured && $featured->{id}) {
 		$options->{not_id} = $featured->{id};
 	}
-	my($items, $results) = $firehose_reader->getFireHoseEssentials($options);
+	my($items, $results, $count, $future_count) = $firehose_reader->getFireHoseEssentials($options);
 
 	my $itemnum = scalar @$items;
 
@@ -2346,6 +2364,8 @@
 		$section = $gSkin->{skid};
 	}
 
+	my $firehose_more = getData('firehose_more_link', { future_count => $future_count, options => $options }, 'firehose');
+
 	slashDisplay("list", {
 		itemstext	=> $itemstext, 
 		itemnum		=> $itemnum,
@@ -2361,7 +2381,8 @@
 		fh_page		=> $base_page,
 		search_results	=> $results,
 		featured	=> $featured,
-		section		=> $section
+		section		=> $section,
+		firehose_more 	=> $firehose_more
 	}, { Page => "firehose", Return => 1 });
 }
 

Modified: slashjp/trunk/plugins/FireHose/templates/data;firehose;default
===================================================================
--- slashjp/trunk/plugins/FireHose/templates/data;firehose;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/FireHose/templates/data;firehose;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -93,6 +93,24 @@
 		[% END %]
 		</b>
 		<br><br>
+[% CASE 'firehose_more_link' %]
+		[% extra_onclick = '' %]
+		[% IF !contentsonly %]
+		<div id="firehose_more">
+		[% END %]
+		[% IF day_label && day_count;
+			label = day_count _ " more " _ $day_label _ "...";
+		   ELSIF future_count > 0;
+		        label = future_count _ " more...";
+		   ELSIF future_count <= 0 && !options.startdate && options.duration != -1 && options.orderby == "createtime";
+		   	label = "Get more...";
+			extra_onclick = 'firehose_set_options(\'duration\', -1); ';
+		   END;
+		%]
+		<a href="#" onclick="[% extra_onclick %] firehose_more(); return false;">[% label %]</a>
+		[% IF !contentsonly %]
+		</div>
+		[% END %]
 [% CASE 'notavailable' %]
 	The item you're trying to view either does not exist, or is not viewable to you.
 [% END %]

Modified: slashjp/trunk/plugins/FireHose/templates/dispTopicFireHose;misc;default
===================================================================
--- slashjp/trunk/plugins/FireHose/templates/dispTopicFireHose;misc;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/FireHose/templates/dispTopicFireHose;misc;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -14,12 +14,16 @@
 10000
 __template__
 [% topic = Slash.db.getTopic(item.tid) %]
-[% IF (item.thumb && (item.type == "story" || (item.type == "submission" && adminmode))) %]
+[% IF item.type == "story";
+	story = Slash.db.getStory(item.srcid);
+	
+ END %]
+[% IF item.thumb && ((item.type == "story" && (!story.thumb_signoff_needed || adminmode)) || (item.type == "submission" && adminmode)) %]
 	[% file = Slash.db.getStaticFile(item.thumb); %]
 	[% fh = Slash.getObject("Slash::FireHose");
 	   link_url = fh.linkFireHose(item);
 	%]
-	<a href="[% link_url %]"><img src="[% constants.imagedir %][% file.name %]" alt="thumbnail" [% IF file.width; "width='$file.width'"; END; IF file.height; " height='$file.height'"; END; %] [% IF item.media %]onclick="firehose_get_media_popup('[% item.id %]'); return false;"[% END %]></a><br>[% IF item.media %]<a href="[% link_url %]" onclick="firehose_get_media_popup('[% item.id %]')">[% END %]<br>[% IF item.media %]<a href="[% link_url %]" onclick="firehose_get_media_popup('[% item.id %]'); return false;">Watch</a>[% END %]
+	<a href="[% link_url %]"><img src="[% constants.imagedir %][% file.name %]" alt="thumbnail" [% IF file.width; "width='$file.width'"; IF file.height; " height='$file.height'"; END; %] [% IF item.media %]onclick="firehose_get_media_popup('[% item.id %]'); return false;"[% END %]></a><br>[% IF item.media %]<a href="[% link_url %]" onclick="firehose_get_media_popup('[% item.id %]')">[% END %]<br>[% IF item.media %]<a href="[% link_url %]" onclick="firehose_get_media_popup('[% item.id %]'); return false;">Watch</a>[% END %][% END %]
 							 
 [% ELSIF user.noicons || user.simpledesign || user.lowbandwidth %]
 	[ <a href="[% gSkin.rootdir %]/search.pl?tid=[% topic.tid %]">[% topic.textname %]</a> ]

Modified: slashjp/trunk/plugins/FireHose/templates/fireHoseForm;misc;default
===================================================================
--- slashjp/trunk/plugins/FireHose/templates/fireHoseForm;misc;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/FireHose/templates/fireHoseForm;misc;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -48,7 +48,7 @@
 [% END %]
 [% IF needjssubmit %]
 <script type="text/javascript">
-	$('postform-[% item.id %]').submit();
+	$dom('postform-[% item.id %]').submit();
 </script>
 [% END %]
 __version__

Modified: slashjp/trunk/plugins/FireHose/templates/firehose_pages;misc;default
===================================================================
--- slashjp/trunk/plugins/FireHose/templates/firehose_pages;misc;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/FireHose/templates/firehose_pages;misc;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -66,7 +66,7 @@
        IF i_page == start_page_num %]<b>&lsaquo;</b> [% END;
        IF i_page == page_cur %]<strong>[% i_page %]</strong>[%
          ELSE; i_page; END;
-       IF i_page == end_page_num %] <b>&rsaquo;</b>[% END %]</a>
+       IF i_page == end_page_num %] [% END %]</a>
 [%   END;
      END;
 

Modified: slashjp/trunk/plugins/FireHose/templates/list;firehose;default
===================================================================
--- slashjp/trunk/plugins/FireHose/templates/list;firehose;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/FireHose/templates/list;firehose;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -153,6 +153,7 @@
 <span id="itemsreturned">
 	[% IF itemnum == 0; Slash.getData('noitems', { options => options }, 'firehose'); END %]
 </span>
+[% firehose_more %]
 [% PROCESS paginate options = options ulid = "fh-paginate" divid = "fh-pag-div" num_items = itemnum fh_page = fh_page %]
 <script type="text/javascript">
 	[% FOR opt = [ 'startdate', 'mode', 'fhfilter', 'orderdir', 'orderby', 'startdate', 'duration', 'color'] %]

Modified: slashjp/trunk/plugins/HumanConf/HumanConf.pm
===================================================================
--- slashjp/trunk/plugins/HumanConf/HumanConf.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/HumanConf/HumanConf.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -36,25 +36,26 @@
 }
 
 sub _formnameNeedsHC {
-        my($self, $formname) = @_;            
+        my($self, $formname, $options) = @_;            
+	return 1 if $options->{needs_hc};
 	my $regex = getCurrentStatic('hc_formname_regex') || '^comments$';
         return 1 if $formname =~ /$regex/;
         return 0;
 }
 
 sub createFormkeyHC {
-	my($self, $formname) = @_;
+	my($self, $formname, $options) = @_;
 
 	# Only certain formnames need human confirmation.  From any       
 	# other formname, just return 1, meaning everything is ok
 	# (no humanconf necessary).
-	return 'ok' if !$self->_formnameNeedsHC($formname);
+	return 'ok' if !$self->_formnameNeedsHC($formname, $options);
 
 	my $slashdb = getCurrentDB();
 	my $form = getCurrentForm();
 	my $user = getCurrentUser();
 	my $constants = getCurrentStatic();
-	my $formkey = $form->{formkey};
+	my $formkey = $options->{frkey} || $form->{formkey};
 	return 0 unless $formkey;
 
 	# Decide which question we're asking.
@@ -113,7 +114,7 @@
 	return 0 unless $success;
 	my $hcid = $slashdb->getLastInsertId();
 
-	$user->{state}{hcid} = $hcid; # for debugging
+	$user->{state}{hcid} = $hcid;
 	$user->{state}{hc} = 1;
 	$user->{state}{hcinvalid} = 0;
 	$user->{state}{hcquestion} = $question;
@@ -121,25 +122,34 @@
 	return 1;
 }
 
+sub updateFormkeyHCValue {
+	my($self, $hcid, $formkey) = @_;
+	my $slashdb = getCurrentDB();
+	return $slashdb->sqlUpdate('humanconf', {
+			formkey => $formkey
+		}, 'hcid=' . $slashdb->sqlQuote($hcid)
+	);
+}
+
 sub reloadFormkeyHC {
-	my($self, $formname) = @_;
+	my($self, $formname, $options) = @_;
 
 	my $user = getCurrentUser();
 
 	# Only certain formnames need human confirmation.  Other formnames
 	# won't even have HC data created for them, so there's no need to
 	# waste time hitting the DB.
-	if (!$self->_formnameNeedsHC($formname)) {
+	if (!$self->_formnameNeedsHC($formname, $options)) {
 		$user->{state}{hc} = 0;
-		return ;
+		return;
 	}
 	$user->{state}{hc} = 1;
 
 	my $slashdb = getCurrentDB();
 	my $form = getCurrentForm();
 	my $constants = getCurrentStatic();
-	my $formkey = $form->{formkey};
-	my $formkey_quoted = $slashdb->sqlQuote($form->{formkey});
+	my $formkey = $options->{frkey} || $form->{formkey};
+	my $formkey_quoted = $slashdb->sqlQuote($formkey);
 
 	my($hcid, $html, $question, $tries_left) = $slashdb->sqlSelect(
 		"hcid, html, question, tries_left",
@@ -156,22 +166,23 @@
 		$user->{state}{hcinvalid} = 1;
 		$user->{state}{hcerror} = getData('nomorechances', {}, 'humanconf');
 	}
+	return !$user->{state}{hcinvalid};
 }
 
 sub validFormkeyHC {
-	my($self, $formname) = @_;
+	my($self, $formname, $options) = @_;
 
 	# Only certain formnames need human confirmation.  Other formnames
 	# won't even have HC data created for them, so there's no need to
 	# waste time hitting the DB.
-	return 'ok' if !$self->_formnameNeedsHC($formname);
+	return 'ok' if !$self->_formnameNeedsHC($formname, $options);
 
 	my $slashdb = getCurrentDB();
 	my $form = getCurrentForm();
-	my $formkey = $form->{formkey};
+	my $formkey = $options->{frkey} || $form->{formkey};
 	return 'invalidhc' unless $formkey;
 
-	my $formkey_quoted = $slashdb->sqlQuote($form->{formkey});
+	my $formkey_quoted = $slashdb->sqlQuote($formkey);
 
 	# If this formkey is valid, and there is a corresponding humanconf
 	# entry, check that as well.  Note that if there is an hcid in the 
@@ -183,7 +194,7 @@
                 "humanconf, humanconf_pool",
                 "humanconf.formkey = $formkey_quoted
                  AND humanconf_pool.hcpid = humanconf.hcpid
-		 AND tries_left > 0"      
+		 AND tries_left > 0"
         );
         if (!$hcid) {
                 # No humanconf associated with this formkey.  Either there

Modified: slashjp/trunk/plugins/Journal/journal.pl
===================================================================
--- slashjp/trunk/plugins/Journal/journal.pl	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Journal/journal.pl	2008-04-07 00:26:30 UTC (rev 572)
@@ -593,6 +593,7 @@
 	}
 
 	my $slashdb = getCurrentDB();
+        my $event_id;
 	if ($form->{id}) {
 		my %update;
 		my $article = $journal_reader->get($form->{id});
@@ -624,6 +625,7 @@
 				url	=> "$rootdir/~" . fixparam($user->{nickname}) . "/journal/$form->{id}",
 			});
 			$update{discussion}  = $did;
+                        $event_id = $did;
 
 		# update description if changed
 		} elsif (!$form->{comments_on} && $article->{discussion} && $article->{description} ne $description) {
@@ -665,6 +667,7 @@
 				url	=> "$rootdir/~" . fixparam($user->{nickname}) . "/journal/$id",
 			});
 			$journal->set($id, { discussion => $did });
+                        $event_id = $did;
 		}
 
 		slashHook('journal_save_success', { id => $id });
@@ -705,6 +708,44 @@
 		}) if $validator;
 	}
 
+        # Add the User2 event.
+        my $events = $slashdb->sqlSelectAllHashref(
+                'eid', 'eid, date', 'user_events', "uid = " . $user->{uid} . " and code = 2");
+
+        if ((scalar keys %$events) == 5) {
+                my $eid = [sort keys %$events]->[0];
+                $slashdb->sqlDelete('user_events', "uid = " . $user->{uid} . " and code = 2 and eid = $eid");
+        }
+
+        $slashdb->sqlInsert('user_events', {
+                code  => 2,
+                uid   => $user->{uid},
+                event => $event_id,
+                -date  => 'NOW()',
+        });
+
+        my $event_blocks = $slashdb->sqlSelectAllHashref(
+                'uid', 'bid, uid, block', 'user_event_blocks', "uid = " . $user->{uid} . " and code = 2");
+
+        if (!%$event_blocks) {
+                $slashdb->sqlInsert('user_event_blocks', {
+                        code  => 2,
+                        uid   => $user->{uid},
+                        block => $event_id,
+                });
+        } else {
+                my @blocks = split(/,/, $event_blocks->{$user->{uid}}->{block});
+
+                if (scalar @blocks == 5) {
+                        @blocks = @blocks[1 .. 4];
+                }
+
+                $blocks[$#blocks + 1] = $event_id;
+                my $new_blocks = join(",", @blocks);
+
+                $slashdb->sqlUpdate('user_event_blocks', { block => $new_blocks }, "uid = " . $user->{uid} . " and code = 2");
+        }
+
 	return 0;
 }
 

Modified: slashjp/trunk/plugins/Moderation/Moderation.pm
===================================================================
--- slashjp/trunk/plugins/Moderation/Moderation.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Moderation/Moderation.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -955,7 +955,7 @@
 		}
 	}
 
-	my $removed_text = slashDisplay('undo_mod', { removed => $removed }, { Return => 1 });
+	my $removed_text = slashDisplay('undo_mod', { removed => $removed }, { Return => 1, Page => 'comments' });
 	return $removed_text;
 }
 

Modified: slashjp/trunk/plugins/ResKey/MANIFEST
===================================================================
--- slashjp/trunk/plugins/ResKey/MANIFEST	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/ResKey/MANIFEST	2008-04-07 00:26:30 UTC (rev 572)
@@ -6,8 +6,11 @@
 ResKey/Checks/AL2/AnonNoPost.pm
 ResKey/Checks/AL2/NoPost.pm
 ResKey/Checks/AL2/NoPostAnon.pm
+ResKey/Checks/AL2/NoSubmit.pm
+ResKey/Checks/AL2/Spammer.pm
 ResKey/Checks/AL2.pm
 ResKey/Checks/Duration.pm
+ResKey/Checks/HumanConf.pm
 ResKey/Checks/Moderate.pm
 ResKey/Checks/Post.pm
 ResKey/Checks/ProxyScan.pm

Modified: slashjp/trunk/plugins/ResKey/ResKey/Checks/Duration.pm
===================================================================
--- slashjp/trunk/plugins/ResKey/ResKey/Checks/Duration.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/ResKey/ResKey/Checks/Duration.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -140,9 +140,8 @@
 	my($self, $reskey_obj) = @_;
 
 	my $slashdb = getCurrentDB();
-	my $check_vars = $self->getCheckVars;
 
-	my $limit = $check_vars->{duration_uses};
+	my $limit = &duration;
 	if ($limit) {
 		my $where = $self->getWhereUserClause;
 		$where .= ' AND rkrid=' . $self->rkrid;
@@ -186,4 +185,56 @@
 }
 
 
+sub duration {
+	my($self, $reskey_obj) = @_;
+	(my $caller = (caller(1))[3]) =~ s/^.*:(\w+)$/$1/;
+
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $constants = getCurrentStatic();
+	my $form = getCurrentForm();
+	my $user = getCurrentUser();
+
+	my $check_vars = $self->getCheckVars;
+	my $limit = $constants->{reskey_timeframe};
+	my $duration = 0;
+
+	if ($caller eq 'minDurationBetweenUses') {
+		my $duration_name      = 'duration_uses';
+		my $duration_name_anon = "$duration_name-anon";
+		# this is kinda ugly ... i'd like a better way to know anon, and
+		# this constant should be a reskey constant i think -- pudge 2008.03.21
+		my $is_anon = $user->{is_anon} || $form->{postanon} || $user->{karma} < $constants->{formkey_minloggedinkarma};
+		$duration = $check_vars->{$is_anon ? $duration_name_anon : $duration_name} || 0;
+
+		# If this user has access modifiers applied, check for possible
+		# different speed limits based on those.  First match, if any,
+		# wins. (taken from MySQL::checkPostInterval()
+		my $al2_hr = $user->{srcids} ? $reader->getAL2($user->{srcids}) : { };
+		my $al2_name_used = "_none_"; # for debugging
+		for my $al2_name (sort keys %$al2_hr) {
+			my $sl_name_al2 = $is_anon
+				? "$duration_name_anon-$al2_name"
+				: "$duration_name-$al2_name";
+			if (defined $check_vars->{$sl_name_al2}) {
+				$al2_name_used = $al2_name;
+				$duration = $check_vars->{$sl_name_al2};
+				last;
+			}
+		}
+
+		if ($self->resname eq 'comments' && $is_anon) {
+			my $multiplier = $check_vars->{"$duration_name_anon-mult"};
+			if ($multiplier && $multiplier != 1) {
+				my $num_comm = $reader->getNumCommPostedAnonByIPID($user->{ipid});
+				$duration *= ($multiplier ** $num_comm);
+				$duration = int($duration + 0.5);
+			}
+		}
+	}
+
+
+	return $duration;
+}
+
+
 1;

Copied: slashjp/trunk/plugins/ResKey/ResKey/Checks/HumanConf.pm (from rev 571, slashjp/branches/upstream/current/plugins/ResKey/ResKey/Checks/HumanConf.pm)
===================================================================
--- slashjp/trunk/plugins/ResKey/ResKey/Checks/HumanConf.pm	                        (rev 0)
+++ slashjp/trunk/plugins/ResKey/ResKey/Checks/HumanConf.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -0,0 +1,105 @@
+# This code is a part of Slash, and is released under the GPL.
+# Copyright 1997-2005 by Open Source Technology Group. See README
+# and COPYING for more information, or see http://slashcode.com/.
+# $Id: HumanConf.pm,v 1.3 2008/04/02 15:15:45 pudge Exp $
+
+package Slash::ResKey::Checks::HumanConf;
+
+use warnings;
+use strict;
+
+use Slash::Utility;
+use Slash::Constants ':reskey';
+
+use base 'Slash::ResKey::Key';
+
+our($VERSION) = ' $Revision: 1.3 $ ' =~ /\$Revision:\s+([^\s]+)/;
+
+sub updateResKey {
+	my($self) = @_;
+	return unless useHumanConf($self);
+
+	my $user = getCurrentUser();
+	return unless $user;
+
+	my $hc = getObject('Slash::HumanConf');
+	return unless $hc;
+
+	$hc->updateFormkeyHCValue($user->{state}{hcid}, $self->reskey);
+}
+
+sub doCheckCreate {
+	my($self) = @_;
+
+	return RESKEY_SUCCESS unless useHumanConf($self);
+
+	my $hc = getObject('Slash::HumanConf');
+
+	if (!$hc || !$hc->createFormkeyHC($self->resname, { frkey => $self->reskey, needs_hc => 1 })) {
+		return(RESKEY_DEATH, ["HumanConf failure"]);
+	}
+
+	return RESKEY_SUCCESS;
+}
+
+sub doCheckTouch {
+	my($self) = @_;
+	return RESKEY_SUCCESS unless useHumanConf($self);
+
+	my $hc = getObject('Slash::HumanConf');
+	if (!$hc || !$hc->reloadFormkeyHC($self->resname, { frkey => $self->reskey, needs_hc => 1 })) {
+		return(RESKEY_DEATH, ['invalidhc']);
+	}
+
+	return RESKEY_SUCCESS;
+}
+
+sub doCheckUse {
+	my($self) = @_;
+	return RESKEY_SUCCESS unless useHumanConf($self);
+
+	my $hc = getObject('Slash::HumanConf');
+	return(RESKEY_DEATH, ["HumanConf failure"]) if !$hc;
+
+	# form->{hcanswer} 
+	my $return = $hc->validFormkeyHC($self->resname, { frkey => $self->reskey, needs_hc => 1 });
+	if ($return ne 'ok') {
+		$hc->reloadFormkeyHC($self->resname, { frkey => $self->reskey, needs_hc => 1 });
+		return(($return =~ /retry/ ? RESKEY_FAILURE : RESKEY_DEATH), [$return]);
+	}
+
+	return RESKEY_SUCCESS;
+}
+
+
+sub useHumanConf {
+	my($self) = @_;
+	my $constants = getCurrentStatic();
+	my $user = getCurrentUser();
+
+	return 0 if
+			# HumanConf is not running...
+		   !$constants->{plugin}{HumanConf}
+		|| !$constants->{hc};
+
+	# stolen from comments.pl
+	if ($self->resname eq 'comments') {
+		return 0 if
+				# ...or it's turned off for comments...
+			   $constants->{hc_sw_comments} == 0
+				# ...or it's turned off for logged-in users
+				# and this user is logged-in...
+			|| $constants->{hc_sw_comments} == 1
+			   && !$user->{is_anon}
+				# ...or it's turned off for logged-in users
+				# with high enough karma, and this user
+				# qualifies.
+			|| $constants->{hc_sw_comments} == 2
+			   && !$user->{is_anon}
+		   	&&  $user->{karma} > $constants->{hc_maxkarma};
+	}
+
+	return 1;
+}
+
+1;

Modified: slashjp/trunk/plugins/ResKey/ResKey/Key.pm
===================================================================
--- slashjp/trunk/plugins/ResKey/ResKey/Key.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/ResKey/ResKey/Key.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -141,7 +141,15 @@
 	if ($reskey) {
 		$reskey =~ s|[^a-zA-Z0-9_]+||g;
 	} elsif (!defined $reskey) {
-		$reskey = $opts->{nostate} ? '' : getCurrentForm('reskey');
+		$reskey = getCurrentForm('reskey') unless $opts->{nostate}; # if we already have one
+		if (!$reskey) {
+			if ($self->static) {
+				$reskey = $self->makeStaticKey;
+			} else {
+				$reskey = $self->createResKey;  
+				$self->unsaved(1); # still needs to be inserted/checked
+			}
+		}
 	}
 
 	# reskey() to set the value is called only here and from dbCreate
@@ -233,7 +241,7 @@
 	if ($name =~ /^(?:noop|success|failure|death)$/) {
 		$sub = _createStatusAccessor($name, \@_);
 
-	} elsif ($name =~ /^(?:error|reskey|debug|rkrid|resname|origtype|type|code|opts|static)$/) {
+	} elsif ($name =~ /^(?:error|reskey|debug|rkrid|resname|origtype|type|code|opts|static|unsaved)$/) {
 		$sub = _createAccessor($name, \@_);
 
 	} elsif ($name =~ /^(?:create|touch|use|createuse)$/) {
@@ -315,9 +323,7 @@
 		# create is done for use, too.
 		if ($self->type eq 'createuse') {
 			$self->type('use');
-			unless ($self->reskey) {
-				$self->dbCreate;
-			}
+			$self->dbCreate if $self->unsaved;
 		}
 
 		if ($self->type eq 'use' && !$self->static) {
@@ -413,6 +419,11 @@
 }
 
 #========================================================================
+sub createResKey {
+	return getAnonId(1, 20);
+}
+
+#========================================================================
 sub dbCreate {
 	my($self) = @_;
 	$self->_flow;
@@ -431,16 +442,16 @@
 	# bothering).  When we do pull it out, remember to lose
 	# the 'use Time::HiRes' :)
 	if ($self->static) {
-		$self->reskey($self->makeStaticKey);
 		$ok = 1;
 	} else {
-		my $reskey = '';
+		my $reskey;
+		$reskey = $self->reskey if $self->unsaved;
 		my $srcid = $self->getSrcid;
 
 		my $try_num = 1;
 		my $num_tries = 10;
 		while ($try_num < $num_tries) {
-			$reskey = getAnonId(1, 20);
+			$reskey ||= $self->createResKey;
 			my $rows = $slashdb->sqlInsert('reskeys', {
 				reskey		=> $reskey,
 				rkrid		=> $self->rkrid,
@@ -451,10 +462,14 @@
 
 			if ($rows > 0) {
 				$self->reskey($reskey);
+				$self->unsaved(0);
 				$ok = 1;
 				last;
 			}
 
+			# blank for next try
+			$reskey = '';
+			
 			# The INSERT failed because $reskey is already being
 			# used.  Presumably this would be due to a collision
 			# in the randomly-generated string, which indicates
@@ -470,7 +485,13 @@
 			Time::HiRes::sleep(rand($try_num));
 		}
 		if ($try_num > 1) {
+			$try_num--;
 			errorLog("Slash::ResKey::Key->create INSERT failed $try_num times: uid=$user->{uid} rkrid=$self->{rkrid} reskey=$reskey");
+			# XXX: this should be more modularized, bad to keep
+			# this all here, but OK to hack in for now -- pudge
+			if (defined &Slash::ResKey::Checks::HumanConf::updateResKey) {
+				Slash::ResKey::Checks::HumanConf::updateResKey($self);
+			}
 		}
 	}
 
@@ -973,6 +994,15 @@
 	return $salts;
 }
 
+sub ERROR {
+	my($self, $extra, $user) = @_;
+	$extra ||= '';
+	$user ||= getCurrentUser();
+	printf STDERR "AJAXE %d: UID:%d, extra:%s: %s (%s) (%s:%s:%s:%s:%s:%s:%s)\n",
+		$$, $user->{uid}, $extra, $self->errstr, $self->error->[0], $self->reskey,
+		$self->type, $self->resname, $self->rkrid, $self->code, $self->static,
+		$user->{srcids}{ 24 };
+}
 
 1;
 

Modified: slashjp/trunk/plugins/ResKey/example.plx
===================================================================
--- slashjp/trunk/plugins/ResKey/example.plx	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/ResKey/example.plx	2008-04-07 00:26:30 UTC (rev 572)
@@ -11,28 +11,35 @@
 for (1..1) {
 	$reskey = getObject('Slash::ResKey');
 
-	$lkey = $reskey->key('comments-moderation-ajax', {
-		debug	=> $debug
-	});
+# 	$lkey = $reskey->key('comments-moderation-ajax', {
+# 		debug	=> $debug
+# 	});
+# 
+# 	handle($lkey, 'create');
+# 	handle($lkey, 'use') for 0..19;
+# 
+# 
+# 	$rkey = $reskey->key('pollbooth', {
+# 		debug	=> $debug,
+# 		qid	=> 1
+# 	});
+# 	handle($rkey, 'createuse');
 
-	handle($lkey, 'create');
-	handle($lkey, 'use') for 0..19;
 
-
-	$rkey = $reskey->key('pollbooth', {
-		debug	=> $debug,
-		qid	=> 1
-	});
-	handle($rkey, 'createuse');
-
-
 	$rkey1 = $reskey->key('comments', {
 		debug	=> $debug
 	});
 
 	handle($rkey1, 'create');
 	handle($rkey1, 'touch');
+	chomp(my $answer = <>);
+	getCurrentForm()->{hcanswer} = $answer;
+	handle($rkey1, 'use');
 
+use Data::Dumper;
+print $rkey1;
+exit;
+
 	$rkey2 = $reskey->key('comments', {
 		debug	=> $debug,
 		reskey	=> $rkey1->reskey,

Modified: slashjp/trunk/plugins/ResKey/mysql_dump.sql
===================================================================
--- slashjp/trunk/plugins/ResKey/mysql_dump.sql	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/ResKey/mysql_dump.sql	2008-04-07 00:26:30 UTC (rev 572)
@@ -54,6 +54,7 @@
 INSERT INTO reskey_resource_checks VALUES (NULL, 1, 'all', 'Slash::ResKey::Checks::AL2::NoPost',         501);
 INSERT INTO reskey_resource_checks VALUES (NULL, 1, 'all', 'Slash::ResKey::Checks::AL2::Spammer',        531);
 INSERT INTO reskey_resource_checks VALUES (NULL, 1, 'all', 'Slash::ResKey::Checks::Duration',            601);
+INSERT INTO reskey_resource_checks VALUES (NULL, 1, 'all', 'Slash::ResKey::Checks::HumanConf',           701);
 #INSERT INTO reskey_resource_checks VALUES (NULL, 1, 'use', 'Slash::ResKey::Checks::ProxyScan',          1001);
 
 # dummy example of how to disable the Slash::ResKey::Checks::User check for "touch"
@@ -68,6 +69,10 @@
 INSERT INTO reskey_vars VALUES (1, 'duration_max-failures',  10, 'how many failures per reskey');
 INSERT INTO reskey_vars VALUES (1, 'duration_uses',          60, 'min duration (in seconds) between uses');
 INSERT INTO reskey_vars VALUES (1, 'duration_creation-use',  10, 'min duration between (in seconds) creation and use');
+INSERT INTO reskey_vars VALUES (1, 'duration_uses-anon',         300, 'duration_uses for anon');
+INSERT INTO reskey_vars VALUES (1, 'duration_uses-anon-trolla', 3600, 'duration_uses for anon + trolla AL2');
+INSERT INTO reskey_vars VALUES (1, 'duration_uses-trolla',       300, 'duration_uses for tolla AL2');
+INSERT INTO reskey_vars VALUES (1, 'duration_uses-anon-mult',    1.5, 'multiply by this amount for each comment previously posted in the past 24 hours');
 
 
 

Modified: slashjp/trunk/plugins/ResKey/templates/data;reskey;default
===================================================================
--- slashjp/trunk/plugins/ResKey/templates/data;reskey;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/ResKey/templates/data;reskey;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -75,6 +75,16 @@
 	You must wait a little bit before using this resource; please try again later.
 
 
+[%# HumanConf %]
+[% CASE 'invalidhcretry' %]
+	You failed to confirm you are a human. Please double-check the image and
+	make sure you typed in what it says.
+
+[% CASE 'invalidhc' %]
+	You failed to confirm you are a human. Please start from the beginning
+	and try again.  If you are a human, we apologize for the inconvenience.
+
+
 [%# ProxyScan %]
 [% CASE 'open proxy' %]
 	You may not post using an open proxy.

Modified: slashjp/trunk/plugins/TagDataView/TagDataView.pm
===================================================================
--- slashjp/trunk/plugins/TagDataView/TagDataView.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/TagDataView/TagDataView.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -105,7 +105,6 @@
 	my $tagsdb = getObject('Slash::Tags');
 	my $tagnames = $tagsdb->getNegativeTags;
 	$tagnames = ['nix'] unless @$tagnames;
-	#$constants->{tags_negative_tagnames} || $constants->{tags_downvote_tagname} || 'nix';
 	my $tagnameids = join ',', grep $_, map {
 		s/\s+//g;
 		$tagsdb->getTagnameidFromNameIfExists($_)

Modified: slashjp/trunk/plugins/TagModeration/TagModeration.pm
===================================================================
--- slashjp/trunk/plugins/TagModeration/TagModeration.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/TagModeration/TagModeration.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -955,7 +955,7 @@
 		}
 	}
 
-	my $removed_text = slashDisplay('undo_mod', { removed => $removed }, { Return => 1 });
+	my $removed_text = slashDisplay('undo_mod', { removed => $removed }, { Return => 1, Page => 'comments' });
 	return $removed_text;
 }
 

Modified: slashjp/trunk/plugins/Tags/Clout/Describe.pm
===================================================================
--- slashjp/trunk/plugins/Tags/Clout/Describe.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Tags/Clout/Describe.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -19,19 +19,36 @@
 	# propagate up to 50% of the weight, the second another 25%, the
 	# third another 12.5% etc.
 	$self->{cumfrac} = 0.5;
-	$self->{debug_uids} = { };
+	my $constants = getCurrentStatic();
+	$self->{debug_uids} = { map { ($_, 1) } split / /,
+		($constants->{tags_updateclouts_debuguids} || '')
+	};
 	$self->{debug} = 0;
 	1;
 }
 
 sub getUserClout {
 	my($self, $user_stub) = @_;
-	my $clout = $user_stub->{karma} >= -3
-		? log($user_stub->{karma}+10)
-		: 0;
+	my $clout;
+	my $karma = $user_stub->{karma};
+	if ($karma >= 1) {
+		# Full graduated clout for positive karma.
+		$clout = log($karma+5); # karma 1 clout 1.8 ; karma 50 clout 4.0
+	} elsif ($karma == 0) {
+		# Karma of 0 means low clout.
+		$clout = 0.2;
+	} elsif ($karma >= -2) {
+		# Mild negative karma means extremely low clout.
+		$clout = ($karma+3)*0.01;
+	} else {
+		# Significant negative karma means no clout.
+		$clout = 0;
+	}
 	$clout += 5 if $user_stub->{seclev} > 1;
 	$clout *= $user_stub->{tag_clout};
 
+	# An account created within the past 3 days has low clout.
+	# Once an account reaches 30 days old, it gets full clout.
 	my $created_at_ut;
 	if (defined($user_stub->{created_at_ut})) {
 		$created_at_ut = $user_stub->{created_at_ut};

Modified: slashjp/trunk/plugins/Tags/Clout/Vote.pm
===================================================================
--- slashjp/trunk/plugins/Tags/Clout/Vote.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Tags/Clout/Vote.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -19,16 +19,33 @@
         # propagate up to 50% of the weight, the second another 25%, the
         # third another 12.5% etc.
         $self->{cumfrac} = 0.45;
-	$self->{debug_uids} = { };
+	my $constants = getCurrentStatic();
+	$self->{debug_uids} = { map { ($_, 1) } split / /,
+		($constants->{tags_updateclouts_debuguids} || '')
+	};
 	$self->{debug} = 0;
 	1;
 }
 
 sub getUserClout {
 	my($self, $user_stub) = @_;
-	my $clout = $user_stub->{karma} >= -3
-		? log($user_stub->{karma}+10)/50
-		: 0;
+
+	my $clout;
+	my $karma = $user_stub->{karma};
+	if ($karma >= 1) {
+		# Full graduated clout for positive karma.
+		$clout = log($karma+5); # karma 1 clout 1.8 ; karma 50 clout 4.0
+	} elsif ($karma == 0) {
+		# Karma of 0 means low clout.
+		$clout = 0.3;
+	} elsif ($karma >= -2) {
+		# Mild negative karma means extremely low clout.
+		$clout = ($karma+3)*0.01;
+	} else {
+		# Significant negative karma means no clout.
+		$clout = 0;
+	}
+
 	$clout *= $user_stub->{tag_clout};
 
 	my $created_at_ut;
@@ -38,7 +55,7 @@
 		$created_at_ut = str2time( $user_stub->{created_at} ) || 0;
 	}
 	my $secs_since = time - $created_at_ut;
-	my $frac = $secs_since / (120*86400);
+	my $frac = $secs_since / (30*86400);
 	$frac = 0.1 if $frac < 0.1;
 	$frac = 1   if $frac > 1;
 	$clout *= $frac;

Modified: slashjp/trunk/plugins/Tags/PLUGIN
===================================================================
--- slashjp/trunk/plugins/Tags/PLUGIN	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Tags/PLUGIN	2008-04-07 00:26:30 UTC (rev 572)
@@ -20,4 +20,6 @@
 template=templates/tagsurldivadmin;misc;default
 template=templates/taghistory;misc;default
 template=templates/usertaghistory;users;default
+template=templates/usertagnames;users;default
 template=templates/usertags;users;default
+template=templates/usertagsforname;users;default

Modified: slashjp/trunk/plugins/Tags/Tags.pm
===================================================================
--- slashjp/trunk/plugins/Tags/Tags.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Tags/Tags.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -728,6 +728,8 @@
 	my $orderdir = uc($options->{orderdir}) eq "DESC" ? "DESC" : "ASC";
 	my $inact_clause =   $options->{include_inactive} ? '' : ' AND inactivated IS NULL';
 	my $private_clause = $options->{include_private}  ? '' : " AND private='no'";
+	my $tagname_clause = $options->{tagnameid}
+		? ' AND tags.tagnameid=' . $self->sqlQuote($options->{tagnameid}) : '';
 
 	my($table_extra, $where_extra) = ("","");
 	my $uid_q = $self->sqlQuote($uid);
@@ -752,7 +754,7 @@
 		'tags.*',
 		"tags $table_extra",
 		"tags.uid = $uid_q
-		 $inact_clause $private_clause $where_extra",
+		 $inact_clause $private_clause $tagname_clause $where_extra",
 		"ORDER BY $orderby $orderdir $limit");
 	return [ ] unless $ar && @$ar;
 	$self->dataConversionForHashrefArray($ar);
@@ -845,6 +847,7 @@
 
 sub getAllObjectsTagname {
 	my($self, $name, $options) = @_;
+	my $constants = getCurrentStatic();
 #	my $mcd = undef;
 #	my $mcdkey = undef;
 #	if (!$options->{include_private}) {
@@ -855,15 +858,24 @@
 #		my $value = $mcd->get("$mcdkey$name");
 #		return $value if defined $value;
 #	}
-	my $private_clause = ref($options) && $options->{include_private} ? '' : " AND private='no'";
+	$options = { } if !$options || !ref $options;
+	my $private_clause = $options->{include_private} ? '' : " AND private='no'";
 	my $id = $self->getTagnameidFromNameIfExists($name);
 	return [ ] if !$id;
+	# XXX make this degrade gracefully if plugins/FireHose not installed
+	my $firehose_db = getObject('Slash::FireHose');
+	my $min_pop = $options->{min_pop}
+		|| $firehose_db->getMinPopularityForColorLevel( $constants->{tags_active_mincare} || 5 );
+	# 117K rows unjoined, 7 seconds ; 10K rows unjoined, 3 seconds ; 10K rows joined, 18 seconds
 	my $hr_ar = $self->sqlSelectAllHashrefArray(
 		'*, UNIX_TIMESTAMP(created_at) AS created_at_ut',
-		'tags',
-		"tagnameid=$id AND inactivated IS NULL $private_clause",
-		'ORDER BY tagid');
+		'tags, firehose',
+		"tags.globjid=firehose.globjid AND popularity >= $min_pop
+		 AND tagnameid=$id AND inactivated IS NULL $private_clause",
+		'ORDER BY tagid DESC LIMIT 5000');
+	# 117K rows, 6 minutes ; 10K rows, 30 seconds
 	$self->addGlobjEssentialsToHashrefArray($hr_ar);
+	# 117K rows, 8 minutes ; 10K rows, 60 seconds
 	$self->addCloutsToTagArrayref($hr_ar);
 #	if ($mcd) {
 #		my $constants = getCurrentStatic();
@@ -1701,6 +1713,7 @@
 
 sub listTagnamesAll {
 	my($self, $clout_type, $options) = @_;
+	$options = { } if !$options || !ref $options;
 	my $tagname_ar;
 	if ($options->{really_all}) {
 		$tagname_ar = $self->sqlSelectColArrayref('tagname', 'tagnames',
@@ -1720,10 +1733,12 @@
 sub listTagnamesActive {
 	my($self, $options) = @_;
 	my $constants = getCurrentStatic();
-	my $max_num =         ref($options) && $options->{max_num}         || 100;
-	my $seconds =         ref($options) && $options->{seconds}         || (3600*6);
-	my $include_private = ref($options) && $options->{include_private} || 0;
-	my $min_slice =       ref($options) && $options->{min_slice}       || 0;
+	$options = { } if !$options || !ref $options;
+	my $max_num =         defined($options->{max_num})	   ? $options->{max_num} : 100;
+	my $seconds =         defined($options->{seconds})	   ? $options->{seconds} : (3600*6);
+	my $include_private = defined($options->{include_private}) ? $options->{include_private} : 0;
+	my $min_slice =       defined($options->{min_slice})	   ? $options->{min_slice} : 0;
+	my $min_clout =       defined($options->{min_clout})	   ? $options->{min_clout} : $constants->{tags_stories_top_minscore} || 0;
 	$min_slice = 0 if !$constants->{plugin}{FireHose};
 
 	# This seems like a horrendous query, but I _think_ it will run
@@ -1809,7 +1824,7 @@
 
 	# List all tags with at least a minimum clout.
 	my @tagnames = grep
-		{ $tagname_clout{$_} >= $constants->{tags_stories_top_minscore} }
+		{ $tagname_clout{$_} >= $min_clout }
 		keys %tagname_clout;
 
 	# Sort by sum of normalized clout and (opposite of) last-seen time.
@@ -1828,8 +1843,9 @@
 sub listTagnamesRecent {
 	my($self, $options) = @_;
 	my $constants = getCurrentStatic();
-	my $seconds =         ref($options) && $options->{seconds}         || (3600*6);
-	my $include_private = ref($options) && $options->{include_private} || 0;
+	$options = { } if !$options || !ref $options;
+	my $seconds =         $options->{seconds}         || (3600*6);
+	my $include_private = $options->{include_private} || 0;
 	my $private_clause = $include_private ? '' : " AND private='no'";
 	my $recent_ar = $self->sqlSelectColArrayref(
 		'DISTINCT tagnames.tagname',
@@ -2019,19 +2035,48 @@
 		'tagnameid, tagname',
 		'tagnames',
 		"tagname IN ($tagname_str)");
-	my $tagnameid_str = join(',', map { $self->sqlQuote($_) } sort keys %$tagnameid_to_name);
+	my $tagnameid_recent_ar = [ sort { $a <=> $b } keys %$tagnameid_to_name ];
 	my $tagname_to_id = { reverse %$tagnameid_to_name };
 
-	# Next, build a hash identifying which of those are new tagnames,
-	# i.e. which were used for the first time within the same recent
-	# time interval we're looking at.
-	my $tagname_firstrecent_ar = $self->sqlSelectColArrayref(
-		'tagname, MIN(created_at) AS firstuse',
-		'tagnames, tags',
-		"tagnames.tagnameid IN ($tagnameid_str)
-		 AND tagnames.tagnameid=tags.tagnameid",
-		"GROUP BY tagname
-		 HAVING firstuse >= DATE_SUB(NOW(), INTERVAL $secsback SECOND)");
+	# Heuristic to optimize the selection process.  Right now we have
+	# a list of many tagnameids (say, around 1000), most of which are
+	# not new (were used prior to the time interval in question).
+	# We eliminate those known not to be new by finding the newest
+	# tagnameid for the day prior to the time interval, then grepping
+	# out tagnameids in our list less than that.
+	#
+	# That should leave us with a much shorter list which will be
+	# processed much faster.  On a non-busy site or during a
+	# pathological case where nobody types in any new tagnames for an
+	# entire day, the worst case here is that the processing is as
+	# slow as it was prior to this optimization (up to a minute or so
+	# on Slashdot).
+	my $tagid_secsback = $self->sqlSelect('MIN(tagid)', 'tags',
+		"created_at >= DATE_SUB(NOW(), INTERVAL $secsback SECOND)")
+		|| 0;
+	my $secsback_1moreday = $secsback + 86400;
+	my $tagid_secsback_1moreday = $self->sqlSelect('MIN(tagid)', 'tags',
+		"created_at >= DATE_SUB(NOW(), INTERVAL $secsback_1moreday SECOND)")
+		|| 0;
+	my $max_previously_known_tagnameid = $self->sqlSelect('MAX(tagnameid)', 'tags',
+		"tagid BETWEEN $tagid_secsback_1moreday AND $tagid_secsback")
+		|| 0;
+	$tagnameid_recent_ar = [
+		grep { $_ > $max_previously_known_tagnameid }
+		@$tagnameid_recent_ar ]
+		if $max_previously_known_tagnameid > 0;
+	my $tagnameid_str = join(',', map { $self->sqlQuote($_) } @$tagnameid_recent_ar);
+
+	# Now do the select to find the actually-new tagnameids.
+	my $tagnameid_firstrecent_ar = $self->sqlSelectColArrayref(
+		'DISTINCT tagnameid',
+		'tags',
+		"tagid >= $tagid_secsback AND tagnameid IN ($tagnameid_str)");
+
+	# Run through the hash we built earlier to convert ids back to names.
+	my $tagname_firstrecent_ar = [
+		map { $tagnameid_to_name->{$_} }
+		@$tagnameid_firstrecent_ar ];
 	my %tagname_firstrecent = ( map { ($_, 1) } @$tagname_firstrecent_ar );
 
 	# Build a regex that will identify tagnames that begin with an
@@ -2085,7 +2130,7 @@
 		'*',
 		'tags',
 		"tagnameid IN ($tagnameids_of_interest_str)
-		 AND created_at >= DATE_SUB(NOW(), INTERVAL $secsback SECOND)");
+		 AND tagid >= $tagid_secsback");
 	$self->addCloutsToTagArrayref($tags_ar);
 	my %tagnameid_weightsum = ( );
 	my %t_globjid_weightsum = ( );
@@ -2158,11 +2203,16 @@
 
 sub showRecentTagnamesBox {
 	my($self, $options) = @_;
-	my $rtoi_ar = $self->getRecentTagnamesOfInterest($options);
+	$options ||= {};
 
-	my $text = slashDisplay('recenttagnamesbox', {
-		rtoi => $rtoi_ar,
-	}, { Return => 1 });
+	my $text = " ";
+	
+	unless ($options->{box_only}) {
+		my $rtoi_ar = $self->getRecentTagnamesOfInterest();
+		$text = slashDisplay('recenttagnamesbox', {
+			rtoi => $rtoi_ar,
+		}, { Return => 1 });
+	}
 
 	return $text if $options->{contents_only};
 

Modified: slashjp/trunk/plugins/Tags/mysql_dump.sql
===================================================================
--- slashjp/trunk/plugins/Tags/mysql_dump.sql	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Tags/mysql_dump.sql	2008-04-07 00:26:30 UTC (rev 572)
@@ -4,6 +4,7 @@
 
 INSERT INTO vars (name, value, description) VALUES ('memcached_exptime_tags', '3600', 'Seconds to cache tag data in memcached');
 INSERT INTO vars (name, value, description) VALUES ('memcached_exptime_tags_brief', '300', 'Seconds to cache tag data that only needs brief caching, in memcached');
+INSERT INTO vars (name, value, description) VALUES ('tags_active_maxshow', '200', 'Maximum number of tagged objects to display on /tags/foo');
 INSERT INTO vars (name, value, description) VALUES ('tags_active_mincare', '5', 'Minimum color slice number to "care" about tags for the Slashdot Recent Tags box (only matters if FireHose installed and if you are Slashdot)');
 INSERT INTO vars (name, value, description) VALUES ('tags_admin_private_tags', '', 'List of tags separated by | that are private for admins');
 INSERT INTO vars (name, value, description) VALUES ('tags_admin_autoaddstorytopics', '1', 'Auto-add tags for story topic keywords?');
@@ -34,10 +35,11 @@
 INSERT INTO vars (name, value, description) VALUES ('tags_urls_neg_tags', 'minus|binspam', '| separated list of tags applied which negatively affect url popularity');
 INSERT INTO vars (name, value, description) VALUES ('tags_userfrac_read', '1', 'Fraction (0.0-1.0) of user UIDs which are allowed to read tags, if tags_*_allow* is set that way');
 INSERT INTO vars (name, value, description) VALUES ('tags_userfrac_write', '0.95', 'Fraction (0.0-1.0) of user UIDs which are allowed to tag, if tags_*_allow* is set that way');
+INSERT INTO vars (name, value, description) VALUES ('tags_usershow_cutoff', '200', 'More tags than this, and instead of showing the full taglist /~user/tags will show the list of tagnames');
 INSERT INTO vars (name, value, description) VALUES ('tags_tagname_regex', '^\!?[a-z][a-z0-9/]{0,63}$', 'Regex that tag names must conform to');
+INSERT INTO vars (name, value, description) VALUES ('tags_updateclouts_debuguids', '', 'UIDs to print debug info on during clout recalculation');
 INSERT INTO vars (name, value, description) VALUES ('tags_upvote_tagname', 'nod', 'Tag for upvote');
 INSERT INTO vars (name, value, description) VALUES ('tags_downvote_tagname', 'nix', 'Tag for downvote');
-INSERT INTO vars (name, value, description) VALUES ('tags_negative_tagnames', 'nix,dupe,whocares', 'Negative tags (comma separated)');
 INSERT INTO vars (name, value, description) VALUES ('tags_viewed_tagname', 'viewed', 'Tagname to assign to stories and other items a user has viewed');
 
 INSERT INTO ajax_ops VALUES (NULL, 'tags_get_user_story', 'Slash::Tags', 'ajaxGetUserStory', 'ajax_tags_write', 'createuse');

Modified: slashjp/trunk/plugins/Tags/tags.pl
===================================================================
--- slashjp/trunk/plugins/Tags/tags.pl	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Tags/tags.pl	2008-04-07 00:26:30 UTC (rev 572)
@@ -35,11 +35,11 @@
 				: 'active';
 
 		if ($type eq 'all') {
-			$index_hr->{tagnames} = $tags_reader->listTagnamesAll('describe');
+			$index_hr->{tagnames} = $tags_reader->listTagnamesAll();
 		} elsif ($type eq 'active') {
-			$index_hr->{tagnames} = $tags_reader->listTagnamesActive('describe');
+			$index_hr->{tagnames} = $tags_reader->listTagnamesActive();
 		} else { # recent
-			$index_hr->{tagnames} = $tags_reader->listTagnamesRecent('describe');
+			$index_hr->{tagnames} = $tags_reader->listTagnamesRecent();
 		}
 
 		$title = getData('head1');
@@ -61,7 +61,7 @@
 			@objects = @$value;
 #print STDERR "tags.pl got '$mcdkey$tagname' as " . scalar(@objects) . " objects\n";
 		} else {
-			my $objects = $tags_reader->getAllObjectsTagname($tagname, 'describe');
+			my $objects = $tags_reader->getAllObjectsTagname($tagname);
 			my %globjids = ( map { ( $_->{globjid}, 1 ) } @$objects );
 			my $mintc = defined($constants->{tags_list_mintc}) ? $constants->{tags_list_mintc} : 4;
 			for my $globjid (keys %globjids) {
@@ -73,12 +73,18 @@
 				push @objects, {
 					url	=> $objs[0]{url},
 					title	=> $objs[0]{title},
-					count	=> $sum_tc,
+					clout	=> $sum_tc,
 				} if $sum_tc >= $mintc;
 			}
-			@objects = sort { $b->{count} <=> $a->{count} || ($a->{title}||'') cmp ($b->{title}||'') } @objects;
+			@objects = sort {
+				    $b->{clout}      <=>  $a->{clout}
+				|| ($a->{title}||'') cmp ($b->{title}||'')
+			} @objects;
+			my $max_display = $constants->{tags_active_maxshow} || 200;
+			if (scalar @objects > $max_display) {
+				$#objects = $max_display-1;
+			}
 			if ($mcd) {
-				my $constants = getCurrentStatic();
 				my $secs = $constants->{memcached_exptime_tags_brief} || 300;
 				$mcd->set("$mcdkey$tagname", \@objects, $secs);
 #print STDERR "tags.pl set '$mcdkey$tagname' to " . scalar(@objects) . " objects\n";

Modified: slashjp/trunk/plugins/Tags/templates/usertaghistory;users;default
===================================================================
--- slashjp/trunk/plugins/Tags/templates/usertaghistory;users;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Tags/templates/usertaghistory;users;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -24,7 +24,7 @@
 					[% IF tag.globj_type == "stories" %]
 						[% PROCESS linkStory dynamic=1 sid=tag.story.sid text=tag.story.title title=tag.story.title %]
 					[% ELSIF tag.globj_type == "urls" %]
-						<a href="[% tag.url.url %]">[% tag.url.bookmark.title || tag.url.validtitle || tag.url.initialtitle | strip_literal %]</a>
+						<a href="[% tag.url.url %]" rel="nofollow">[% tag.url.bookmark.title || tag.url.validtitle || tag.url.initialtitle | strip_literal %]</a>
 					[% ELSIF tag.globj_type == "journals" %]
 						[% nick = Slash.db.getUser(tag.journal.uid, 'nickname') %]
 						<a href="[% gSkin.rootdir %]/~[% nick | fixparam %]/journal/[% tag.journal.id %]/">[% tag.journal.description | strip_literal %]</a>

Copied: slashjp/trunk/plugins/Tags/templates/usertagnames;users;default (from rev 571, slashjp/branches/upstream/current/plugins/Tags/templates/usertagnames;users;default)
===================================================================
--- slashjp/trunk/plugins/Tags/templates/usertagnames;users;default	                        (rev 0)
+++ slashjp/trunk/plugins/Tags/templates/usertagnames;users;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -0,0 +1,43 @@
+__section__
+default
+__description__
+useredit = user whose tags are being displayed
+user_submenu = html text of user submenu, if any
+tagnames = array of tagnames
+
+This template and its API will likely change.
+
+__title__
+
+__page__
+users
+__lang__
+en_US
+__name__
+usertagnames
+__template__
+[% thisnickname = useredit.nickname | strip_literal;
+   url_nick = useredit.nickname | strip_paramattr;
+   url_base = "$gSkin.rootdir/~$url_nick/";
+   uid = useredit.uid;
+   title = "Tags by <a href=\"$url_base\">$thisnickname ($uid)</a>";
+   PROCESS user_titlebar tab_selected='tags' %]
+
+[% IF user.tags_canread_stories && tagnames.size %]
+<div><table border="0" cellpadding="0" cellspacing="0" width="100%" class="tagslist">
+[% FOREACH tagname = tagnames %]
+	<tr><td class="tagname" valign="top"><a href="[% url_base %]tags/[% tagname | strip_paramattr %]">[% tagname | strip_html %]</a></td></tr>
+[% END %]
+</table></div>
+[% ELSE %]
+	[% IF !user.tags_canread_stories %]
+	<div>You are unable to read tags at this time.</div>
+	[% ELSE %]
+	<div>No tags have been assigned.</div>
+	[% END %]
+[% END %]
+
+__seclev__
+10000
+__version__
+$Id: usertagnames;users;default,v 1.1 2008/03/27 00:10:26 jamiemccarthy Exp $

Modified: slashjp/trunk/plugins/Tags/templates/usertags;users;default
===================================================================
--- slashjp/trunk/plugins/Tags/templates/usertags;users;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/plugins/Tags/templates/usertags;users;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -18,6 +18,7 @@
 __template__
 [% thisnickname = useredit.nickname | strip_literal;
    url_nick = useredit.nickname | strip_paramattr;
+   url_base = "$gSkin.rootdir/~$url_nick/";
    uid = useredit.uid;
    title = "Tags by <a href=\"$gSkin.rootdir/~$url_nick/\">$thisnickname ($uid)</a>";
    PROCESS user_titlebar tab_selected='tags' %]
@@ -28,18 +29,18 @@
         tagname_shown = 0;
 	FOREACH tag = tags_grouped.$tagname %]
 		[% UNLESS tag.globj_type == "submissions" %]
-			<tr>[% IF !tagname_shown; tagname_shown = 1 %]<td class="tagname" valign="top">[% tagname | strip_html %]</td>[% ELSE %]<td>&nbsp;</td>[% END %]
+			<tr>[% IF !tagname_shown; tagname_shown = 1 %]<td class="tagname" valign="top"><a href="[% url_base %]tags/[% tagname | strip_paramattr %]">[% tagname | strip_literal %]</a></td>[% ELSE %]<td>&nbsp;</td>[% END %]
 			<td>&nbsp;</td>
-				<td valign="top">
-					[% IF tag.globj_type == "stories" %]
-						[% PROCESS linkStory dynamic=1 sid=tag.story.sid text=tag.story.title title=tag.story.title %]
-					[% ELSIF tag.globj_type == "urls" %]
-						<a href="[% tag.url.url %]">[% tag.url.bookmark.title || tag.url.validtitle || tag.url.initialtitle | strip_literal %]</a>
-					[% ELSIF tag.globj_type == "journals" %]
-						[% nick = Slash.db.getUser(tag.journal.uid, 'nickname') %]
-						<a href="[% gSkin.rootdir %]/~[% nick | fixparam %]/journal/[% tag.journal.id %]/">[% tag.journal.description | strip_literal %]</a>
-					[% END %]
-				</td>
+			<td valign="top">
+				[% IF tag.globj_type == "stories" %]
+					[% PROCESS linkStory dynamic=1 sid=tag.story.sid text=tag.story.title title=tag.story.title %]
+				[% ELSIF tag.globj_type == "urls" %]
+					<a href="[% tag.url.url %]" rel="nofollow">[% tag.url.bookmark.title || tag.url.validtitle || tag.url.initialtitle | strip_literal %]</a>
+				[% ELSIF tag.globj_type == "journals" %]
+					[% nick = Slash.db.getUser(tag.journal.uid, 'nickname') %]
+					<a href="[% gSkin.rootdir %]/~[% nick | fixparam %]/journal/[% tag.journal.id %]/">[% tag.journal.description | strip_literal %]</a>
+				[% END %]
+			</td>
 			</tr>
 		[% END %]
 	[% END;

Copied: slashjp/trunk/plugins/Tags/templates/usertagsforname;users;default (from rev 571, slashjp/branches/upstream/current/plugins/Tags/templates/usertagsforname;users;default)
===================================================================
--- slashjp/trunk/plugins/Tags/templates/usertagsforname;users;default	                        (rev 0)
+++ slashjp/trunk/plugins/Tags/templates/usertagsforname;users;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -0,0 +1,58 @@
+__section__
+default
+__description__
+useredit = user whose tags for a particular tagname are being displayed
+tagname = the tagname
+tags = an arrayref of hashrefs of the public tags applied by that user with that tagname
+
+This template and its API will likely change.
+
+__title__
+
+__page__
+users
+__lang__
+en_US
+__name__
+usertagsforname
+__template__
+[% thisnickname = useredit.nickname | strip_literal;
+   thistagname = tagname | strip_literal;
+   url_nick = useredit.nickname | strip_paramattr;
+   url_tagname = tagname | strip_paramattr;
+   url_base = "$gSkin.rootdir/~$url_nick/";
+   uid = useredit.uid;
+   title = "Items tagged <a href=\"$gSkin.rootdir/tags/$url_tagname\">$thistagname</a> by <a href=\"$url_base\">$thisnickname ($uid)</a>";
+   PROCESS user_titlebar tab_selected='tags' %]
+
+[% IF user.tags_canread_stories && tags.size %]
+<div><table border="0" cellpadding="0" cellspacing="0" width="100%" class="tagslist">
+[% FOREACH tag = tags %]
+	[% UNLESS tag.globj_type == "submissions" %]
+		<tr>
+		<td valign="top">
+			[% IF tag.globj_type == "stories" %]
+				[% PROCESS linkStory dynamic=1 sid=tag.story.sid text=tag.story.title title=tag.story.title %]
+			[% ELSIF tag.globj_type == "urls" %]
+				<a href="[% tag.url.url %]" rel="nofollow">[% tag.url.bookmark.title || tag.url.validtitle || tag.url.initialtitle | strip_literal %]</a>
+			[% ELSIF tag.globj_type == "journals" %]
+				[% nick = Slash.db.getUser(tag.journal.uid, 'nickname') %]
+				<a href="[% gSkin.rootdir %]/~[% nick | fixparam %]/journal/[% tag.journal.id %]/">[% tag.journal.description | strip_literal %]</a>
+			[% END %]
+		</td>
+		</tr>
+	[% END;
+END %]
+</table></div>
+[% ELSE %]
+	[% IF !user.tags_canread_stories %]
+	<div>You are unable to read tags at this time.</div>
+	[% ELSE %]
+	<div>This user hasn't publicly tagged anything "[% thistagname %]".</div>
+	[% END %]
+[% END %]
+
+__seclev__
+10000
+__version__
+$Id: usertagsforname;users;default,v 1.2 2008/04/02 15:27:13 jamiemccarthy Exp $

Modified: slashjp/trunk/sql/mysql/defaults.sql
===================================================================
--- slashjp/trunk/sql/mysql/defaults.sql	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/sql/mysql/defaults.sql	2008-04-07 00:26:30 UTC (rev 572)
@@ -833,7 +833,7 @@
 INSERT INTO vars (name, value, description) VALUES ('cur_performance_stats_lastid', '0', 'accesslogid to start searching at');
 INSERT INTO vars (name, value, description) VALUES ('cur_performance_stats_weeks', '8', 'number of weeks back to compare current stats to');
 INSERT INTO vars (name, value, description) VALUES ('currentqid',1,'The Current Question on the homepage pollbooth');
-INSERT INTO vars (name, value, description) VALUES ('cvs_tag_currentcode','T_2_5_0_197','The current cvs tag that the code was updated to - this does not affect site behavior but may be useful for your records');
+INSERT INTO vars (name, value, description) VALUES ('cvs_tag_currentcode','T_2_5_0_200','The current cvs tag that the code was updated to - this does not affect site behavior but may be useful for your records');
 INSERT INTO vars (name, value, description) VALUES ('datadir','/usr/local/slash/www.example.com','What is the root of the install for Slash');
 INSERT INTO vars (name, value, description) VALUES ('db_auto_increment_increment','1','If your master DB uses auto_increment_increment, i.e. multiple master replication, echo its value into this var');
 INSERT INTO vars (name, value, description) VALUES ('dbsparklines_disp','0','Display dbsparklines in the currentAdminUsers box?');

Modified: slashjp/trunk/sql/mysql/upgrades
===================================================================
--- slashjp/trunk/sql/mysql/upgrades	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/sql/mysql/upgrades	2008-04-07 00:26:30 UTC (rev 572)
@@ -5157,8 +5157,6 @@
 # 2008-03-12
 UPDATE vars SET value = 'T_2_5_0_197' WHERE name = 'cvs_tag_currentcode';
 
-# SLASHDOT LAST UPDATED HERE
-
 # for plugins/Ajax
 UPDATE ajax_ops set reskey_name = 'ajax_user_static', reskey_type='createuse' WHERE op='admin_signoff';
 
@@ -5190,10 +5188,66 @@
    UNIQUE tagname (tagname)
 ) TYPE=InnoDB;
 
-
 # 2008-03-19
 UPDATE vars SET value = 'T_2_5_0_198' WHERE name = 'cvs_tag_currentcode';
 
+# PUDGE LAST UPDATED HERE
+
+# for plugins/ResKey
+INSERT INTO reskey_resource_checks VALUES (NULL, 1, 'all', 'Slash::ResKey::Checks::HumanConf', 701);
+INSERT INTO reskey_vars VALUES (1, 'duration_uses-anon',         300, 'duration_uses for anon');
+INSERT INTO reskey_vars VALUES (1, 'duration_uses-anon-trolla', 3600, 'duration_uses for anon + trolla AL2');
+INSERT INTO reskey_vars VALUES (1, 'duration_uses-trolla',       300, 'duration_uses for tolla AL2');
+INSERT INTO reskey_vars VALUES (1, 'duration_uses-anon-mult',    1.5, 'multiply by this amount for each comment previously posted in the past 24 hours');
+
+# for plugins/Tags
+INSERT INTO vars (name, value, description) VALUES ('tags_usershow_cutoff', '200', 'More tags than this, and instead of showing the full taglist /~user/tags will show the list of tagnames');
+INSERT INTO vars (name, value, description) VALUES ('tags_active_maxshow', '200', 'Maximum number of tagged objects to display on /tags/foo');
+
+# for tagboxes/FHEditorPop
+INSERT INTO vars (name, value, description) VALUES ('tagbox_fheditorpop_susp_flagpop', '185', 'Admin score to artificially raise a suspicious host item to until an admin tags it');
+INSERT INTO vars (name, value, description) VALUES ('tagbox_fheditorpop_susp_maxtaggers', '7', 'Max number of unique users tagging (not voting) a low-scoring firehose item that should not raise suspicion');
+INSERT INTO vars (name, value, description) VALUES ('tagbox_fheditorpop_susp_minscore', '100', 'Min score required to not raise suspicion');
+
+# for User2
+CREATE TABLE user_events (
+   eid int UNSIGNED NOT NULL auto_increment,
+   code int UNSIGNED NOT NULL default 0,
+   uid mediumint(8) UNSIGNED NOT NULL default 0,
+   event int UNSIGNED NOT NULL default 0,
+   date datetime NOT NULL default '0000-00-00 00:00:00',
+   PRIMARY KEY eid (eid),
+   KEY uid (uid)
+) TYPE=InnoDB;
+
+CREATE TABLE user_event_blocks (
+   bid int UNSIGNED NOT NULL auto_increment,
+   uid mediumint UNSIGNED NOT NULL default 0,
+   code int UNSIGNED NOT NULL default 0,
+   block varchar(255) default NULL,
+   PRIMARY KEY bid (bid),
+   KEY uid (uid)
+) TYPE=InnoDB;
+
+CREATE TABLE user_event_types (
+   code smallint UNSIGNED NOT NULL auto_increment,
+   type varchar(32) NOT NULL default '',
+   PRIMARY KEY code (code)
+) TYPE=InnoDB;
+
+# 2008-04-02
+UPDATE vars SET value = 'T_2_5_0_199' WHERE name = 'cvs_tag_currentcode';
+
+# for tagboxes/RecentTags
+INSERT IGNORE INTO vars (name, value, description) VALUES ('tagbox_recenttags_minclout', '4', 'Minimum clout sum to count a tagname');
+
+# for plugins/Tags
+INSERT INTO vars (name, value, description) VALUES ('tags_updateclouts_debuguids', '', 'UIDs to print debug info on during clout recalculation');
+
+# 2008-04-03
+UPDATE vars SET value = 'T_2_5_0_200' WHERE name = 'cvs_tag_currentcode';
+
 # SLASHCODE/USEPERL LAST UPDATED HERE
 
-# PUDGE LAST UPDATED HERE
+# SLASHDOT LAST UPDATED HERE
+

Modified: slashjp/trunk/tagboxes/FHEditorPop/FHEditorPop.pm
===================================================================
--- slashjp/trunk/tagboxes/FHEditorPop/FHEditorPop.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/tagboxes/FHEditorPop/FHEditorPop.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -217,9 +217,30 @@
 		$popularity += $extra_pop;
 	}
 
+	# If more than a certain number of users have tagged this item with
+	# public non-voting tags and its popularity is low, there may be a
+	# bad reason why.  Boost its editor score up so that an editor sees
+	# it and can review it.
+	if ($popularity >= ($constants->{tagbox_fheditorpop_susp_minscore} || 100)) {
+		my $max_taggers = $constants->{tagbox_fheditorpop_susp_maxtaggers} || 7;
+		my $flag_pop = $constants->{tagbox_fheditorpop_susp_flagpop} || 185;
+		my %tagger_uids = ( );
+		my $tagged_by_admin = 0;
+		for my $tag_hr (@$tags_ar) {
+			next if    $tag_hr->{tagnameid} == $upvoteid
+				|| $tag_hr->{tagnameid} == $downvoteid
+				|| $tag_hr->{private};
+			my $uid = $tag_hr->{uid};
+			$tagged_by_admin = 1, last if $admins->{$uid};
+			$tagger_uids{$uid} = 1;
+		}
+		if (!$tagged_by_admin && scalar(keys %tagger_uids) > $max_taggers) {
+			$popularity = $flag_pop;
+		}
+	}
+
 	# If this is spam, its score goes way down.
-	my $firehose_db = getObject('Slash::FireHose');
-	if ($fhitem->{is_spam} eq 'yes' || $firehose_db->itemHasSpamURL($fhitem)) {
+	if ($fhitem->{is_spam} eq 'yes' || $firehose->itemHasSpamURL($fhitem)) {
 		my $max = defined($constants->{firehose_spam_score})
 			? $constants->{firehose_spam_score}
 			: -50;
@@ -227,12 +248,12 @@
 	}
 
 	# Set the corresponding firehose row to have this popularity.
-	warn "Slash::Tagbox::FHEditorPop->run bad data, fhid='$fhid' db='$firehose_db'" if !$fhid || !$firehose_db;
+	warn "Slash::Tagbox::FHEditorPop->run bad data, fhid='$fhid' db='$firehose'" if !$fhid || !$firehose;
 	if ($options->{return_only}) {
 		return $popularity;
 	}
 	main::tagboxLog(sprintf("FHEditorPop->run setting %d (%d) to %.6f", $fhid, $affected_id, $popularity));
-	$firehose_db->setFireHose($fhid, { editorpop => $popularity });
+	$firehose->setFireHose($fhid, { editorpop => $popularity });
 }
 
 { # closure

Modified: slashjp/trunk/tagboxes/FHEditorPop/mysql_dump.sql
===================================================================
--- slashjp/trunk/tagboxes/FHEditorPop/mysql_dump.sql	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/tagboxes/FHEditorPop/mysql_dump.sql	2008-04-07 00:26:30 UTC (rev 572)
@@ -2,4 +2,7 @@
 INSERT INTO tagboxes (tbid, name, affected_type, clid, weight, last_run_completed, last_tagid_logged, last_tdid_logged, last_tuid_logged) VALUES (NULL, 'FHEditorPop', 'globj', 2, 1, '2000-01-01 00:00:00', 0, 0, 0);
 INSERT IGNORE INTO vars (name, value, description) VALUES ('tagbox_fheditorpop_edmult', '10', 'Multiplier by which editor nod/nixes are weighted for editor view');
 INSERT IGNORE INTO vars (name, value, description) VALUES ('tagbox_fheditorpop_udcbasis', '1000', 'Basis for tags_udc vote clout weighting');
+INSERT IGNORE INTO vars (name, value, description) VALUES ('tagbox_fheditorpop_susp_flagpop', '185', 'Admin score to artificially raise a suspicious host item to until an admin tags it');
+INSERT IGNORE INTO vars (name, value, description) VALUES ('tagbox_fheditorpop_susp_maxtaggers', '7', 'Max number of unique users tagging (not voting) a low-scoring firehose item that should not raise suspicion');
+INSERT IGNORE INTO vars (name, value, description) VALUES ('tagbox_fheditorpop_susp_minscore', '100', 'Min score required to not raise suspicion');
 

Copied: slashjp/trunk/tagboxes/RecentTags (from rev 571, slashjp/branches/upstream/current/tagboxes/RecentTags)

Modified: slashjp/trunk/tagboxes/Top/Top.pm
===================================================================
--- slashjp/trunk/tagboxes/Top/Top.pm	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/tagboxes/Top/Top.pm	2008-04-07 00:26:30 UTC (rev 572)
@@ -205,7 +205,6 @@
 		grep { $_ }
 		map { ($_, $tags_reader->getOppositeTagname($_)) }
 		@{$tags_reader->getExcludedTags}
-#		split / /, ($constants->{tagbox_top_excludetagnames} || '')
 	);
 	# Eliminate tagnames that are just the author's name.
 	my @names = map { lc } @{ $tags_reader->getAuthorNames() };

Modified: slashjp/trunk/themes/slashcode/THEME
===================================================================
--- slashjp/trunk/themes/slashcode/THEME	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/themes/slashcode/THEME	2008-04-07 00:26:30 UTC (rev 572)
@@ -59,6 +59,10 @@
 image=htdocs/images/comm-minus.gif
 image=htdocs/images/comm-plus.gif
 image=htdocs/images/contract.gif
+image=htdocs/images/corner_w_br.png
+image=htdocs/images/corner_w_bl.png
+image=htdocs/images/corner_w_tr.png
+image=htdocs/images/corner_w_tl.png
 image=htdocs/images/cr.gif
 image=htdocs/images/ctl_grey.gif
 image=htdocs/images/ctl_red.gif
@@ -126,6 +130,7 @@
 htdoc=htdocs/robots.txt
 htdoc=htdocs/topics.pl
 htdoc=htdocs/users.pl
+htdoc=htdocs/users2.pl
 htdoc=htdocs/images/comments.js
 htdoc=htdocs/images/dumper.js
 htdoc=htdocs/badge.pl
@@ -317,9 +322,11 @@
 template=templates/undo_mod;comments;default
 template=templates/url_related;misc;default
 template=templates/userboxes;misc;default
+template=templates/userboxes2;misc;default
 template=templates/userCom;users;default
 template=templates/userFireHose;users;default
 template=templates/userInfo;users;default
+template=templates/userInfo2;users;default
 template=templates/userSub;users;default
 template=templates/user_titlebar;misc;default
 template=templates/userlogin;misc;default

Modified: slashjp/trunk/themes/slashcode/htdocs/comments.css
===================================================================
--- slashjp/trunk/themes/slashcode/htdocs/comments.css	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/themes/slashcode/htdocs/comments.css	2008-04-07 00:26:30 UTC (rev 572)
@@ -86,6 +86,7 @@
 	border: 1px solid #ddd;
 	border-bottom: none;
 	padding: .3em;
+	overflow: hidden;
 }
 
 .commentshrunk, .commentstatus, .commentload { font-weight: bold }
@@ -118,7 +119,7 @@
 	margin: 0 0 0 -.3em;
 	padding: 0 .3em;
 	text-decoration: none;
-	background: url("http://images.slashcode.com/comm-minus.gif") 0 .2em no-repeat;
+	background: url("/images/comm-minus.gif") 0 .2em no-repeat;
 	right: -1.4em;
 	z-index: 2;
 	cursor: pointer;
@@ -129,7 +130,7 @@
 	margin: 0;
 	padding: 0 .3em;
 	text-decoration: none;
-	background: url("http://images.slashcode.com/comm-plus.gif") 0 .2em no-repeat;
+	background: url("/images/comm-plus.gif") 0 .2em no-repeat;
 	right: -1.4em;
 	z-index: 1;
 	cursor: pointer;
@@ -160,7 +161,7 @@
 .comment > .oneline {
 	border: 1px solid #e5e5e5;
 	margin: 1em 0 0 0;
-	padding: .3em 0 0 0;
+	padding: 0.35em 0 0 0.25em;
 	position: relative;
 	width: 99%;
 	height: 1.3em;
@@ -168,7 +169,8 @@
 }
 
 .comment > .currcomment {
-	border: 1px dotted #666;
+	margin: 0.85em 0px -0.15em -0.15em;
+	border: .15em solid rgb(142,142,142);
 }
 
 /* lots of space between lines */
@@ -379,3 +381,53 @@
 #ccw-hide-bar-tab { border-top-width: 0; }
 .horizontal #ccw-abbr-bar-tab { border-bottom-width: 1px; border-right: none; }
 .horizontal #ccw-hide-bar-tab { border-top-width: 1px; border-left: none; }
+
+
+/* Action Buttons */
+
+.nbutton{
+    background: #666 none top left no-repeat;
+    color:#fff;
+    margin:0.25em;
+    padding: .4em 0;
+}
+.nbutton p{
+    display:inline;
+    background: transparent none top right no-repeat;
+    padding: .4em 0;
+}
+.nbutton p b{
+    display:inline;
+    background: transparent none bottom left no-repeat;
+    padding: .4em 0;
+}
+.nbutton p b a {
+    background: transparent none bottom right no-repeat !important;
+    padding: .4em 1em;
+    color:#fff !important;
+    font-weight:normal;
+    text-decoration:none;
+}
+.nbutton:hover {background-color: #444}
+
+span.current {position: absolute; left: .5em; margin-top: -.2em; z-index: 0; font-weight: bold; font-size: 197%;}
+
+.contain {
+	border: .15em solid #d5d5d5;
+	border-top: none;
+}
+
+.inline_comment {border: 2px solid #666; margin: 1em; position: relative;}
+.inline_comment input[type="text"] {width: auto}
+.inline_comment textarea {margin: 1em; width: auto}
+.inline_comment .generalbody {margin: 0; padding: 0;}
+.inline_comment .pref {position: absolute; right: 0; top: -.8em}
+.inline_comment .pref a {right: 10px; background-image: url("/images/sic_icons.png"); color: #fff; background-repeat: no-repeat; background-position: 0px -1198px;padding: 5px 10px; position:absolute; text-decoration:none; text-indent:-7000em; top: .8em; }
+.inline_comment .state { display: none }
+.inline_comment .replyto_buttons, .inlinemsg {margin: .5em 1em;}
+.inline_comment #commentlisting {padding: 0; margin: 0;}
+.warning {background: #ffd; border: 1px solid #fe6; padding: .5em; margin: .5em 0 1em 0; color: #000;}
+.inline_comment .replyto_msg {background: #fdd; border: 1px solid #fbb; padding: .5em; margin: .5em 1em 0; color: #000 }
+.inline_comment .commentload {margin: 0 1em; font-weight: bold; color: #aaa;}
+.inline_comment .contain {margin: 0; padding: 0; border: none}
+

Modified: slashjp/trunk/themes/slashcode/htdocs/comments.pl
===================================================================
--- slashjp/trunk/themes/slashcode/htdocs/comments.pl	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/themes/slashcode/htdocs/comments.pl	2008-04-07 00:26:30 UTC (rev 572)
@@ -609,6 +609,46 @@
 		);
 	}
 
+        # Add user2 event. This will eventually live in its own method.
+        if (!$user->{is_anon}) {
+                my $uid = $user->{uid};
+                my $cid = $saved_comment->{cid};
+                my $events = $slashdb->sqlSelectAllHashref(
+                        'eid', 'eid, date', 'user_events', "uid = $uid and code = 1");
+
+                if ((scalar keys %$events) == 5) {
+                        my $eid = [sort keys %$events]->[0];
+                        $slashdb->sqlDelete('user_events', "uid = $uid and code = 1 and eid = $eid");
+                }
+
+                $slashdb->sqlInsert('user_events', {
+                        code  => 1,
+                        uid   => $uid,
+                        event => $cid,
+                        -date  => 'NOW()',
+                });
+
+                my $event_blocks = $slashdb->sqlSelectAllHashref(
+                        'uid', 'bid, uid, block', 'user_event_blocks', "uid = $uid and code = 1");
+
+                if (!%$event_blocks) {
+                        $slashdb->sqlInsert('user_event_blocks', {
+                                code  => 1,
+                                uid   => $uid,
+                                block => $cid,
+                        });
+                } else {
+                        my @blocks = split(/,/, $event_blocks->{$uid}->{block});
+                        @blocks = @blocks[1 .. 4] if (scalar @blocks == 5);
+                        $blocks[$#blocks + 1] = $cid;
+                        my $new_blocks = join(",", @blocks);
+
+                        $slashdb->sqlUpdate('user_event_blocks', { block => $new_blocks }, "uid = $uid and code = 1");
+                }
+        }
+                        
+
+
 	# OK -- if we make it all the way here, and there were
 	# no errors so no header has been emitted, and we were
 	# asked to redirect to a new URL, NOW we can finally

Modified: slashjp/trunk/themes/slashcode/htdocs/images/comments.js
===================================================================
--- slashjp/trunk/themes/slashcode/htdocs/images/comments.js	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/themes/slashcode/htdocs/images/comments.js	2008-04-07 00:26:30 UTC (rev 572)
@@ -84,7 +84,7 @@
 				if (!cd.innerHTML) {
 					var cs = fetchEl('comment_sub_' + cid);
 					if (cs)
-						cs.innerHTML = '<span class="commentload">Loading ...</span>';
+						cs.innerHTML = '<span class="commentload">Loading...</span>';
 					fetch_comments.push(cid);
 					fetch_comments_pieces[cid] = 1;
 					doshort = 1;
@@ -93,7 +93,14 @@
 		}
 //		if (doshort)
 		setShortSubject(cid, mode, cl);
-		existingdiv.className = existingdiv.className.replace(/full|hidden|oneline/, mode);
+		var new_class = existingdiv.className.replace(/full|hidden|oneline/, mode);
+		if (new_class != existingdiv.className) {
+			existingdiv.className = new_class;
+			var parentdiv = fetchEl('tree_' + cid);
+			parentdiv.className = parentdiv.className.replace(' contain', '');
+			if (mode == 'full')
+				parentdiv.className = parentdiv.className + ' contain';
+		}
 		if (adTimerUrl) {
 			var addiv = fetchEl('comment_ad_' + cid);
 			if (addiv) {
@@ -262,7 +269,7 @@
 	changeThreshold(user_threshold + ''); // needs to be a string value
 }
 
-function changeT(delta) {
+function changeT(delta, skip_ht) {
 	if (!delta)
 		return void(0);
 
@@ -271,14 +278,19 @@
 	threshold = Math.min(Math.max(threshold, -1), 6);
 
 	// HT moves with T, but that is taken care of by changeThreshold()
-	changeThreshold(threshold + ''); // needs to be a string value
+	changeThreshold(threshold + '', skip_ht); // needs to be a string value
 }
 
-function changeThreshold(threshold) {
+function changeThreshold(threshold, skip_ht) {
 	var threshold_num = parseInt(threshold);
 	var t_delta = threshold_num + (user_highlightthresh - user_threshold);
-	user_highlightthresh = Math.min(Math.max(t_delta, -1), 6);
 	user_threshold = threshold_num;
+	if (skip_ht) { // don't move highlightthresh with thresh
+		if (user_threshold > user_highlightthresh)
+			user_highlightthresh = user_threshold;
+	} else {
+		user_highlightthresh = Math.min(Math.max(t_delta, -1), 6);
+	}
 
 	for (var root = 0; root < root_comments.length; root++) {
 		updateCommentTree(root_comments[root], threshold);
@@ -389,10 +401,17 @@
 	return false;
 }
 
-function vertBarClick (pid) {
-	comments_started = 1;
-	setCurrentComment(pid);
-	return selectParent(pid, 2);
+function vertBarClick() {
+	if (this && this.id) {
+		var pid = this.id.match(/_(\d+)/);
+		pid = pid[1];
+
+		if (pid) {
+			comments_started = 1;
+			setCurrentComment(pid);
+			return selectParent(pid, 2);
+		}
+	}
 }
 
 function setShortSubject(cid, mode, cl) {
@@ -569,9 +588,9 @@
 	if (!comment) { return }
 
 	var defmode = comment.className.match(/full|hidden|oneline/);
-	if (!defmode) { return }
+	if (!defmode || !defmode.length || !defmode[0]) { return }
 
-	futuredisplaymode[cid] = prehiddendisplaymode[cid] = displaymode[cid] = defmode;
+	futuredisplaymode[cid] = prehiddendisplaymode[cid] = displaymode[cid] = defmode[0];
 }
 
 function updateDisplayMode(cid, mode, newdefault) {
@@ -720,9 +739,11 @@
 		params['highlightthresh'] = user_highlightthresh;
 	}
 
-	params['cid']             = root_comment;
+// we could use this in future if we want to restrict "More" to
+// the cid's thread when possible
+//	if (root_comment)
+//		params['cid']     = root_comment;
 	params['discussion_id']   = discussion_id;
-//	params['reskey']          = reskey_static;
 
 	var abbrev = {};
 	for (var i = 0; i < cids.length; i++) {
@@ -798,7 +819,7 @@
 
 				for (var i = 0; i < update.new_cids_order.length; i++) {
 					var this_cid = update.new_cids_order[i];
-					if (!placeholder_no_update[this_cid]) {
+					if (!placeholder_no_update[this_cid] && comments[this_cid]['points'] >= -1) {
 						var mode = determineMode(this_cid);
 						updateDisplayMode(this_cid, mode, 1);
 						currents[displaymode[this_cid]]++;
@@ -936,7 +957,7 @@
 		}
 	};
 
-	shrunkdiv.innerHTML = 'Loading...';
+	shrunkdiv.innerHTML = '<span class="loading">Loading...</span>';
 	ajax_update(params, 'comment_body_' + cid, handlers);
 
 	return false;
@@ -975,8 +996,12 @@
 	replydiv.innerHTML = '';
 	if (pid) { // XXX
 		var reply_link = $dom('reply_link_' + pid);
-		reply_link.innerHTML = reply_link_html[pid];
-		reply_link_html[pid] = '';
+		// in some cases this won't exist; if not, fine, we
+		// just don't do it
+		if (reply_link || !reply_link_html[pid]) {
+			reply_link.innerHTML = reply_link_html[pid];
+			reply_link_html[pid] = '';
+		}
 	}
 }
 
@@ -987,6 +1012,7 @@
 	if (!replydiv || !reply || !preview)
 		return false;
 
+	setReplyMsg(pid, '');
 	preview.style.display = 'none';
 	reply.style.display   = 'block';
 
@@ -994,15 +1020,26 @@
 	$dom('replyto_buttons_1_' + pid).style.display = 'inline';
 }
 
+function setReplyMsg(pid, msg) {
+	var msgdiv = $('#replyto_msg_' + (pid || 0));
+	if (!msgdiv)
+		return;
+
+	msgdiv.html(msg);
+	if (msg)
+		msgdiv.show();
+	else
+		msgdiv.hide();
+}
+
 function replyPreviewOrSubmit (pid, op, handlers) {
 	var replydiv = $dom('replyto_' + pid);
 	var reply = $dom('replyto_reply_' + pid);
 	var preview = $dom('replyto_preview_' + pid);
 	var this_reskey = $dom('reskey_reply_' + pid);
 	var msgdiv = 'replyto_msg_' + pid;
-	var msg = $dom(msgdiv);
 
-	if (!replydiv || !reply || !preview || !this_reskey || !msg)
+	if (!replydiv || !reply || !preview || !this_reskey)
 		return false;
 
 	var params = {};
@@ -1015,24 +1052,26 @@
 	params['postersubj'] = $dom('postersubj_' + pid).value;
 	params['postercomment'] = $dom('postercomment_' + pid).value;
 
+	var hcanswer = $dom('hcanswer_' + pid);
+	if (hcanswer)
+		params['hcanswer'] = hcanswer.value;
+
 	var postanon = $dom('postanon_' + pid);
 	if (postanon && postanon.checked)
 		params['postanon'] = postanon.value;
 
-	msg.innerHTML = 'Loading...';
+	setReplyMsg(pid, '<span class="loading">Loading...</span>');
 	ajax_update(params, '', handlers);
 }
 
 function submitReply(pid) {
 	return replyPreviewOrSubmit(pid, 'comments_submit_reply', {
 		onComplete: function(transport) {
-			var msg = $dom('replyto_msg_' + pid);
-			msg.innerHTML = '';
+			setReplyMsg(pid, '');
 			var response = json_handler(transport);
-
 			var cid = response.cid;
 			if (response.error)
-				msg.innerHTML = response.error;
+				setReplyMsg(pid, response.error);
 			else if (cid) {
 				cancelReply(pid);
 				addComment(cid, { pid: pid, kids: [] }, '', 1);
@@ -1046,17 +1085,15 @@
 function previewReply(pid) {
 	return replyPreviewOrSubmit(pid, 'comments_preview_reply', {
 		onComplete: function(transport) {
-			var msg = $dom('replyto_msg_' + pid);
-			msg.innerHTML = '';
+			setReplyMsg(pid, '');
 			var response = json_handler(transport);
-
 			if (response.error)
-				msg.innerHTML = response.error;
+				setReplyMsg(pid, response.error);
 			if (response.html) {
-				$dom('replyto_reply_' + pid).style.display   = 'none';
-				$dom('replyto_preview_' + pid).style.display = 'block';
-				$dom('replyto_buttons_1_' + pid).style.display  = 'none';
-				$dom('replyto_buttons_2_' + pid).style.display = 'inline';
+				$('#replyto_reply_' + pid).hide();
+				$('#replyto_preview_' + pid).show();
+				$('#replyto_buttons_1_' + pid).hide();
+				$('#replyto_buttons_2_' + pid).show();
 			}
 		}
 	});
@@ -1078,15 +1115,19 @@
 	params['pid'] = pid;
 	params['sid'] = discussion_id;
 
-	replydiv.innerHTML = 'Loading...';
+	replydiv.innerHTML = '<span class="loading">Loading...</span>';
 
 	var handlers = {
 		onComplete: function(transport) {
 			json_handler(transport);
 			if (pid) { // XXX
 				var reply_link = $dom('reply_link_' + pid);
-				reply_link_html[pid] = reply_link.innerHTML;
-				reply_link.innerHTML = '<a href="#" onclick="cancelReply(' + pid + '); return false;">Cancel Reply</a>';
+				// in some cases this won't exist; if not, fine, we
+				// just don't do it
+				if (reply_link) {
+					reply_link_html[pid] = reply_link.innerHTML;
+					reply_link.innerHTML = '<p><b><a href="#" onclick="cancelReply(' + pid + '); return false;">Cancel Reply</a></b></p>';
+				}
 			}
 			$dom('postercomment_' + pid).focus();
 		}
@@ -1213,9 +1254,6 @@
 		loadAllElements('a');
 	}
 
-	if (root_comment)
-		currents['full'] += 1;
-
 	for (var i = 0; i < root_comments.length; i++) {
 		root_comments_hash[ root_comments[i] ] = 1;
 	}
@@ -1248,11 +1286,12 @@
 			document.body.onkeydown = keyHandler;
 	}
 
-	setCurrentComment(last_updated_comments[last_updated_comments_index]);
+	setCurrentComment(root_comment || last_updated_comments[last_updated_comments_index]);
 
+	//$('.contain').click(vertBarClick);
+
 	if (more_comments_num)
 		updateMoreNum(more_comments_num);
-	updateTotals();
 	enableControls();
 
 	//setTimeout('ajaxFetchComments()', 10*1000);
@@ -1487,13 +1526,13 @@
 /* code for the draggable threshold widget */
 
 function showPrefs( category ) {
-	var panel = document.getElementById("d2prefs");
+	var panel = $dom("d2prefs");
 	panel.className = category;
 	panel.style.display = "block";
 }
 
 function hidePrefs() {
-	var panel = document.getElementById("d2prefs");
+	var panel = $dom("d2prefs");
 	panel.className = "";
 	panel.style.display = "none";
 }
@@ -1590,6 +1629,7 @@
 
 YAHOO.slashdot.ThresholdWidget.prototype.setTHT = function( T, HT ) {
 	this._setTs(pinToRange(this.constraintRange, [HT, T]));
+	updateTotals();
 }
 
 YAHOO.slashdot.ThresholdWidget.prototype.getTHT = function() {
@@ -1600,6 +1640,7 @@
 	var ts = this.displayedTs.slice();
 	ts[threshold] += step;
 	this._setTs(pinToRange(this.constraintRange, ts));
+	updateTotals();
 }
 
 YAHOO.slashdot.ThresholdWidget.prototype.setCounts = function( counts ) {
@@ -1645,6 +1686,10 @@
 			this._getEl(prefix+"-count-pos").style.top = 0;
 		}
 		this._setTs();
+		// setOrientation can rewrite our totals for us, even if they
+		// are different from defaults, so let's set them again
+		// sure they are correct
+		updateTotals();
 	}
 }
 
@@ -1771,6 +1816,7 @@
 
 YAHOO.slashdot.ThresholdBar.prototype.endDrag = function( e ) {
 	this.parentWidget._onBarEndDrag(this.whichBar);
+	updateTotals();
 }
 
 YAHOO.slashdot.ThresholdBar.prototype.alignElWithMouse = function( el, iPageX, iPageY ) {
@@ -1840,19 +1886,19 @@
 		if (cid == current_cid)
 			return;
 
-		this_id  = fetchEl('comment_top_' + current_cid);
-		if (this_id)
-			this_id.className = this_id.className.replace(' newcomment', ' oldcomment');
+		this_id = $('#comment_top_' + current_cid);
+		this_id.removeClass('newcomment');
+		this_id.addClass('oldcomment');
 
-		this_id  = fetchEl('comment_' + current_cid);
-		if (this_id)
-			this_id.className = this_id.className.replace(' currcomment', '');
+		this_id = $('#comment_' + current_cid);
+		this_id.removeClass('currcomment');
+		$('.current').remove();
 	}
 
 
-	this_id  = fetchEl('comment_' + cid);
-	if (this_id)
-		this_id.className = this_id.className + ' currcomment';
+	this_id = $('#comment_' + cid);
+	this_id.addClass('currcomment');
+	this_id.before('<span class="current">&rsaquo;</span>');
 
 	current_cid = cid;
 }
@@ -1866,7 +1912,18 @@
 prev comm chrono: Q
 next comm chrono: E
 next unread comm: F
-reply: R
+reply to current comment: R
+parent of current comment: P
+history (modlog) of current comment: M
+skip to end (last): V
+skip to top (first): T
+get more comments: G
+lower top threshold: [
+raise top threshold: ]
+lower bottom threshold: ,
+raise bottom threshold: .
+toggle d2 widget: /
+hide_modal_box(): esc XXX
 */
 
 var validkeys = {
@@ -1877,7 +1934,21 @@
 	Q: { chrono : 1, prev: 1, comment: 1 },
 	E: { chrono : 1, next: 1, comment: 1 },
 	F: { thread : 1, next: 1, comment: 1, unread: 1 },
-	R: { reply  : 1 },
+
+	R: { current : 1, reply   : 1 },
+	P: { current : 1, parent  : 1 },
+	M: { current : 1, history : 1 },
+
+	G: { nav: 1, more : 1 },
+	T: { nav: 1, skip : 1, top    : 1 }, 
+	V: { nav: 1, skip : 1, bottom : 1 }, 
+
+	219 : { chr: '[', thresh : 1, top    : 1, down: 1 },
+	221 : { chr: ']', thresh : 1, top    : 1, up  : 1 },
+	188 : { chr: ',', thresh : 1, bottom : 1, down: 1 },
+	190 : { chr: '.', thresh : 1, bottom : 1, up  : 1 },
+
+	191 : { chr: '/', toggle : 1, widget : 1 }
 };
 
 validkeys['H'] = validkeys['A'];
@@ -1885,6 +1956,13 @@
 validkeys['J'] = validkeys['S'];
 validkeys['K'] = validkeys['W'];
 
+//testing
+//validkeys['1'] = validkeys['['];
+//validkeys['2'] = validkeys[']'];
+//validkeys['3'] = validkeys[','];
+//validkeys['4'] = validkeys['.'];
+
+
 function keyHandler(e, k) {
 	if (!k)
 		e = e || window.event;
@@ -1912,71 +1990,112 @@
 
 			var update = 0;
 			var next_cid = 0;
-			var key = k || String.fromCharCode(c);
+			var key = k || validkeys[c] ? c : String.fromCharCode(c);
 			var keyo = validkeys[key];
-			if (keyo && keyo['reply'] && user_is_subscriber && current_cid) { // XXX
-				replyTo(current_cid);
+			if (keyo) {
+				if (keyo['toggle']) {
+					if (keyo['widget'])
+						toggleDisplayOptions();
 
-			// forward and back between comments, in order of how they were loaded
-			} else if (keyo && keyo['chrono']) {
-				var i = last_updated_comments_index;
-				var l = last_updated_comments.length - 1;
-				update = 1;
+				// keys that rely on current comment
+				} else if (keyo['current'] && current_cid) {
+					if (keyo['reply'] && !user_is_anon) // XXX will be anon too
+						replyTo(current_cid);
 
-				if (keyo['prev']) {
-					if (i <= 0) {
-						// this did go back to end; nothing, for now
-						//i = l;
-					} else
-						i = i - 1;
-				} else if (keyo['next']) {
-					if (i >= l) {
-						if (ajaxCommentsWait())
-							return;
-						update = 2;
-						ajaxFetchComments(0, 1, '', 1);
-					} else {
-						if (!i && noSeeFirstComment(last_updated_comments[i]))
-							comments_started = 1; // only come here once
-						else
-							i = i + 1;
+					else if (keyo['history'])
+						getModalPrefs('modcommentlog', 'Moderation Comment Log', current_cid);
+
+					else if (keyo['parent']) {
+						if (current_cid && comments[current_cid] && comments[current_cid]['pid'])
+							selectParent(comments[current_cid]['pid']);
 					}
-				}
 
-				if (update == 1) {
-					last_updated_comments_index = i;
-					next_cid = last_updated_comments[i];
-				}
-			}
 
-			// forward and back between threads, and comments within each thread
-			else if (keyo && keyo['thread']) {
-				update = 1;
-				if (keyo['next']) {
-					if (noSeeFirstComment(current_cid))
-						next_cid = current_cid;
-					else {
-						if (keyo['unread'])
-							getNextUnread = 1;
-						if (keyo['comment']) {
-							next_cid = commTreeNextComm(current_cid, 0, getNextUnread);
-							if (!next_cid) { // && getNextUnread) {
-								if (ajaxCommentsWait())
-									return;
-								update = 2;
-								var highlight = 1 + collapseCurrent;
-								ajaxFetchComments(0, 1, '', highlight);
-							}
+				// misc. navigation keys
+				} else if (keyo['nav']) {
+					if (keyo['more'])
+						ajaxFetchComments(0, 1);
+
+					else if (keyo['skip']) { // XXX how to find top/bottom?
+						if (keyo['top']) {
+							next_cid = commTreeFirstComm();
+							update = 1;
+						} else if (keyo['bottom']) {
+							next_cid = commTreeLastComm();
+							update = 1;
+						}
+					}
+
+				// threshold keys keys
+				} else if (keyo['thresh']) {
+					if (keyo['top'])
+						changeHT(keyo['up'] ? 1 : -1);
+					if (keyo['bottom'])
+						changeT((keyo['up'] ? 1 : -1), 1);
+					gCommentControlWidget.setTHT(user_threshold, user_highlightthresh);
+
+
+				// forward and back between comments, in order of how they were loaded
+				} else if (keyo['chrono']) {
+					var i = last_updated_comments_index;
+					var l = last_updated_comments.length - 1;
+					update = 1;
+
+					if (keyo['prev']) {
+						if (i <= 0) {
+							// this did go back to end; nothing, for now
+							//i = l;
 						} else
-							next_cid = commTreeNextComm(comments[current_cid].pid, current_cid, getNextUnread);
+							i = i - 1;
+					} else if (keyo['next']) {
+						if (i >= l) {
+							if (ajaxCommentsWait())
+								return;
+							update = 2;
+							ajaxFetchComments(0, 1, '', 1);
+						} else {
+							if (!i && noSeeFirstComment(last_updated_comments[i]))
+								comments_started = 1; // only come here once
+							else
+								i = i + 1;
+						}
 					}
+
+					if (update == 1) {
+						last_updated_comments_index = i;
+						next_cid = last_updated_comments[i];
+					}
 				}
 
-				else if (keyo['prev'] && keyo['comment'])
-					next_cid = commTreePrevComm(current_cid);
-
-				else if (keyo['prev'])
-					next_cid = commTreePrevComm(current_cid, 1);
+				// forward and back between threads, and comments within each thread
+				else if (keyo['thread']) {
+					update = 1;
+					if (keyo['next']) {
+						if (noSeeFirstComment(current_cid))
+							next_cid = current_cid;
+						else {
+							if (keyo['unread'])
+								getNextUnread = 1;
+							if (keyo['comment']) {
+								next_cid = commTreeNextComm(current_cid, 0, getNextUnread);
+								if (!next_cid) { // && getNextUnread) {
+									if (ajaxCommentsWait())
+										return;
+									update = 2;
+									var highlight = 1 + collapseCurrent;
+									ajaxFetchComments(0, 1, '', highlight);
+								}
+							} else
+								next_cid = commTreeNextComm(comments[current_cid].pid, current_cid, getNextUnread);
+						}
+					}
+	
+					else if (keyo['prev'] && keyo['comment'])
+						next_cid = commTreePrevComm(current_cid);
+	
+					else if (keyo['prev'])
+						next_cid = commTreePrevComm(current_cid, 1);
+				}
 			}
 
 			if (update && next_cid) {
@@ -2032,6 +2151,30 @@
 	return commTreeNextComm(comments[cid].pid, cid, getNextUnread);
 }
 
+function commTreeLastComm () {
+	var this_cid = current_cid;
+	if (!current_cid)
+		this_cid = last_updated_comments[0];
+	for (;;) {
+		var new_cid = commTreeNextComm(this_cid);
+		if (!new_cid)
+			return this_cid;
+		this_cid = new_cid;
+	}
+}
+
+function commTreeFirstComm () {
+	var this_cid = current_cid;
+	if (!current_cid)
+		this_cid = last_updated_comments[0];
+	for (;;) {
+		var new_cid = commTreePrevComm(this_cid, 2);
+		if (!new_cid)
+			return this_cid;
+		this_cid = new_cid;
+	}
+}
+
 function commTreePrevComm (cid, to_parent) {
 	var root_kids = rootSort();
 	var comm = comments[cid];

Copied: slashjp/trunk/themes/slashcode/htdocs/images/corner_w_bl.png (from rev 571, slashjp/branches/upstream/current/themes/slashcode/htdocs/images/corner_w_bl.png)
===================================================================
(Binary files differ)

Copied: slashjp/trunk/themes/slashcode/htdocs/images/corner_w_br.png (from rev 571, slashjp/branches/upstream/current/themes/slashcode/htdocs/images/corner_w_br.png)
===================================================================
(Binary files differ)

Copied: slashjp/trunk/themes/slashcode/htdocs/images/corner_w_tl.png (from rev 571, slashjp/branches/upstream/current/themes/slashcode/htdocs/images/corner_w_tl.png)
===================================================================
(Binary files differ)

Copied: slashjp/trunk/themes/slashcode/htdocs/images/corner_w_tr.png (from rev 571, slashjp/branches/upstream/current/themes/slashcode/htdocs/images/corner_w_tr.png)
===================================================================
(Binary files differ)

Modified: slashjp/trunk/themes/slashcode/htdocs/slashcode_lite.css
===================================================================
--- slashjp/trunk/themes/slashcode/htdocs/slashcode_lite.css	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/themes/slashcode/htdocs/slashcode_lite.css	2008-04-07 00:26:30 UTC (rev 572)
@@ -156,3 +156,6 @@
 { background: #ddd !important;}
 
 
+div.commentTop div.title h4, .comment .oneline div.title h4 a {background: transparent !important;}
+ .comment .oneline div.title h4 a {color: #00E !important}
+

Modified: slashjp/trunk/themes/slashcode/htdocs/users.pl
===================================================================
--- slashjp/trunk/themes/slashcode/htdocs/users.pl	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/themes/slashcode/htdocs/users.pl	2008-04-07 00:26:30 UTC (rev 572)
@@ -1611,6 +1611,11 @@
 	my $constants = getCurrentStatic();
 	my $tags_reader = getObject('Slash::Tags', { db_type => 'reader' });
 
+	# XXX if $user_edit->{acl}{spammer}, either abort or put ref=nofollow in all links
+
+	my $tagname = $form->{tagname} || '';
+	$tagname = '' if !$tags_reader->tagnameSyntaxOK($tagname);
+
 	my($uid, $user_edit);
 	if ($form->{uid} || $form->{nick}) {
 		$uid = $form->{uid} || $tags_reader->getUserUID($form->{nick});
@@ -1627,11 +1632,41 @@
 		return;
 	}
 
-	my $tags_ar = $tags_reader->getGroupedTagsFromUser($user_edit->{uid});
-	slashDisplay('usertags', {
-		useredit	=> $user_edit,
-		tags_grouped	=> $tags_ar,
-	});
+	my $tagnameid = $tags_reader->getTagnameidFromNameIfExists($tagname);
+	if ($tagnameid) {
+		# Show all user's tags for one particular tagname.
+		my $tags_hr = $tags_reader->getGroupedTagsFromUser($user_edit->{uid},
+			{ tagnameid => $tagnameid });
+		my $tags_ar = $tags_hr->{$tagname} || [ ];
+		slashDisplay('usertagsforname', {
+			useredit	=> $user_edit,
+			tagname		=> $tagname,
+			tags		=> $tags_ar,
+		});
+		
+	} else {
+		my $tags_hr = $tags_reader->getGroupedTagsFromUser($user_edit->{uid});
+		my $num_tags = 0;
+		for my $tn (keys %$tags_hr) {
+			$num_tags += scalar @{ $tags_hr->{$tn} };
+		}
+		my $cutoff = $constants->{tags_usershow_cutoff} || 200;
+		if ($num_tags <= $cutoff) {
+			# Show all user's tags, grouped by tagname.
+			slashDisplay('usertags', {
+				useredit	=> $user_edit,
+				tags_grouped	=> $tags_hr,
+			});
+		} else {
+			# Show all user's tagnames, with links to show all
+			# tags for each particular tagname.
+			my $tagname_ar = [ sort keys %$tags_hr ];
+			slashDisplay('usertagnames', {
+				useredit	=> $user_edit,
+				tagnames	=> $tagname_ar,
+			});
+		}
+	}
 }
 
 #################################################################

Copied: slashjp/trunk/themes/slashcode/htdocs/users2.pl (from rev 571, slashjp/branches/upstream/current/themes/slashcode/htdocs/users2.pl)
===================================================================
--- slashjp/trunk/themes/slashcode/htdocs/users2.pl	                        (rev 0)
+++ slashjp/trunk/themes/slashcode/htdocs/users2.pl	2008-04-07 00:26:30 UTC (rev 572)
@@ -0,0 +1,3762 @@
+#!/usr/bin/perl -w
+# This code is a part of Slash, and is released under the GPL.
+# Copyright 1997-2005 by Open Source Technology Group. See README
+# and COPYING for more information, or see http://slashcode.com/.
+# $Id: users2.pl,v 1.1 2008/04/02 14:38:23 entweichen Exp $
+
+use strict;
+use Digest::MD5 'md5_hex';
+use Slash;
+use Slash::Display;
+use Slash::Utility;
+use Slash::Constants qw(:messages);
+
+#################################################################
+sub main {
+	my $slashdb = getCurrentDB();
+	my $constants = getCurrentStatic();
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $gSkin = getCurrentSkin();
+	my $formname = $0;
+	$formname =~ s/.*\/(\w+)\.pl/$1/;
+
+	my $error_flag = 0;
+	my $formkey = $form->{formkey};
+
+	my $suadmin_flag = $user->{seclev} >= 10000 ? 1 : 0 ;
+	my $postflag = $user->{state}{post};
+	my $op = lc($form->{op});
+
+        return if (!$user->{is_admin});
+
+	# savepasswd is a special case, because once it's called, you
+	# have to reload the form, and you don't want to do any checks if
+	# you've just saved.
+	my $savepass_flag = $op eq 'savepasswd' ? 1 : 0 ;
+
+	my $ops = {
+		admin		=>  {
+			function 	=> \&adminDispatch,
+			seclev		=> 10000,	# if this should be lower,
+							# then something else is
+							# broken, because it allows
+							# anyone with this seclev
+							# to change their own seclev
+			formname	=> $formname,
+			# just in case we need it for something else, we have it ...
+			checks		=> [ qw (generate_formkey) ],
+		},
+		no_user	=>  {
+			function	=> \&noUser,
+			seclev		=> 0,
+			formname	=> $formname,
+			checks		=> [],
+		},
+		userinfo	=>  {
+			function	=> \&showInfo,
+			#I made this change, not all sites are going to care. -Brian
+			seclev		=> $constants->{users_show_info_seclev},
+			formname	=> $formname,
+			checks		=> [],
+			tab_selected_1	=> 'me',
+			tab_selected_2	=> 'info',
+		},
+		userfirehose 	=> {
+			function	=> \&showFireHose,
+			seclev		=> 0,
+			formname	=> $formname,
+			checks		=> [],
+			tab_selected_1	=> 'me',
+			tab_selected_2	=> 'firehose'
+		},
+		usersubmissions	=>  {
+			function	=> \&showSubmissions,
+			#I made this change, not all sites are going to care. -Brian
+			seclev		=> $constants->{users_show_info_seclev},
+			checks		=> [],
+			tab_selected_1	=> 'me',
+		},
+		usercomments	=>  {
+			function	=> \&showComments,
+			#I made this change, not all sites are going to care. -Brian
+			seclev		=> $constants->{users_show_info_seclev},
+			checks		=> [],
+			tab_selected_1	=> 'me',
+		},
+		display	=>  {
+			function	=> \&showInfo,
+			#I made this change, not all sites are going to care. -Brian
+			seclev		=> $constants->{users_show_info_seclev},
+			formname	=> $formname,
+			checks		=> [],
+			tab_selected_1	=> 'me',
+			tab_selected_2	=> 'info',
+		},
+#		savepasswd	=> {
+#			function	=> \&savePasswd,
+#			seclev		=> 1,
+#			post		=> 1,
+#			formname	=> $formname,
+#			checks		=> [ qw (max_post_check valid_check
+#						formkey_check regen_formkey) ],
+#			tab_selected_1	=> 'preferences',
+#			tab_selected_2	=> 'password',
+#		},
+		saveuseradmin	=> {
+			function	=> \&saveUserAdmin,
+			seclev		=> 10000,
+			post		=> 1,
+			formname	=> $formname,
+			checks		=> [],
+		},
+		savehome	=> {
+			function	=> \&saveHome,
+			seclev		=> 1,
+			post		=> 1,
+			formname	=> $formname,
+			checks		=> [ qw (valid_check
+						formkey_check regen_formkey) ],
+			tab_selected_1	=> 'preferences',
+			tab_selected_2	=> 'home',
+		},
+		savecomm	=> {
+			function	=> \&saveComm,
+			seclev		=> 1,
+			post		=> 1,
+			formname	=> $formname,
+			checks		=> [ qw (valid_check
+						formkey_check regen_formkey) ],
+			tab_selected_1	=> 'preferences',
+			tab_selected_2	=> 'comments',
+		},
+		saveuser	=> {
+			function	=> \&saveUser,
+			seclev		=> 1,
+			post		=> 1,
+			formname	=> $formname,
+			checks		=> [ qw (valid_check
+						formkey_check regen_formkey) ],
+			tab_selected_1	=> 'preferences',
+			tab_selected_2	=> 'user',
+		},
+#		changepasswd	=> {
+#			function	=> \&changePasswd,
+#			seclev		=> 1,
+#			formname	=> $formname,
+#			checks		=> $savepass_flag ? [] :
+#						[ qw (generate_formkey) ],
+#			tab_selected_1	=> 'preferences',
+#			tab_selected_2	=> 'password',
+#		},
+		editmiscopts	=> {
+			function	=> \&editMiscOpts,
+			seclev		=> 1,
+			formname	=> $formname,
+			checks		=> [ ],
+			tab_selected_1	=> 'preferences',
+			tab_selected_2	=> 'misc',
+		},
+		savemiscopts	=> {
+			function	=> \&saveMiscOpts,
+			seclev		=> 1,
+			formname	=> $formname,
+			checks		=> [ ],
+			tab_selected_1	=> 'preferences',
+			tab_selected_2	=> 'misc',
+		},
+		edituser	=> {
+			function	=> \&editUser,
+			seclev		=> 1,
+			formname	=> $formname,
+			checks		=> [ qw (generate_formkey) ],
+			tab_selected_1	=> 'preferences',
+			tab_selected_2	=> 'user',
+		},
+		authoredit	=> {
+			function	=> \&editUser,
+			seclev		=> 10000,
+			formname	=> $formname,
+			checks		=> [],
+		},
+		edithome	=> {
+			function	=> \&editHome,
+			seclev		=> 1,
+			formname	=> $formname,
+			checks		=> [ qw (generate_formkey) ],
+			tab_selected_1	=> 'preferences',
+			tab_selected_2	=> 'home',
+		},
+		editcomm	=> {
+			function	=> \&editComm,
+			seclev		=> 1,
+			formname	=> $formname,
+			checks		=> [ qw (generate_formkey) ],
+			tab_selected_1	=> 'preferences',
+			tab_selected_2	=> 'comments',
+		},
+#		newuser		=> {
+#			function	=> \&newUser,
+#			seclev		=> 0,
+#			formname	=> "${formname}/nu",
+#			checks		=> [ qw (max_post_check valid_check
+#						formkey_check regen_formkey) ],
+#		},
+		newuseradmin	=> {
+			function	=> \&newUserForm,
+			seclev		=> 10000,
+			formname	=> "${formname}/nu",
+			checks		=> [],
+		},
+		previewbox	=> {
+			function	=> \&previewSlashbox,
+			seclev		=> 0,
+			formname	=> $formname,
+			checks		=> [],
+		},
+#		mailpasswd	=> {
+#			function	=> \&mailPasswd,
+#			seclev		=> 0,
+#			formname	=> "${formname}/mp",
+#			checks		=> [ qw (max_post_check valid_check
+#						interval_check formkey_check ) ],
+#			tab_selected_1	=> 'preferences',
+#			tab_selected_2	=> 'password',
+#		},
+		validateuser	=> {
+			function	=> \&validateUser,
+			seclev		=> 1,
+			formname	=> $formname,
+			checks		=> ['regen_formkey'],
+		},
+		showtags => {
+			function	=> \&showTags,
+			seclev		=> 1,
+			formname	=> $formname,
+			checks		=> [],
+			tab_selected	=> 'tags',
+		},
+		showbookmarks => {
+			function	=> \&showBookmarks,
+			seclev		=> 0,
+			formname	=> $formname,
+			checks		=> [],
+			tab_selected	=> 'bookmarks',
+		},
+		edittags => {
+			function	=> \&editTags,
+			seclev		=> 1,
+			formname	=> $formname,
+			checks		=> [],
+			tab_selected	=> 'tags',
+		},
+		savetags => {
+			function	=> \&saveTags,
+			seclev		=> 1,
+			formname	=> $formname,
+			checks		=> [],
+			tab_selected	=> 'tags',
+		},
+#		userclose	=>  {
+#			function	=> \&displayForm,
+#			seclev		=> 0,
+#			formname	=> $formname,
+#			checks		=> [],
+#		},
+#		newuserform	=> {
+#			function	=> \&displayForm,
+#			seclev		=> 0,
+#			formname	=> "${formname}/nu",
+#			checks		=> [ qw (max_post_check
+#						generate_formkey) ],
+#		},
+#		mailpasswdform 	=> {
+#			function	=> \&displayForm,
+#			seclev		=> 0,
+#			formname	=> "${formname}/mp",
+#			checks		=> [ qw (max_post_check
+#						generate_formkey) ],
+#			tab_selected_1	=> 'preferences',
+#			tab_selected_2	=> 'password',
+#		},
+		displayform	=> {
+			function	=> \&displayForm,
+			seclev		=> 0,
+			formname	=> $formname,
+			checks		=> [ qw (generate_formkey) ],
+			tab_selected_1	=> 'me',
+		},
+		listreadonly => {
+			function	=> \&listReadOnly,
+			seclev		=> 100,
+			formname	=> $formname,
+			checks		=> [],
+			adminmenu	=> 'security',
+			tab_selected	=> 'readonly',
+		},
+		listbanned => {
+			function	=> \&listBanned,
+			seclev		=> 100,
+			formname	=> $formname,
+			checks		=> [],
+			adminmenu	=> 'security',
+			tab_selected	=> 'banned',
+		},
+		topabusers 	=> {
+			function	=> \&topAbusers,
+			seclev		=> 100,
+			formname	=> $formname,
+			checks		=> [],
+			adminmenu	=> 'security',
+			tab_selected	=> 'abusers',
+		},
+		listabuses 	=> {
+			function	=> \&listAbuses,
+			seclev		=> 100,
+			formname	=> $formname,
+			checks		=> [],
+		},
+		force_acct_verify => {
+			function	=> \&forceAccountVerify,
+			seclev		=> 100,
+			post		=> 1,
+			formname 	=> $formname,
+			checks		=> []
+		}	
+	
+	} ;
+
+	# Note this is NOT the default op.  "userlogin" or "userinfo" is
+	# the default op, and it's set either 5 lines down or about 100
+	# lines down, depending.  Yes, that's dumb.  Yes, we should
+	# change it.  It would require tracing through a fair bit of logic
+	# though and I don't have the time right now. - Jamie
+	$ops->{default} = $ops->{displayform};
+	for (qw(newuser newuserform mailpasswd mailpasswdform changepasswd savepasswd userlogin userclose)) {
+		$ops->{$_} = $ops->{default};
+	}
+
+	my $errornote = "";
+	if ($form->{op} && ! defined $ops->{$op}) {
+		$errornote .= getError('bad_op', { op => $form->{op}}, 0, 1);
+		$op = $user->{is_anon} ? 'userlogin' : 'userinfo'; 
+	}
+
+	if ($op eq 'userlogin' && ! $user->{is_anon}) {
+		redirect(cleanRedirectUrl($form->{returnto} || ''));
+		return;
+
+	# this will only redirect if it is a section-based rootdir with
+	# its rootdir different from real_rootdir
+	} elsif ($op eq 'userclose' && $gSkin->{rootdir} ne $constants->{real_rootdir}) {
+		redirect($constants->{real_rootdir} . '/login.pl?op=userclose');
+		return;
+
+	} elsif ($op =~ /^(?:newuser|newuserform|mailpasswd|mailpasswdform|changepasswd|savepasswd|userlogin|userclose|displayform)$/) {
+		my $op = $form->{op};
+		$op = 'changeprefs' if $op eq 'changepasswd';
+		$op = 'saveprefs'   if $op eq 'savepasswd';
+		redirect($constants->{real_rootdir} . '/login.pl?op=' . $op);
+		return;
+
+	# never get here now
+	} elsif ($op eq 'savepasswd') {
+		my $error_flag = 0;
+		if ($user->{seclev} < 100) {
+			for my $check (@{$ops->{savepasswd}{checks}}) {
+				# the only way to save the error message is to pass by ref
+				# $errornote and add the message to note (you can't print
+				# it out before header is called)
+				$error_flag = formkeyHandler($check, $formname, $formkey, \$errornote);
+				last if $error_flag;
+			}
+		}
+
+		if (! $error_flag) {
+			$error_flag = savePasswd({ noteref => \$errornote }) ;
+		}
+		# change op to edituser and let fall through;
+		# we need to have savePasswd set the cookie before
+		# header() is called -- pudge
+		if ($user->{seclev} < 100 && ! $error_flag) {
+			$slashdb->updateFormkey($formkey, length($ENV{QUERY_STRING}));
+		}
+		$op = $error_flag ? 'changepasswd' : 'userinfo';
+		$form->{userfield} = $user->{uid};
+	}
+
+	# Figure out what the op really is.
+	$op = 'userinfo' if (! $form->{op} && ($form->{uid} || $form->{nick}));
+	$op ||= $user->{is_anon} ? 'userlogin' : 'userinfo';
+	if ($user->{is_anon} && ( ($ops->{$op}{seclev} > 0) || ($op =~ /^newuserform|mailpasswdform|displayform$/) )) {
+		redirect($constants->{real_rootdir} . '/login.pl');
+		return;
+	} elsif ($user->{seclev} < $ops->{$op}{seclev}) {
+		$op = 'userinfo';
+	}
+	if ($ops->{$op}{post} && !$postflag) {
+		$op = $user->{is_anon} ? 'default' : 'userinfo';
+	}
+
+	# Print the header and very top stuff on the page.  We have
+	# three ops that (may) end up routing into showInfo(), which
+	# needs to do some stuff before it calls header(), so for
+	# those three, don't bother.
+	my $header;
+	if ($op !~ /^(userinfo|display|saveuseradmin|admin|userfirehose$)/) {
+		my $data = {
+			adminmenu => $ops->{$op}{adminmenu} || 'admin',
+			tab_selected => $ops->{$op}{tab_selected},
+		};
+		header(getMessage('user_header'), '', $data) or return;
+		# This is a hardcoded position, bad idea and should be fixed -Brian
+		# Yeah, we should pull this into a template somewhere...
+		print getMessage('note', { note => $errornote }) if defined $errornote;
+		$header = 1;
+	}
+
+	if ($constants->{admin_formkeys} || $user->{seclev} < 100) {
+
+		my $done = 0;
+		$done = 1 if $op eq 'savepasswd'; # special case
+		$formname = $ops->{$op}{formname};
+
+		# No need for HumanConf if the constant for it is not
+		# switched on, or if the user's karma is high enough
+		# to get out of it.  (But for "newuserform," the current
+		# user's karma doesn't get them out of having to prove
+		# they're a human for creating a *new* user.)
+		my $options = {};
+		if (	   !$constants->{plugin}{HumanConf}
+			|| !$constants->{hc}
+			|| !$constants->{hc_sw_newuser}
+			   	&& ($formname eq 'users/nu' || $op eq 'newuserform')
+			|| !$constants->{hc_sw_mailpasswd}
+			   	&& ($formname eq 'users/mp' || $op eq 'mailpasswdform')
+			|| $user->{karma} > $constants->{hc_maxkarma}
+				&& !$user->{is_anon}
+				&& !($op eq 'newuser' || $op eq 'newuserform')
+		) {
+			$options->{no_hc} = 1;
+		}
+
+		DO_CHECKS: while (!$done) {
+			for my $check (@{$ops->{$op}{checks}}) {
+				$ops->{$op}{update_formkey} = 1 if $check eq 'formkey_check';
+				$error_flag = formkeyHandler($check, $formname, $formkey,
+					undef, $options);
+				if ($error_flag == -1) {
+					# Special error:  HumanConf failed.  Go
+					# back to the previous op, start over.
+					if ($op =~ /^(newuser|mailpasswd)$/) {
+						$op .= "form";
+						$error_flag = 0;
+						next DO_CHECKS;
+					}
+				} elsif ($error_flag) {
+					$done = 1;
+					last;
+				}
+			}
+			$done = 1;
+		}
+
+		if (!$error_flag && !$options->{no_hc}) {
+			my $hc = getObject("Slash::HumanConf");
+			$hc->reloadFormkeyHC($formname) if $hc;
+		}
+
+	}
+
+	errorLog("users.pl error_flag '$error_flag'") if $error_flag;
+
+	# call the method
+	my $retval;
+	$retval = $ops->{$op}{function}->({
+		op		=> $op,
+		tab_selected_1	=> $ops->{$op}{tab_selected_1} || "",
+		note		=> $errornote,
+	}) if !$error_flag;
+
+	return if !$retval;
+
+	if ($op eq 'mailpasswd' && $retval) {
+		$ops->{$op}{update_formkey} = 0;
+	}
+
+	if ($ops->{$op}{update_formkey} && $user->{seclev} < 100 && ! $error_flag) {
+		# successful save action, no formkey errors, update existing formkey
+		# why assign to an unused variable? -- pudge
+		my $updated = $slashdb->updateFormkey($formkey, length($ENV{QUERY_STRING}));
+	}
+	# if there were legit error levels returned from the save methods
+	# I would have it clear the formkey in case of an error, but that
+	# needs to be sorted out later
+	# else { resetFormkey($formkey); }
+
+	writeLog($user->{nickname});
+	footer();
+}
+
+#################################################################
+sub checkList {
+	my($string, $len) = @_;
+	my $constants = getCurrentStatic();
+
+	$string =~ s/[^\w,-]//g;
+	my @items = grep { $_ } split /,/, $string;
+	$string = join ",", @items;
+
+	$len ||= $constants->{checklist_length} || 255;
+	if (length($string) > $len) {
+		print getError('checklist_err');
+		$string = substr($string, 0, $len);
+		$string =~ s/,?\w*$//g;
+	} elsif (length($string) < 1) {
+		$string = '';
+	}
+
+	return $string;
+}
+
+#################################################################
+sub previewSlashbox {
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $constants = getCurrentStatic();
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+
+	my $block = $reader->getBlock($form->{bid}, ['title', 'block', 'url']);
+	my $is_editable = $user->{seclev} >= 1000;
+
+	my $title = getTitle('previewslashbox_title', { blocktitle => $block->{title} });
+	slashDisplay('previewSlashbox', {
+		width		=> '100%',
+		title		=> $title,
+		block 		=> $block,
+		is_editable	=> $is_editable,
+	});
+
+	print portalbox($constants->{fancyboxwidth}, $block->{title},
+		$block->{block}, '', $block->{url});
+}
+
+#################################################################
+sub newUserForm {
+	my $user = getCurrentUser();
+	my $suadmin_flag = $user->{seclev} >= 10000;
+	my $title = getTitle('newUserForm_title');
+
+	slashDisplay('newUserForm', {
+		title 		=> $title, 
+		suadmin_flag 	=> $suadmin_flag,
+	});
+}
+
+#################################################################
+sub newUser {
+	my $slashdb = getCurrentDB();
+	my $form = getCurrentForm();
+	my $user = getCurrentUser();
+	my $constants = getCurrentStatic();
+	
+	my $plugins = $slashdb->getDescriptions('plugins');
+	my $title;
+	my $suadmin_flag = $user->{seclev} >= 10000 ? 1 : 0;
+
+	# Check if User Exists
+	$form->{newusernick} = nickFix($form->{newusernick});
+	my $matchname = nick2matchname($form->{newusernick});
+
+	if (!$form->{email} || $form->{email} !~ /\@/) {
+		print getError('email_invalid', 0, 1);
+		return;
+	} elsif ($form->{email} ne $form->{email2}) {
+		print getError('email_do_not_match', 0, 1);
+		return;	
+	} elsif ($slashdb->existsEmail($form->{email})) {
+		print getError('emailexists_err', 0, 1);
+		return;
+	} elsif ($matchname ne '' && $form->{newusernick} ne '') {
+		if ($constants->{newuser_portscan}) {
+			my $is_trusted = $slashdb->checkAL2($user->{srcids}, 'trusted');
+			if (!$is_trusted) {
+				my $is_proxy = $slashdb->checkForOpenProxy($user->{hostip});
+				if ($is_proxy) {
+					print getError('new user open proxy', {
+						unencoded_ip	=> $ENV{REMOTE_ADDR},
+						port		=> $is_proxy,
+					});
+					return;
+				}
+			}
+		}
+		my $uid;
+		my $rootdir = getCurrentSkin('rootdir');
+
+		$uid = $slashdb->createUser(
+			$matchname, $form->{email}, $form->{newusernick}
+		);
+		if ($uid) {
+			my $data = {};
+			getOtherUserParams($data);
+
+			for (qw(tzcode)) {
+				$data->{$_} = $form->{$_} if defined $form->{$_};
+			}
+			$data->{creation_ipid} = $user->{ipid};
+
+			$slashdb->setUser($uid, $data) if keys %$data;
+			$title = getTitle('newUser_title');
+
+			$form->{pubkey} = $plugins->{'Pubkey'} ?
+				strip_nohtml($form->{pubkey}, 1) : '';
+			print getMessage('newuser_msg', { 
+				suadmin_flag	=> $suadmin_flag, 
+				title		=> $title, 
+				uid		=> $uid
+			});
+
+			if ($form->{newsletter} || $form->{comment_reply} || $form->{headlines}) {
+				my $messages  = getObject('Slash::Messages');
+				my %params;
+				$params{MSG_CODE_COMMENT_REPLY()} = MSG_MODE_EMAIL()
+					if $form->{comment_reply};
+				$params{MSG_CODE_NEWSLETTER()}  = MSG_MODE_EMAIL()
+					if $form->{newsletter};
+				$params{MSG_CODE_HEADLINES()}   = MSG_MODE_EMAIL()
+					if $form->{headlines};
+				$messages->setPrefs($uid, \%params);
+			}
+
+			mailPasswd({ uid => $uid });
+
+			return;
+		} else {
+			$slashdb->resetFormkey($form->{formkey});	
+			print getError('duplicate_user', { 
+				nick => $form->{newusernick},
+			});
+			return;
+		}
+
+	} else {
+		print getError('duplicate_user', { 
+			nick => $form->{newusernick},
+		});
+		return;
+	}
+}
+
+#################################################################
+sub mailPasswd {
+	my($hr) = @_;
+	my $user = getCurrentUser();
+	my $constants = getCurrentStatic();
+	
+	my $uid = $hr->{uid} || 0;
+
+	my $slashdb = getCurrentDB();
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $form = getCurrentForm();
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $hr->{tab_selected_1} || "",
+	});
+
+	if (! $uid) {
+		if ($form->{unickname} =~ /\@/) {
+			$uid = $slashdb->getUserEmail($form->{unickname});
+
+		} elsif ($form->{unickname} =~ /^\d+$/) {
+			my $tmpuser = $slashdb->getUser($form->{unickname}, ['uid']);
+			$uid = $tmpuser->{uid};
+
+		} else {
+			$uid = $slashdb->getUserUID($form->{unickname});
+		}
+	}
+
+	my $user_edit;
+	my $err_name = '';
+	my $err_opts = {};
+	if (!$uid || isAnon($uid)) {
+		$err_name = 'mailpasswd_notmailed_err';
+	}
+	if (!$err_name) {
+		# Check permissions of _this_ user, not the target
+		# user, to determine whether this IP is OK'd to
+		# send the mail to the target user.
+		# XXXSRCID This should check a separate field like
+		# 'openproxy' instead of piggybacking off of 'nopost'
+		my $srcids_to_check = $user->{srcids};
+		$err_name = 'mailpasswd_readonly_err'
+			if $reader->checkAL2($srcids_to_check, 'nopost');
+	}
+	if (!$err_name) {
+		$err_name = 'mailpasswd_toooften_err'
+			if $slashdb->checkMaxMailPasswords($user_edit);
+	}
+	
+	if (!$err_name) {
+		if ($constants->{mailpasswd_portscan}) {
+			my $is_trusted = $slashdb->checkAL2($user->{srcids}, 'trusted');
+			if (!$is_trusted) {
+				my $is_proxy = $slashdb->checkForOpenProxy($user->{hostip});
+				if ($is_proxy) {
+					$err_name = 'mailpasswd open proxy';
+					$err_opts = { unencoded_ip => $ENV{REMOTE_ADDR}, port => $is_proxy }; 
+				}
+			}
+
+		}
+	}
+
+	if ($err_name) {
+		print getError($err_name, $err_opts);
+		$slashdb->resetFormkey($form->{formkey});	
+		$form->{op} = 'mailpasswdform';
+		displayForm();
+		return(1);
+	}
+
+	my $newpasswd = $slashdb->getNewPasswd($uid);
+	my $tempnick = $user_edit->{nickname};
+
+	my $emailtitle = getTitle('mailPassword_email_title', {
+		nickname	=> $user_edit->{nickname}
+	}, 1);
+
+	# Pull out some data passed in with the request.  Only the IP
+	# number is actually trustworthy, the others could be forged.
+	# Note that we strip the forgeable ones to make sure there
+	# aren't any "<>" chars which could fool a stupid mail client
+	# into parsing a plaintext email as HTML.
+	my $r = Apache->request;
+	my $remote_ip = $r->connection->remote_ip;
+	my $xff = $r->header_in('X-Forwarded-For') || '';
+	$xff =~ s/\s+/ /g;
+	$xff = substr(strip_notags($xff), 0, 20);
+	my $ua = $r->header_in('User-Agent') || '';
+	$ua =~ s/\s+/ /g;
+	$ua = substr(strip_attribute($ua), 0, 60);
+
+	my $msg = getMessage('mailpasswd_msg', {
+		newpasswd	=> $newpasswd,
+		tempnick	=> $tempnick,
+		remote_ip	=> $remote_ip,
+		x_forwarded_for	=> $xff,
+		user_agent	=> $ua,
+	}, 1);
+
+	doEmail($uid, $emailtitle, $msg) if $user_edit->{nickname};
+	print getMessage('mailpasswd_mailed_msg', { name => $user_edit->{nickname} });
+	$slashdb->setUserMailPasswd($user_edit);
+}
+
+
+#################################################################
+sub showSubmissions {
+	my($hr) = @_;
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $form = getCurrentForm();
+	my $constants = getCurrentStatic();
+	my $user = getCurrentUser();
+	my($uid, $nickname);
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $hr->{tab_selected_1} || "",
+	});
+
+	if ($form->{uid} or $form->{nick}) {
+		$uid		= $form->{uid} || $reader->getUserUID($form->{nick});
+		$nickname	= $reader->getUser($uid, 'nickname');
+	} else {
+		$nickname	= $user->{nickname};
+		$uid		= $user->{uid};
+	}
+
+	my $storycount = $reader->countStoriesBySubmitter($uid);
+	my $stories = $reader->getStoriesBySubmitter(
+		$uid,
+		$constants->{user_submitter_display_default}
+	) unless !$storycount;
+
+	slashDisplay('userSub', {
+		nick			=> $nickname,
+		uid			=> $uid,
+		nickmatch_flag		=> ($user->{uid} == $uid ? 1 : 0),
+		stories 		=> $stories,
+		storycount 		=> $storycount,
+	});
+}
+
+#################################################################
+sub showComments {
+	my($hr) = @_;
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $form = getCurrentForm();
+	my $constants = getCurrentStatic();
+	my $user = getCurrentUser();
+	my $commentstruct = [];
+	my($uid, $nickname);
+
+	my $user_edit;
+	if ($form->{uid} || $form->{nick}) {
+		$uid = $form->{uid} || $reader->getUserUID($form->{nick});
+		$user_edit = $reader->getUser($uid);
+	} else {
+		$uid = $user->{uid};
+		$user_edit = $user;
+	}
+	$nickname = $user_edit->{nickname};
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $user_edit->{uid} == $user->{uid} ? 'me' : 'otheruser',
+	});
+
+	my $min_comment = $form->{min_comment} || 0;
+	$min_comment = 0 unless $user->{seclev} > $constants->{comments_more_seclev}
+		|| $constants->{comments_more_seclev} == 2 && $user->{is_subscriber};
+	my $comments_wanted = $user->{show_comments_num}
+		|| $constants->{user_comment_display_default};
+	my $commentcount = $reader->countCommentsByUID($uid);
+	my $comments = $reader->getCommentsByUID(
+		$uid, $comments_wanted, $min_comment
+	) if $commentcount;
+
+	if (ref($comments) eq 'ARRAY') {
+		my $kinds = $reader->getDescriptions('discussion_kinds');
+		for my $comment (@$comments) {
+			# This works since $sid is numeric.
+			$comment->{replies} = $reader->countCommentsBySidPid($comment->{sid}, $comment->{cid});
+
+			# This is ok, since with all luck we will not be hitting the DB
+			# ...however, the "sid" parameter here must be the string
+			# based SID from either the "stories" table or from
+			# pollquestions.
+			my $discussion = $reader->getDiscussion($comment->{sid});
+
+			if ($kinds->{ $discussion->{dkid} } =~ /^journal(?:-story)?$/) {
+				$comment->{type} = 'journal';
+			} elsif ($kinds->{ $discussion->{dkid} } eq 'poll') {
+				$comment->{type} = 'poll';
+			} else {
+				$comment->{type} = 'story';
+			}
+			$comment->{disc_title}	= $discussion->{title};
+			$comment->{url}	= $discussion->{url};
+		}
+	}
+
+	my $mod_reader = getObject("Slash::$constants->{m1_pluginname}", { db_type => 'reader' });
+	slashDisplay('userCom', {
+		nick			=> $nickname,
+		useredit		=> $user_edit,
+		nickmatch_flag		=> ($user->{uid} == $uid ? 1 : 0),
+		commentstruct		=> $comments,
+		commentcount		=> $commentcount,
+		min_comment		=> $min_comment,
+		reasons			=> $mod_reader->getReasons(),
+		karma_flag		=> 0,
+		admin_flag		=> $user->{is_admin},
+	});
+}
+
+sub noUser {
+	print getData("no_user");
+}
+
+sub showFireHose {
+	my($hr) = @_;
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+
+	my $uid = $form->{uid} || $user->{uid};
+	my $user_edit = $reader->getUser($uid);
+	
+	$user->{state}{firehose_page} = "user";
+	$user->{state}{firehose_user_uid} = $uid;
+
+	my $firehose = getObject("Slash::FireHose");
+	header(getMessage('userfirehose_header', { useredit => $user_edit })) or return;
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $user_edit->{uid} == $user->{uid} ? 'me' : 'otheruser',
+	});
+	
+	$form->{mode} = "full";
+	$form->{color} = "black";
+	$form->{orderby} = "createtime";
+	$form->{orderdidr} = "DESC";
+	$form->{skipmenu} = 1;
+	$form->{duration} = -1;
+	$form->{fhfilter} = "\"user:$user_edit->{nickname}\"";
+	$form->{pause} = 1;
+
+	my $fhbox = $firehose->listView({ fh_page => 'users.pl'});
+	slashDisplay("userFireHose", { firehosebox => $fhbox, uid => $uid, useredit => $user_edit });
+}
+
+#################################################################
+# arhgghgh. I love torture. I love pain. This subroutine satisfies
+# these needs of mine
+sub showInfo {
+	my($hr) = @_;
+	my $id = $hr->{uid} || 0;
+
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $slashdb = getCurrentDB();
+	my $form = getCurrentForm();
+	my $constants = getCurrentStatic();
+	my $user = getCurrentUser();
+
+
+	my $admin_flag = ($user->{is_admin}) ? 1 : 0;
+	my($title, $admin_block, $fieldkey) = ('', '', '');
+	my $comments = undef;
+	my $commentcount = 0;
+	my $commentcount_time = 0;
+	my $commentstruct = [];
+	my $requested_user = {};
+	my $time_period = $constants->{admin_comment_display_days} || 30;
+	my $cid_for_time_period = $reader->getVar("min_cid_last_$time_period\_days",'value', 1) || 0;
+	my $admin_time_period_limit = $constants->{admin_daysback_commentlimit} || 100;
+	my $admin_non_time_limit    = $constants->{admin_comment_subsequent_pagesize} || 24;
+	
+	my($points, $nickmatch_flag, $uid, $nick);
+	my($mod_flag, $karma_flag, $n) = (0, 0, 0);
+
+	if ($admin_flag
+		&& (defined($form->{show_m2s}) || defined($form->{show_m1s}) || defined($form->{m2_listing})))
+	 {
+		my $update_hr = {};
+		$update_hr->{mod_with_comm} = $form->{show_m1s}
+			if defined $form->{show_m1s};
+		$update_hr->{m2_with_mod} =	($constants->{m2} ? $form->{show_m2s} : undef)
+			if defined $form->{show_m2s};
+		$update_hr->{show_m2_listing} =	($constants->{m2} ? $form->{m2_listing} : undef)
+			if defined $form->{m2_listing};
+		$slashdb->setUser($user->{uid}, $update_hr);
+	}
+
+	if (!$id && !$form->{userfield}) {
+		if ($form->{uid} && ! $id) {
+			$fieldkey = 'uid';
+			($uid, $id) = ($form->{uid}, $form->{uid});
+			$requested_user = isAnon($uid) ? $user : $reader->getUser($id);
+			$nick = $requested_user->{nickname};
+			$form->{userfield} = $nick if $admin_flag;
+
+		} elsif ($form->{nick} && ! $id) {
+			$fieldkey = 'nickname';
+			($nick, $id) = ($form->{nick}, $form->{nick});
+			$uid = $reader->getUserUID($id);
+			if (isAnon($uid)) {
+				$requested_user = $user;
+				($nick, $uid, $id) = @{$user}{qw(nickname uid nickname)};
+			} else {
+				$requested_user = $reader->getUser($uid);
+			}
+			$form->{userfield} = $uid if $admin_flag;
+
+		} else {
+			$fieldkey = 'uid';
+			($id, $uid) = ($user->{uid}, $user->{uid});
+			$requested_user = $reader->getUser($uid);
+			$form->{userfield} = $uid if $admin_flag;
+		}
+
+	} elsif ($user->{is_admin}) {
+		$id ||= $form->{userfield} || $user->{uid};
+		if ($id =~ /^[0-9a-f]{16}$/) {
+			$requested_user->{nonuid} = 1;
+			$fieldkey = "srcid";
+			$requested_user->{$fieldkey} = $id;
+		} elsif ($id =~ /^\d+$/) {
+			# If it's longer than a uid could possibly be, it
+			# must be a srcid.  The uid column right now is a
+			# MEDIUMINT (max 16M) but at most might someday
+			# be an INT (max 4G).
+			if (length($id) > 11) {
+				$requested_user->{nonuid} = 1;
+				$fieldkey = "srcid";
+				$requested_user->{$fieldkey} = $id;
+			} else {
+				$fieldkey = 'uid';
+				$requested_user = $reader->getUser($id);
+				$uid = $requested_user->{uid};
+				$nick = $requested_user->{nickname};
+				if ((my $conflict_id = $reader->getUserUID($id)) && $form->{userinfo}) {
+					#slashDisplay('showInfoConflict', {
+						#op		=> 'userinfo',
+						#id		=> $uid,
+						#nick		=> $nick,
+						#conflict_id	=> $conflict_id
+					#});
+					return 1;
+				}
+			}
+
+		} elsif (length($id) == 32) {
+			$requested_user->{nonuid} = 1;
+			if ($form->{fieldname}
+				&& $form->{fieldname} =~ /^(ipid|subnetid)$/) {
+				$fieldkey = $form->{fieldname};
+			} else {
+				$fieldkey = 'md5id';
+			}
+			$requested_user->{$fieldkey} = $id;
+		} elsif ($id =~ /^(\d{1,3}\.\d{1,3}.\d{1,3}\.0)$/ 
+				|| $id =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3})\.?$/) {
+			$fieldkey = 'subnetid';
+			$requested_user->{subnetid} = $1; 
+			$requested_user->{subnetid} .= '.0' if $requested_user->{subnetid} =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}$/; 
+			$requested_user->{nonuid} = 1;
+			$requested_user->{subnetid} = md5_hex($requested_user->{subnetid});
+
+		} elsif ($id =~ /^([\d+\.]+)$/) {
+			$fieldkey = 'ipid';
+			$requested_user->{nonuid} = 1;
+			$id ||= $1;
+			$requested_user->{ipid} = md5_hex($1);
+
+		} elsif ($id =~ /^(.*@.*\..*?)$/) {
+			# check for email addy, but make it by uid
+			$fieldkey = 'uid';
+			$id = $uid = $reader->getUserEmail($id);
+			$requested_user = $reader->getUser($uid);
+			$nick = $requested_user->{nickname};
+
+		} else {  # go by nickname, but make it by uid
+			$fieldkey = 'uid';
+			$id = $uid = $reader->getUserUID($id);
+			$requested_user = $reader->getUser($uid);
+			$nick = $requested_user->{nickname};
+		}
+		
+	} else {
+		$fieldkey = 'uid';
+		($id, $uid) = ($user->{uid}, $user->{uid});
+		$requested_user = $reader->getUser($uid);
+	}
+
+	# Can't get user data for the anonymous user.
+	if ($fieldkey eq 'uid' && isAnon($uid)) {
+		header(getMessage('user_header')) or return;
+		return displayForm();
+	}
+
+	my $user_change = { };
+	if ($fieldkey eq 'uid' && !$user->{is_anon}
+		&& $uid != $user->{uid} && !isAnon($uid)) {
+		# Store the fact that this user last looked at that user.
+		# For maximal convenience in stalking.
+		$user_change->{lastlookuid} = $uid;
+		$user_change->{lastlooktime} = time;
+		$user->{lastlookuid} = $uid;
+		$user->{lastlooktime} = time;
+		$hr->{tab_selected_1} = 'otheruser';
+	}
+
+	# showInfo's header information is delayed until here, because
+	# the target user's info is not available until here.
+	vislenify($requested_user);
+	header(getMessage('user_header', { useredit => $requested_user, fieldkey => $fieldkey })) or return;
+	# This is a hardcoded position, bad idea and should be fixed -Brian
+	# Yeah, we should pull this into a template somewhere...
+	print getMessage('note', { note => $hr->{note} }) if defined $hr->{note};
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $hr->{tab_selected_1} || "",
+	});
+
+	my $comments_wanted = $user->{show_comments_num}
+		|| $constants->{user_comment_display_default};
+	my $min_comment = $form->{min_comment} || 0;
+	$min_comment = 0 unless $user->{seclev} > $constants->{comments_more_seclev}
+		|| $constants->{comments_more_seclev} == 2 && $user->{is_subscriber};
+
+	my($netid, $netid_vis) = ('', '');
+
+	my $comment_time;
+	my $non_admin_limit = $comments_wanted;
+
+	if ($requested_user->{nonuid}) {
+		$requested_user->{fg} = $user->{fg};
+		$requested_user->{bg} = $user->{bg};
+
+		if ($requested_user->{ipid}) {
+			$netid = $requested_user->{ipid} ;
+
+		} elsif ($requested_user->{md5id}) {
+			$netid = $requested_user->{md5id} ;
+
+		} elsif ($requested_user->{srcid}) {
+			$netid = $requested_user->{srcid} ;
+		} else {
+			$netid = $requested_user->{subnetid} ;
+		}
+
+		my $data = {
+			id => $id,
+			md5id => $netid,
+		};
+		vislenify($data); # add $data->{md5id_vis}
+		$netid_vis = $data->{md5id_vis};
+
+		$title = getTitle('user_netID_user_title', $data);
+
+		$admin_block = getUserAdmin($netid, $fieldkey, 0) if $admin_flag;
+
+		if ($form->{fieldname}) {
+			if ($form->{fieldname} eq 'ipid') {
+				$commentcount 		= $reader->countCommentsByIPID($netid);
+				$commentcount_time 	= $reader->countCommentsByIPID($netid, { cid_at_or_after => $cid_for_time_period });
+				$comments = getCommentListing("ipid", $netid,
+					$min_comment, $time_period, $commentcount, $commentcount_time, $cid_for_time_period, 
+					$non_admin_limit, $admin_time_period_limit, $admin_non_time_limit)
+						if $commentcount;
+			} elsif ($form->{fieldname} eq 'subnetid') {
+				$commentcount 		= $reader->countCommentsBySubnetID($netid);
+				$commentcount_time	= $reader->countCommentsBySubnetID($netid, { cid_at_or_after => $cid_for_time_period });
+				$comments = getCommentListing("subnetid", $netid,
+					$min_comment, $time_period, $commentcount, $commentcount_time, $cid_for_time_period,
+					$non_admin_limit, $admin_time_period_limit, $admin_non_time_limit)
+						if $commentcount;
+
+			} else {
+				delete $form->{fieldname};
+			}
+		}
+		if (!defined($comments)) {
+			# Last resort; here for backwards compatibility mostly.
+			my $type;
+			($commentcount,$type) = $reader->countCommentsByIPIDOrSubnetID($netid);
+			$commentcount_time = $reader->countCommentsByIPIDOrSubnetID($netid, { cid_at_or_after => $cid_for_time_period });
+			if ($type eq "ipid") {
+				$comments = getCommentListing("ipid", $netid,
+					$min_comment, $time_period, $commentcount, $commentcount_time, $cid_for_time_period,
+					$non_admin_limit, $admin_time_period_limit, $admin_non_time_limit)
+						if $commentcount;
+			} elsif ($type eq "subnetid") {
+				$comments = getCommentListing("subnetid", $netid,
+					$min_comment, $time_period, $commentcount, $commentcount_time,  $cid_for_time_period,
+					$non_admin_limit, $admin_time_period_limit, $admin_non_time_limit)
+						if $commentcount;
+			}
+		}	
+	} else {
+		$admin_block = getUserAdmin($id, $fieldkey, 1) if $admin_flag;
+
+		$commentcount      = $reader->countCommentsByUID($requested_user->{uid});
+		$commentcount_time = $reader->countCommentsByUID($requested_user->{uid}, { cid_at_or_after => $cid_for_time_period });
+		$comments = getCommentListing("uid", $requested_user->{uid},
+			$min_comment, $time_period, $commentcount, $commentcount_time, $cid_for_time_period,
+			$non_admin_limit, $admin_time_period_limit, $admin_non_time_limit,
+			{ use_uid_cid_cutoff => 1 })
+				if $commentcount;
+		$netid = $requested_user->{uid};
+	}
+
+	# Grab the nicks of the uids we have, we're going to be adding them
+	# into the struct.
+	my @users_extra_cols_wanted       = qw( nickname );
+	my @discussions_extra_cols_wanted = qw( type );
+	my $uid_hr = { };
+	my $sid_hr = { };
+	if ($comments && @$comments) {
+		my %uids = ();
+		my %sids = ();
+		for my $c (@$comments) {
+			$uids{$c->{uid}}++;
+			$sids{$c->{sid}}++;
+		}
+		my $uids = join(", ", sort { $a <=> $b } keys %uids);
+		my $sids = join(", ", sort { $a <=> $b } keys %sids);
+		$uid_hr = $reader->sqlSelectAllHashref(
+			"uid",
+			"uid, " . join(", ", @users_extra_cols_wanted),
+			"users",
+			"uid IN ($uids)"
+		);
+		
+		$sid_hr = $reader->sqlSelectAllHashref(
+			"id",
+			"id, " . join(", ", @discussions_extra_cols_wanted),
+			"discussions",
+			"id IN ($sids)"
+		);
+		
+	}
+
+	my $cids_seen = {};
+	my $kinds = $slashdb->getDescriptions('discussion_kinds');
+	for my $comment (@$comments) {
+		$cids_seen->{$comment->{cid}}++;
+		my $type;
+		# This works since $sid is numeric.
+		my $replies = $reader->countCommentsBySidPid($comment->{sid}, $comment->{cid});
+
+		# This is cached.
+		my $discussion = $reader->getDiscussion($comment->{sid});
+#use Data::Dumper; if ($discussion && !$discussion->{dkid}) { print STDERR scalar(gmtime) . " users.pl discussion but no dkid: " . Dumper($discussion) }
+		if (!$discussion || !$discussion->{dkid}) {
+			# A comment with no accompanying discussion;
+			# basically we pretend it doesn't exist.
+			next;
+		} elsif ($kinds->{ $discussion->{dkid} } =~ /^journal(?:-story)?$/) {
+			$type = 'journal';
+		} elsif ($kinds->{ $discussion->{dkid} } eq 'poll') {
+			$type = 'poll';
+		} else {
+			$type = 'story';
+		}
+
+		$comment->{points} += $user->{karma_bonus}
+			if $user->{karma_bonus} && $comment->{karma_bonus} eq 'yes';
+		$comment->{points} += $user->{subscriber_bonus}
+			if $user->{subscriber_bonus} && $comment->{subscriber_bonus} eq 'yes';
+
+		# fix points in case they are out of bounds
+		$comment->{points} = $constants->{comment_minscore} if $comment->{points} < $constants->{comment_minscore};
+		$comment->{points} = $constants->{comment_maxscore} if $comment->{points} > $constants->{comment_maxscore};
+		vislenify($comment);
+		my $data = {
+			pid 		=> $comment->{pid},
+			url		=> $discussion->{url},
+			disc_type 	=> $type,
+			disc_title	=> $discussion->{title},
+			disc_time	=> $discussion->{ts},
+			sid 		=> $comment->{sid},
+			cid 		=> $comment->{cid},
+			subj		=> $comment->{subject},
+			cdate		=> $comment->{date},
+			pts		=> $comment->{points},
+			reason		=> $comment->{reason},
+			uid		=> $comment->{uid},
+			replies		=> $replies,
+			ipid		=> $comment->{ipid},
+			ipid_vis	=> $comment->{ipid_vis},
+			karma		=> $comment->{karma},
+			tweak		=> $comment->{tweak},
+			tweak_orig	=> $comment->{tweak_orig},
+		
+		};
+		#Karma bonus time
+
+		for my $col (@users_extra_cols_wanted) {
+			$data->{$col} = $uid_hr->{$comment->{uid}}{$col} if defined $uid_hr->{$comment->{uid}}{$col};
+		}
+		for my $col(@discussions_extra_cols_wanted) {
+			$data->{$col} = $sid_hr->{$comment->{sid}}{$col} if defined $sid_hr->{$comment->{sid}}{$col};
+		}
+		push @$commentstruct, $data;
+	}
+#	if (grep { !defined($_->{disc_time}) || !defined($_->{sid}) } @$commentstruct) { use Data::Dumper; print STDERR "showInfo undef in commentstruct for id=$id: " . Dumper($commentstruct) }
+	# Sort so the chosen group of comments is sorted by discussion
+	@$commentstruct = sort {
+		$b->{disc_time} cmp $a->{disc_time} || $b->{sid} <=> $a->{sid}
+	} @$commentstruct
+		unless $user->{user_comment_sort_type} && $user->{user_comment_sort_type} == 1;
+
+	my $cid_list = [ keys %$cids_seen ];
+	my $cids_to_mods = {};
+	my $mod_reader = getObject("Slash::$constants->{m1_pluginname}", { db_type => 'reader' });
+	if ($constants->{m1} && $admin_flag && $constants->{show_mods_with_comments}) {
+		my $comment_mods = $mod_reader->getModeratorCommentLog("DESC",
+			$constants->{mod_limit_with_comments}, "cidin", $cid_list);
+	
+		# Loop through mods and group them by the sid they're attached to
+		while (my $mod = shift @$comment_mods) {
+			push @{$cids_to_mods->{$mod->{cid}}}, $mod;
+		}
+	}
+
+	my $sub_limit = ((($admin_flag || $user->{uid} == $requested_user->{uid}) ? $constants->{submissions_all_page_size} : $constants->{submissions_accepted_only_page_size}) || "");
+	
+	my $sub_options = { limit_days => 365 };
+	$sub_options->{accepted_only} = 1 if !$admin_flag && $user->{uid} != $requested_user->{uid};
+
+	my $sub_field = $form->{fieldname};
+	
+	my ($subcount, $ret_field) = $reader->countSubmissionsByNetID($netid, $sub_field)
+		if $requested_user->{nonuid};
+	my $submissions = $reader->getSubmissionsByNetID($netid, $ret_field, $sub_limit, $sub_options)
+		if $requested_user->{nonuid};
+
+        my $ipid_hoursback = $constants->{istroll_ipid_hours} || 72;
+	my $uid_hoursback = $constants->{istroll_uid_hours} || 72;
+
+	if ($requested_user->{nonuid}) {
+		#slashDisplay('netIDInfo', {
+			#title			=> $title,
+			#id			=> $id,
+			#useredit		=> $requested_user,
+			#commentstruct		=> $commentstruct || [],
+			#commentcount		=> $commentcount,
+			#min_comment		=> $min_comment,
+			#admin_flag		=> $admin_flag,
+			#admin_block		=> $admin_block,
+			#netid			=> $netid,
+			#netid_vis		=> $netid_vis,
+			#reasons			=> $mod_reader->getReasons(),
+			#subcount		=> $subcount,
+			#submissions		=> $submissions,
+			#hr_hours_back		=> $ipid_hoursback,
+			#cids_to_mods		=> $cids_to_mods,
+			#comment_time		=> $comment_time
+		#});
+
+	} else {
+		if (! $requested_user->{uid}) {
+			print getError('userinfo_idnf_err', { id => $id, fieldkey => $fieldkey});
+			return 1;
+		}
+
+		$karma_flag = 1 if $admin_flag;
+		$requested_user->{nick_plain} = $nick ||= $requested_user->{nickname};
+		$nick = strip_literal($nick);
+
+		if ($requested_user->{uid} == $user->{uid}) {
+			$karma_flag = 1;
+			$nickmatch_flag = 1;
+			$points = $requested_user->{points};
+
+			$mod_flag = 1 if $points > 0;
+
+			$title = getTitle('userInfo_main_title', { nick => $nick, uid => $uid });
+
+		} else {
+			$title = getTitle('userInfo_user_title', { nick => $nick, uid => $uid });
+		}
+
+		my $lastjournal = _get_lastjournal($uid);
+		
+		my $subcount = $reader->countSubmissionsByUID($uid);
+	
+		my $submissions = $reader->getSubmissionsByUID($uid, $sub_limit, $sub_options);
+		my $metamods;
+		if ($constants->{m2} && $admin_flag) {
+			my $metamod_reader = getObject('Slash::Metamod', { db_type => 'reader' });
+			$metamods = $metamod_reader->getMetamodlogForUser($uid, 30);
+		}
+
+		my $tags_reader = getObject('Slash::Tags', { db_type => 'reader' });
+		my $tagshist = [];
+		if ($tags_reader && $user->{is_admin}) {
+			$tagshist = $tags_reader->getAllTagsFromUser($requested_user->{uid}, { orderby => 'created_at', orderdir => 'DESC', limit => 30, include_private => 1 });
+		}
+
+                my $comment_blocks = $reader->sqlSelectAllHashref(
+                        'uid', 'bid, uid, block', 'user_event_blocks', "uid = $uid and code = 1");
+
+                my @block_ids = split(/,/, $comment_blocks->{$uid}->{block});
+
+                my %latest_comments;
+                foreach my $comment_block (@block_ids) {
+                        ($latest_comments{$comment_block}->{sid}, $latest_comments{$comment_block}->{subject})
+                                = $reader->sqlSelect("sid, subject", "comments", "cid = $comment_block");
+                        #$latest_comments{$comment_block}->{text} = $reader->sqlSelect("comment", "comment_text", "cid = $comment_block");
+                }
+
+                my $journal_blocks = $reader->sqlSelectAllHashref(
+                        'uid', 'bid, uid, block', 'user_event_blocks', "uid = $uid and code = 2");
+
+                @block_ids = split(/,/, $journal_blocks->{$uid}->{block});
+                my %latest_journals;
+                foreach my $journal_block (@block_ids) {
+                        ($latest_journals{$journal_block}->{id}, $latest_journals{$journal_block}->{desc})
+                                = $reader->sqlSelect("id, description", "journals", "discussion = $journal_block");
+                }
+
+		slashDisplay('userInfo2', {
+			title			=> $title,
+			uid			=> $uid,
+			useredit		=> $requested_user,
+			points			=> $points,
+			commentstruct		=> $commentstruct || [],
+			commentcount		=> $commentcount,
+			min_comment		=> $min_comment,
+			nickmatch_flag		=> $nickmatch_flag,
+			mod_flag		=> $mod_flag,
+			karma_flag		=> $karma_flag,
+			admin_block		=> $admin_block,
+			admin_flag 		=> $admin_flag,
+			reasons			=> $mod_reader->getReasons(),
+			lastjournal		=> $lastjournal,
+			hr_hours_back		=> $ipid_hoursback,
+			cids_to_mods		=> $cids_to_mods,
+			comment_time		=> $comment_time,
+			submissions		=> $submissions,
+			subcount		=> $subcount,
+			metamods		=> $metamods,
+			tagshist		=> $tagshist,
+                        latest_comments         => \%latest_comments,
+                        latest_journals         => \%latest_journals,
+		}, { Page => 'users', Skin => 'default'});
+	}
+
+	if ($user_change && %$user_change) {
+		$slashdb->setUser($user->{uid}, $user_change);
+	}
+
+	return 1;
+}
+
+sub _get_lastjournal {
+	my($uid) = @_;
+	my $user = getCurrentUser();
+	my $constants = getCurrentStatic();
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $lastjournal = undef;
+	if (my $journal = getObject('Slash::Journal', { db_type => 'reader' })) {
+		my $j = $journal->getsByUid($uid, 0, 1);
+		if ($j && @$j) {
+			# Yep, there are 1 or more journals... get the first.
+			$j = $j->[0];
+		}
+		if ($j && @$j) {
+			# Yep, that first journal exists and has entries...
+			# convert from stupid numeric array to a hashref.
+			my @field = qw(	date article description id
+					posttype tid discussion		);
+			$lastjournal = { };
+			for my $i (0..$#field) {
+				$lastjournal->{$field[$i]} = $j->[$i];
+			}
+		}
+	}
+
+	if ($lastjournal) {
+
+		# Strip the article field for display.
+		$lastjournal->{article} = strip_mode($lastjournal->{article},
+			$lastjournal->{posttype});
+
+		# For display, include a reduced-size version, where the
+		# size is based on the user's maxcomment size (which
+		# defaults to 4K) and can't have too many line-breaking
+		# tags.
+		my $art_shrunk = $lastjournal->{article};
+		my $maxsize = int($user->{maxcommentsize} / 25);
+		$maxsize =  80 if $maxsize <  80;
+		$maxsize = 600 if $maxsize > 600;
+		$art_shrunk = chopEntity($art_shrunk, $maxsize);
+
+		my $approvedtags_break = $constants->{approvedtags_break} || [];
+		my $break_tag = join '|', @$approvedtags_break;
+		if (scalar(() = $art_shrunk =~ /<(?:$break_tag)>/gi) > 2) {
+			$art_shrunk =~ s/\A
+			(
+				(?: <(?:$break_tag)> )?
+				.*?   <(?:$break_tag)>
+				.*?
+			)	<(?:$break_tag)>.*
+			/$1/six;
+			if (length($art_shrunk) < 15) {
+				# This journal entry has too much whitespace
+				# in its first few chars;  scrap it.
+				undef $art_shrunk;
+			}
+			$art_shrunk = chopEntity($art_shrunk) if defined($art_shrunk);
+		}
+
+		if (defined $art_shrunk) {
+			if (length($art_shrunk) < length($lastjournal->{article})) {
+				$art_shrunk .= " ...";
+			}
+			$art_shrunk = strip_html($art_shrunk);
+			$art_shrunk = balanceTags($art_shrunk);
+		}
+
+		$lastjournal->{article_shrunk} = $art_shrunk;
+
+		if ($lastjournal->{discussion}) {
+			$lastjournal->{commentcount} = $reader->getDiscussion(
+				$lastjournal->{discussion}, 'commentcount');
+		}
+	}
+	return $lastjournal;
+}
+
+#####################################################################
+sub validateUser {
+	my($hr) = @_;
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $slashdb = getCurrentDB();
+	my $constants = getCurrentStatic();
+
+	# If we aren't expiring accounts in some way, we don't belong here.
+	if (! allowExpiry()) {
+		displayForm();
+		return;
+	}
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $hr->{tab_selected_1} || "",
+	});
+
+	# Since we are here, if the minimum values for the comment trigger and
+	# the day trigger are -1, then they should be reset to 1.
+	$constants->{min_expiry_comm} = $constants->{min_expiry_days} = 1
+		if $constants->{min_expiry_comm} <= 0 ||
+		   $constants->{min_expiry_days} <= 0;
+
+	if ($user->{is_anon} || $user->{registered}) {
+		if ($user->{is_anon}) {
+			print getError('anon_validation_attempt');
+			displayForm();
+			return;
+		} else {
+			print getMessage('no_registration_needed')
+				if !$user->{reg_id};
+			showInfo({ uid => $user->{uid} });
+			return;
+		}
+	# Maybe this should be taken care of in a more centralized location?
+	} elsif ($user->{reg_id} eq $form->{id}) {
+		# We have a user and the registration IDs match. We are happy!
+		my($maxComm, $maxDays) = ($constants->{max_expiry_comm},
+					  $constants->{max_expiry_days});
+		my($userComm, $userDays) =
+			($user->{user_expiry_comm}, $user->{user_expiry_days});
+
+		# Ensure both $userComm and $userDays aren't -1 (expiry has
+		# just been turned on).
+		$userComm = $constants->{min_expiry_comm}
+			if $userComm < $constants->{min_expiry_comm};
+		$userDays = $constants->{min_expiry_days}
+			if $userDays < $constants->{min_expiry_days};
+
+		my $exp = $constants->{expiry_exponent};
+
+		# Increment only the trigger that was used.
+		my $new_comment_expiry = ($maxComm > 0 && $userComm > $maxComm)
+			? $maxComm
+			: $userComm * (($user->{expiry_comm} < 0)
+				? $exp
+				: 1
+		);
+		my $new_days_expiry = ($maxDays > 0 && $userDays > $maxDays)
+			? $maxDays
+			: $userDays * (($user->{expiry_days} < 0)
+				? $exp
+				: 1
+		);
+
+		# Reset re-registration triggers for user.
+		$slashdb->setUser($user->{uid}, {
+			'expiry_comm'		=> $new_comment_expiry,
+			'expiry_days'		=> $new_days_expiry,
+			'user_expiry_comm'	=> $new_comment_expiry,
+			'user_expiry_days'	=> $new_days_expiry,
+		});
+
+		# Handles rest of re-registration process.
+		setUserExpired($user->{uid}, 0);
+	}
+
+	slashDisplay('regResult');
+}
+
+#####################################################################
+sub editTags {
+	my($hr) = @_;
+	my $slashdb = getCurrentDB();
+	my $user = getCurrentUser(); 
+	my $constants = getCurrentStatic();
+	my $note = $hr->{note} || "";
+
+	return if $user->{is_anon}; # shouldn't be, but can't hurt to check
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $hr->{tab_selected_1} || "",
+	});
+
+	my $user_edit = $slashdb->getUser($user->{uid});
+	my $title = getTitle('editTags_title');
+
+	slashDisplay('editTags', {
+		user_edit	=> $user_edit,
+		title		=> $title,
+		note		=> $note,
+	});
+}
+
+sub saveTags {
+	my($hr) = @_;
+	my $slashdb = getCurrentDB();
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $constants = getCurrentStatic();
+
+	return if $user->{is_anon}; # shouldn't be, but can't hurt to check
+
+	$slashdb->setUser($user->{uid}, {
+		tags_turnedoff =>	$form->{showtags} ? '' : 1 });
+	editTags({ note => getMessage('savetags_msg') });
+}
+
+#####################################################################
+sub showTags {
+	my($hr) = @_;
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $slashdb = getCurrentDB();
+	my $constants = getCurrentStatic();
+	my $tags_reader = getObject('Slash::Tags', { db_type => 'reader' });
+
+	my($uid, $user_edit);
+	if ($form->{uid} || $form->{nick}) {
+		$uid = $form->{uid} || $tags_reader->getUserUID($form->{nick});
+		$user_edit = $tags_reader->getUser($uid);
+	}
+	if (!$user_edit || $user_edit->{is_anon}) {
+		$uid = $user->{uid};
+		$user_edit = $user;
+	}
+	my $nickname = $user_edit->{nickname};
+
+	if (!$constants->{plugin}{Tags}) {
+		print getError('bad_op', { op => $form->{op}});
+		return;
+	}
+
+	my $tags_ar = $tags_reader->getGroupedTagsFromUser($user_edit->{uid});
+	slashDisplay('usertags', {
+		useredit	=> $user_edit,
+		tags_grouped	=> $tags_ar,
+	});
+}
+
+#################################################################
+sub showBookmarks {
+	my($hr) = @_;
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $slashdb = getCurrentDB();
+	my $constants = getCurrentStatic();
+	my $tags_reader = getObject('Slash::Tags', { db_type => 'reader' });
+
+	my($uid, $user_edit);
+	if ($form->{uid} || $form->{nick}) {
+		$uid = $form->{uid} || $tags_reader->getUserUID($form->{nick});
+		$user_edit = $tags_reader->getUser($uid);
+	}
+	if (!$user_edit || $user_edit->{is_anon}) {
+		$uid = $user->{uid};
+		$user_edit = $user;
+	}
+	my $nickname = $user_edit->{nickname};
+
+	if (!$constants->{plugin}{Tags}) {
+		print getError('bad_op', { op => $form->{op}});
+		return;
+	}
+
+	my $tags_ar = $tags_reader->getGroupedTagsFromUser($user_edit->{uid}, { type => "urls", only_bookmarked => 1 });
+
+	slashDisplay('userbookmarks', {
+		useredit	=> $user_edit,
+		tags_grouped	=> $tags_ar,
+	});
+}
+
+#################################################################
+sub editKey {
+	my($uid) = @_;
+
+	my $slashdb = getCurrentDB();
+
+	my $pubkey = $slashdb->getUser($uid, 'pubkey');
+	my $editkey = slashDisplay('editKey', { pubkey => $pubkey }, 1);
+	return $editkey;
+}
+
+#################################################################
+# We arrive here without header() having been called.  Some of the
+# functions we dispatch to call it, some do not.
+sub adminDispatch {
+	my($hr) = @_;
+	my $form = getCurrentForm();
+	my $op = $hr->{op} || $form->{op};
+
+	if ($op eq 'authoredit') {
+		# editUser() does not call header(), so we DO need to.
+		header(getMessage('user_header'), '', {}) or return;
+		editUser($hr);
+
+	} elsif ($form->{saveuseradmin}) {
+		# saveUserAdmin() tail-calls showInfo(), which calls
+		# header(), so we need to NOT.
+		saveUserAdmin($hr);
+
+	} elsif ($form->{userinfo}) {
+		# showInfo() calls header(), so we need to NOT.
+		showInfo($hr);
+
+	} elsif ($form->{userfield}) {
+		# none of these calls header(), so we DO need to.
+		header(getMessage('user_header'), '', {}) or return;
+		if ($form->{edituser}) {
+			editUser($hr);
+
+		} elsif ($form->{edithome}) {
+			editHome($hr);
+
+		} elsif ($form->{editcomm}) {
+			editComm($hr);
+
+		} elsif ($form->{changepasswd}) {
+			changePasswd($hr);
+		}
+
+	} else {
+		# showInfo() calls header(), so we need to NOT.
+		showInfo($hr);
+	}
+}
+
+#################################################################
+sub tildeEd {
+	my($user_edit) = @_;
+
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $constants = getCurrentStatic();
+
+	my %story023_default = (
+		author	=> { },
+		nexus	=> { },
+		topic	=> { },
+	);
+
+	my %prefs = ( );
+	for my $field (qw(
+		story_never_topic	story_never_author	story_never_nexus
+		story_always_topic	story_always_author	story_always_nexus	story_brief_always_nexus
+		story_full_brief_nexus	story_full_best_nexus	story_brief_best_nexus
+	)) {
+		for my $id (
+			grep /^\d+$/,
+			split /,/,
+			($user_edit->{$field} || "")
+		) {
+			$prefs{$field}{$id} = 1;
+		}
+	}
+#print STDERR scalar(localtime) . " prefs: " . Dumper(\%prefs);
+
+	# Set up $author_hr, @aid_order, and $story023_default{author}.
+
+	my $author_hr = $reader->getDescriptions('authors');
+	my @aid_order = sort { lc $author_hr->{$a} cmp lc $author_hr->{$b} } keys %$author_hr;
+	for my $aid (@aid_order) {
+		     if ($prefs{story_never_author}{$aid}) {
+			$story023_default{author}{$aid} = 0;
+		} elsif ($prefs{story_always_author}{$aid}) {
+			$story023_default{author}{$aid} = 3;
+		} else {
+			$story023_default{author}{$aid} = 2;
+		}
+	}
+
+	# Set up $topic_hr, @topictid_order, and $story023_default{topic}.
+
+	my $topic_hr = $reader->getDescriptions('non_nexus_topics-storypickable');
+	my @topictid_order = sort { lc $topic_hr->{$a} cmp lc $topic_hr->{$b} } keys %$topic_hr;
+	for my $tid (@topictid_order) {
+		     if ($prefs{story_never_topic}{$tid}) {
+			$story023_default{topic}{$tid} = 0;
+		} elsif ($prefs{story_always_topic}{$tid}) {
+			$story023_default{topic}{$tid} = 3;
+		} else {
+			$story023_default{topic}{$tid} = 2;
+		}
+	}
+
+	# Set up $nexus_hr, @nexustid_order, and $story023_default{nexus}.
+	my $topic_tree = $reader->getTopicTree();
+	my $nexus_tids_ar = $reader->getStorypickableNexusChildren($constants->{mainpage_nexus_tid}, 1);
+	my $nexus_hr = { };
+	
+	for my $tid (@$nexus_tids_ar) {
+		$nexus_hr->{$tid} = $topic_tree->{$tid}{textname};
+	}
+	my @nexustid_order = sort {($b == $constants->{mainpage_nexus_tid}) <=> ($a == $constants->{mainpage_nexus_tid}) || 
+				    lc $nexus_hr->{$a} cmp lc $nexus_hr->{$b} } keys %$nexus_hr;
+
+	my $mp_disp_nexuses = $reader->getMainpageDisplayableNexuses();
+	my %mp_disp_nexus = ( map { ($_, 1) } @$mp_disp_nexuses );
+	for my $tid (@nexustid_order) {
+		     if ($prefs{story_never_nexus}{$tid}) {
+			$story023_default{nexus}{$tid} = 0;
+		} elsif ($prefs{story_always_nexus}{$tid}) {
+			$story023_default{nexus}{$tid} = 5;
+		} elsif ($prefs{story_full_brief_nexus}{$tid}) {
+			$story023_default{nexus}{$tid} = 4;
+		} elsif ($prefs{story_brief_always_nexus}{$tid}) {
+			$story023_default{nexus}{$tid} = 3;
+		} elsif ($prefs{story_full_best_nexus}{$tid}) {
+			$story023_default{nexus}{$tid} = 2;
+		} elsif ($prefs{story_brief_best_nexus}) {
+			$story023_default{nexus}{$tid} = 1;
+		} else {
+			# If brief_sectional_mainpage is set, then all
+			# nexuses in getMainpageDisplayableNexuses are,
+			# by default, shown as brief on the mainpage.
+			if ($constants->{brief_sectional_mainpage}
+				&& $mp_disp_nexus{$tid}
+			) {
+				$story023_default{nexus}{$tid} = 4;
+			} else {
+				$story023_default{nexus}{$tid} = 2;
+			}
+		}
+	}
+
+	# Set up $section_descref and $box_order, used to decide which
+	# slashboxes appear.  Really this doesn't seem to have anything
+	# to do with sections, so I'm not sure why it's called
+	# "section"_descref.
+
+	my $section_descref = { };
+	my $box_order;
+	my $sections_description = $reader->getSectionBlocks();
+
+	# the names of all the boxes in @{$skinBoxes->{$constants->{mainpage_skid}}}
+	# should be unioned into sections_description.  whether the
+	# values are 0 or 1 is calculated correctly, but we're
+	# missing some 0's that should appear, I think, under
+	# some circumstances.  ah heck, the whole concept of
+	# sectional slashboxes should be redone (why the heck
+	# do we have skinname_more instead of just a block
+	# called olderstories?)
+
+	my $slashboxes_hr = { };
+	my $slashboxes_textlist = $user_edit->{slashboxes};
+	if (!$slashboxes_textlist) {
+		# Use the default.
+		my($boxes, $skinBoxes) = $reader->getPortalsCommon();
+		$slashboxes_textlist = join ",", @{$skinBoxes->{$constants->{mainpage_skid}}};
+	}
+	for my $bid (
+		map { /^'?([^']+)'?$/; $1 }
+		split /,/,
+		$slashboxes_textlist
+	) {
+		$slashboxes_hr->{$bid} = 1;
+	}
+	for my $ary (sort { lc $a->[1] cmp lc $b->[1]} @$sections_description) {
+		my($bid, $title, $boldflag) = @$ary;
+		push @$box_order, $bid;
+		$section_descref->{$bid}{checked} =
+			$slashboxes_hr->{$bid}
+				? $constants->{markup_checked_attribute}
+				: '';
+		$title =~ s/<(.*?)>//g;
+		$section_descref->{$bid}{title} = $title;
+	}
+
+#print STDERR scalar(localtime) . " tildeEd story023_default: " . Dumper(\%story023_default);
+
+	# Userspace.
+
+	my $userspace = $user_edit->{mylinks} || "";
+
+	# Titles of stuff.
+
+	my $tildeEd_title = getTitle('tildeEd_title');
+	my $criteria_msg = getMessage('tilded_criteria_msg');
+	my $customize_title = getTitle('tildeEd_customize_title');
+	my $tilded_customize_msg = getMessage('tilded_customize_msg',
+		{ userspace => $userspace });
+	my $tilded_box_msg = getMessage('tilded_box_msg');
+
+	my $tilde_ed = slashDisplay('tildeEd', {
+		user_edit		=> $user_edit,
+		title			=> $tildeEd_title,
+		criteria_msg		=> $criteria_msg,
+		customize_title		=> $customize_title,
+		tilded_customize_msg	=> $tilded_customize_msg,
+		tilded_box_msg		=> $tilded_box_msg,
+
+		story023_default	=> \%story023_default,
+		authorref		=> $author_hr,
+		aid_order		=> \@aid_order,
+		topicref		=> $topic_hr,
+		topictid_order		=> \@topictid_order,
+		nexusref		=> $nexus_hr,
+		nexustid_order		=> \@nexustid_order,
+
+		section_descref		=> $section_descref,
+		box_order		=> $box_order,
+
+		userspace		=> $userspace,
+	}, 1);
+
+	return $tilde_ed;
+}
+
+#################################################################
+sub changePasswd {
+	my($hr) = @_;
+	my $form = getCurrentForm();
+	my $slashdb = getCurrentDB();
+	my $user = getCurrentUser();
+	my $constants = getCurrentStatic();
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $hr->{tab_selected_1} || "",
+	});
+
+	my $user_edit = {};
+	my $title;
+	my $suadmin_flag = ($user->{seclev} >= 10000) ? 1 : 0;
+
+	my $admin_flag = ($user->{is_admin}) ? 1 : 0;
+	my $admin_block = '';
+
+	my $id = '';
+	if ($admin_flag) {
+		if ($form->{userfield}) {
+			$id ||= $form->{userfield};
+			if ($id =~ /^\d+$/) {
+				$user_edit = $slashdb->getUser($id);
+			} else {
+				$user_edit = $slashdb->getUser($slashdb->getUserUID($id));
+			}
+		} else {
+			$user_edit = $id eq '' ? $user : $slashdb->getUser($id);
+			$id = $user_edit->{uid};
+		}
+	} else {
+		$id = $user->{uid};
+		$user_edit = $user;
+	}
+
+	$admin_block = getUserAdmin($id, 'uid', 1) if $admin_flag;
+
+	# print getMessage('note', { note => $form->{note}}) if $form->{note};
+
+	$title = getTitle('changePasswd_title', { user_edit => $user_edit });
+
+	my $session = $slashdb->getDescriptions('session_login');
+	my $session_select = createSelect('session_login', $session, $user_edit->{session_login}, 1);
+
+	my $clocation = $slashdb->getDescriptions('cookie_location');
+	my @clocation_order = grep { exists $clocation->{$_} } qw(none classbid subnetid ipid);
+	my $clocation_select = createSelect('cookie_location', $clocation,
+		$user_edit->{cookie_location}, 1, 0, \@clocation_order
+	);
+
+	my $got_oldpass = 0;
+	if ($form->{oldpass}) {
+		my $return_uid = $slashdb->getUserAuthenticate($id, $form->{oldpass}, 1);
+		$got_oldpass = 1 if $return_uid && $id == $return_uid;
+	}
+
+	slashDisplay('changePasswd', {
+		useredit 		=> $user_edit,
+		admin_flag		=> $suadmin_flag,
+		title			=> $title,
+		session 		=> $session_select,
+		clocation 		=> $clocation_select,
+		admin_block		=> $admin_block,
+		got_oldpass		=> $got_oldpass
+	});
+}
+
+#################################################################
+sub editUser {
+	my($hr) = @_;
+	my $id = $hr->{uid} || '';
+	my $note = $hr->{note} || '';
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $hr->{tab_selected_1} || "",
+	});
+
+	my $form = getCurrentForm();
+	my $slashdb = getCurrentDB();
+	my $user = getCurrentUser();
+	my $constants = getCurrentStatic();
+	my $plugins = $slashdb->getDescriptions('plugins');
+
+	my $user_edit = {};
+	my($admin_block, $title);
+	my $admin_flag = ($user->{is_admin}) ? 1 : 0;
+	my $fieldkey;
+
+	if ($admin_flag && $form->{userfield}) {
+		$id ||= $form->{userfield};
+		if ($form->{userfield} =~ /^\d+$/) {
+			$user_edit = $slashdb->getUser($id);
+			$fieldkey = 'uid';
+		} else {
+			$user_edit = $slashdb->getUser($slashdb->getUserUID($id));
+			$fieldkey = 'nickname';
+		}
+	} else {
+		$user_edit = $id eq '' ? $user : $slashdb->getUser($id);
+		$fieldkey = 'uid';
+		$id = $user_edit->{uid};
+	}
+	return if isAnon($user_edit->{uid}) && ! $admin_flag;
+
+	$admin_block = getUserAdmin($id, $fieldkey, 1) if $admin_flag;
+	$user_edit->{homepage} ||= "http://";
+
+	# Remove domain tags, they'll be added back in, in saveUser.
+	for my $dat (@{$user_edit}{qw(sig bio)}) {
+		$dat = parseDomainTags($dat, 0, 1);
+	}
+
+	$title = getTitle('editUser_title', { user_edit => $user_edit});
+
+	my $editkey = "";
+	$editkey = editKey($user_edit->{uid}) if $fieldkey eq 'uid' && $plugins->{PubKey};
+
+	slashDisplay('editUser', {
+		useredit 		=> $user_edit,
+		admin_flag		=> $admin_flag,
+		title			=> $title,
+		editkey 		=> $editkey,
+		admin_block		=> $admin_block,
+		note			=> $note,
+	});
+}
+
+#################################################################
+sub editHome {
+	my($hr) = @_;
+	my $id = $hr->{uid} || '';
+	my $note = $hr->{note} || '';
+
+	my $slashdb = getCurrentDB();
+	my $form = getCurrentForm();
+	my $user = getCurrentUser();
+	my $constants = getCurrentStatic();
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $hr->{tab_selected_1} || "",
+	});
+
+	my($formats, $title, $tzformat_select);
+	my $user_edit = {};
+	my $fieldkey;
+
+	my $admin_flag = ($user->{is_admin}) ? 1 : 0;
+	my $admin_block = '';
+
+	if ($admin_flag && $form->{userfield}) {
+		$id ||= $form->{userfield};
+		if ($form->{userfield} =~ /^\d+$/) {
+			$user_edit = $slashdb->getUser($id);
+			$fieldkey = 'uid';
+		} else {
+			$user_edit = $slashdb->getUser($slashdb->getUserUID($id));
+			$fieldkey = 'nickname';
+		}
+	} else {
+		$user_edit = $id eq '' ? $user : $slashdb->getUser($id);
+		$fieldkey = 'uid';
+	}
+#use Data::Dumper; $Data::Dumper::Sortkeys = 1; print STDERR scalar(localtime) . " user_edit: " . Dumper($user_edit);
+
+	return if isAnon($user_edit->{uid}) && ! $admin_flag;
+	$admin_block = getUserAdmin($id, $fieldkey, 1) if $admin_flag;
+
+	$title = getTitle('editHome_title');
+
+	return if $user->{seclev} < 100 && isAnon($user_edit->{uid});
+
+	$formats = $slashdb->getDescriptions('dateformats');
+	$tzformat_select = createSelect('tzformat', $formats, $user_edit->{dfid}, 1);
+
+	my $lb_check = $user_edit->{lowbandwidth} ? $constants->{markup_checked_attribute} : '';
+	my $sd_check = $user_edit->{simpledesign} ? $constants->{markup_checked_attribute} : '';
+	my $i_check = $user_edit->{noicons}	? $constants->{markup_checked_attribute} : '';
+	my $w_check = $user_edit->{willing}	? $constants->{markup_checked_attribute} : '';
+
+	my $tilde_ed = tildeEd($user_edit);
+
+	slashDisplay('editHome', {
+		title			=> $title,
+		admin_block		=> $admin_block,
+		user_edit		=> $user_edit,
+		tzformat_select		=> $tzformat_select,
+		i_check			=> $i_check,
+		w_check			=> $w_check,
+		lb_check		=> $lb_check,
+		sd_check		=> $sd_check,
+		tilde_ed		=> $tilde_ed,
+		note			=> $note,
+	});
+}
+
+#################################################################
+sub editComm {
+	my($hr) = @_;
+	my $id = $hr->{uid} || '';
+	my $note = $hr->{note} || '';
+
+	my $slashdb = getCurrentDB();
+	my $form = getCurrentForm();
+	my $user = getCurrentUser();
+	my $constants = getCurrentStatic();
+	my $user_edit = {};
+	my($formats, $commentmodes_select, $commentsort_select, $title,
+		$uthreshold_select, $highlightthresh_select, $posttype_select,
+		$bytelimit_select);
+
+	my $admin_block = '';
+	my $fieldkey;
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $hr->{tab_selected_1} || "",
+	});
+
+	my $admin_flag = $user->{is_admin} ? 1 : 0;
+
+	if ($admin_flag && $form->{userfield}) {
+		$id ||= $form->{userfield};
+		if ($form->{userfield} =~ /^\d+$/) {
+			$user_edit = $slashdb->getUser($id);
+			$fieldkey = 'uid';
+		} else {
+			$user_edit = $slashdb->getUser($slashdb->getUserUID($id));
+			$fieldkey = 'nickname';
+		}
+	} else {
+		$user_edit = $id eq '' ? $user : $slashdb->getUser($id);
+		$fieldkey = 'uid';
+	}
+
+	my $hi = $constants->{comment_maxscore} - $constants->{comment_minscore};
+	my $lo = -$hi;
+	my @range = map { $_ > 0 ? "+$_" : $_ } ($lo .. $hi);
+
+	my @reasons = ( );
+	my %reason_select = ( );
+	if ($constants->{m1}) {
+		my $mod_reader = getObject("Slash::$constants->{m1_pluginname}", { db_type => 'reader' });
+		my $reasons = $mod_reader->getReasons();
+		for my $id (sort { $a <=> $b } keys %$reasons) {
+			push @reasons, $reasons->{$id}{name};
+		}
+		# Reason modifiers
+		for my $reason_name (@reasons) {
+			my $key = "reason_alter_$reason_name";
+			$reason_select{$reason_name} = createSelect(
+				$key, \@range, 
+				$user_edit->{$key} || 0, 1, 1
+			);
+		}
+	}
+
+	# Zoo relation modifiers
+	my %people_select;
+	my @people =  qw(friend foe anonymous fof eof freak fan);
+	for (@people) {
+		my $key = "people_bonus_$_";
+		$people_select{$_} = createSelect($key, \@range, 
+			$user_edit->{$key} || 0, 1, 1
+		);
+	}
+
+	# New-user modifier
+	my $new_user_bonus_select = createSelect('new_user_bonus', \@range, 
+			$user_edit->{new_user_bonus} || 0, 1, 1);
+	my $new_user_percent_select = createSelect('new_user_percent',
+			[( 1..15, 20, 25, 30, 35, 40, 45, 50, 55,
+			  60, 65, 70, 75, 80, 85, 90, 95 )], 
+			$user_edit->{new_user_percent} || 100, 1, 1);
+	# Karma modifier
+	my $karma_bonus = createSelect('karma_bonus', \@range, 
+			$user_edit->{karma_bonus} || 0, 1, 1);
+	# Subscriber modifier
+	my $subscriber_bonus = createSelect('subscriber_bonus', \@range, 
+			$user_edit->{subscriber_bonus} || 0, 1, 1);
+
+	# Length modifier
+	my $small_length_bonus_select = createSelect('clsmall_bonus', \@range, 
+			$user_edit->{clsmall_bonus} || 0, 1, 1);
+	my $long_length_bonus_select = createSelect('clbig_bonus', \@range, 
+			$user_edit->{clbig_bonus} || 0, 1, 1);
+
+	return if isAnon($user_edit->{uid}) && ! $admin_flag;
+	$admin_block = getUserAdmin($id, $fieldkey, 1) if $admin_flag;
+
+	$title = getTitle('editComm_title');
+
+	$formats = $slashdb->getDescriptions('commentmodes');
+	$commentmodes_select=createSelect('umode', $formats, $user_edit->{mode}, 1);
+
+	$formats = $slashdb->getDescriptions('sortcodes');
+	$commentsort_select = createSelect(
+		'commentsort', $formats, $user_edit->{commentsort}, 1
+	);
+
+	$formats = $slashdb->getDescriptions('threshcodes');
+	$uthreshold_select = createSelect(
+		'uthreshold', $formats, $user_edit->{threshold}, 1
+	);
+
+	$formats = $slashdb->getDescriptions('threshcodes');
+	$highlightthresh_select = createSelect(
+		'highlightthresh', $formats, $user_edit->{highlightthresh}, 1
+	);
+
+	$user_edit->{bytelimit} = $constants->{defaultbytelimit}
+		if $user_edit->{bytelimit} < 0 || $user_edit->{bytelimit} > 7;
+	my $bytelimit_desc = $user_edit->{is_subscriber} ? 'bytelimit' : 'bytelimit_sub';
+	$formats = $slashdb->getDescriptions($bytelimit_desc);
+	$bytelimit_select = createSelect(
+		'bytelimit', $formats, $user_edit->{bytelimit}, 1
+	);
+
+	my $h_check  = $user_edit->{hardthresh}		 ? $constants->{markup_checked_attribute} : '';
+	my $r_check  = $user_edit->{reparent}		 ? $constants->{markup_checked_attribute} : '';
+	my $n_check  = $user_edit->{noscores}		 ? $constants->{markup_checked_attribute} : '';
+	my $s_check  = $user_edit->{nosigs}		 ? $constants->{markup_checked_attribute} : '';
+	my $b_check  = $user_edit->{nobonus}		 ? $constants->{markup_checked_attribute} : '';
+	my $sb_check = $user_edit->{nosubscriberbonus}	 ? $constants->{markup_checked_attribute} : '';
+	my $p_check  = $user_edit->{postanon}		 ? $constants->{markup_checked_attribute} : '';
+	my $nospell_check = $user_edit->{no_spell}	 ? $constants->{markup_checked_attribute} : '';
+	my $s_mod_check = $user_edit->{mod_with_comm}	 ? $constants->{markup_checked_attribute} : '';
+	my $s_m2_check = $user_edit->{m2_with_mod}	 ? $constants->{markup_checked_attribute} : '';
+	my $s_m2c_check = $user_edit->{m2_with_comm_mod} ? $constants->{markup_checked_attribute} : '';
+
+	$formats = $slashdb->getDescriptions('postmodes');
+	$posttype_select = createSelect(
+		'posttype', $formats, $user_edit->{posttype}, 1
+	);
+
+	slashDisplay('editComm', {
+		title			=> $title,
+		admin_block		=> $admin_block,
+		user_edit		=> $user_edit,
+		h_check			=> $h_check,
+		r_check			=> $r_check,
+		n_check			=> $n_check,
+		s_check			=> $s_check,
+		b_check			=> $b_check,
+		sb_check		=> $sb_check,
+		p_check			=> $p_check,
+		s_mod_check		=> $s_mod_check,
+		s_m2_check		=> $s_m2_check,
+		s_m2c_check		=> $s_m2c_check,
+		nospell_check		=> $nospell_check,
+		commentmodes_select	=> $commentmodes_select,
+		commentsort_select	=> $commentsort_select,
+		highlightthresh_select	=> $highlightthresh_select,
+		uthreshold_select	=> $uthreshold_select,
+		posttype_select		=> $posttype_select,
+		reasons			=> \@reasons,
+		reason_select		=> \%reason_select,
+		people			=> \@people,
+		people_select		=> \%people_select,
+		new_user_percent_select	=> $new_user_percent_select,
+		new_user_bonus_select	=> $new_user_bonus_select,
+		note			=> $note,
+		karma_bonus		=> $karma_bonus,
+		subscriber_bonus	=> $subscriber_bonus,
+		small_length_bonus_select => $small_length_bonus_select,
+		long_length_bonus_select => $long_length_bonus_select,
+		bytelimit_select	=> $bytelimit_select,
+	});
+}
+
+#################################################################
+sub saveUserAdmin {
+	my($hr) = @_;
+	my $slashdb = getCurrentDB();
+	my $form = getCurrentForm();
+	my $user = getCurrentUser();
+	my $constants = getCurrentStatic();
+
+	my($user_edits_table, $user_edit) = ({}, {});
+	my $author_flag;
+	my $note = '';
+	my $srcid;
+	my $id;
+	my $user_editfield_flag;
+	my $banned = 0;
+	my $banref;
+	if ($form->{uid}) {
+		$user_editfield_flag = 'uid';
+		$id = $form->{uid};
+		$user_edit = $slashdb->getUser($id);
+		$srcid = $id;
+
+	} elsif ($form->{subnetid}) {
+		$user_editfield_flag = 'subnetid';
+		($id, $user_edit->{subnetid})  = ($form->{subnetid}, $form->{subnetid});
+		$user_edit->{nonuid} = 1;
+		$srcid = convert_srcid(subnetid => $id);
+
+	} elsif ($form->{ipid}) {
+		$user_editfield_flag = 'ipid';
+		($id, $user_edit->{ipid})  = ($form->{ipid}, $form->{ipid});
+		$user_edit->{nonuid} = 1;
+		$srcid = convert_srcid(ipid => $id);
+
+	} elsif ($form->{md5id}) {
+		$user_editfield_flag = 'md5id';
+		my $fieldname = $form->{fieldname} || 'md5id';
+		($id, $user_edit->{$fieldname})
+			= ($form->{md5id}, $form->{md5id});
+		warn "form field md5id specified, no srcid saving possible";
+	
+	} elsif ($form->{srcid}) {
+		$user_editfield_flag = 'srcid';
+		my $fieldname = $form->{fieldname} || 'srcid';
+		($id, $user_edit->{$fieldname})
+			= ($form->{srcid}, $form->{srcid});
+		$srcid = $id;
+
+	} else {
+		# If we were not fed valid data, don't do anything.
+		return ;
+	}
+
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $all_al2types = $reader->getAL2Types;
+	my $al2_change = { };
+
+	# First, get a hash of what aclams (AL2's and ACLs) this user
+	# had when the previous admin page was loaded.
+	# XXXSRCID Right now acl_old is just calculated for debugging
+	# printing purposes, not actually used.
+	my @al2_old = ( );
+	@al2_old =
+		@{ $form->{al2_old_multiple}   } if $form->{al2_old_multiple};
+	my %al2_old = ( map { ($_, 1) } @al2_old );
+	my @acl_old = ( );
+	@acl_old =
+		@{ $form->{acl_old_multiple}   } if $form->{acl_old_multiple};
+	my %acl_old = ( map { ($_, 1) } @acl_old );
+
+	# Next, get the list of the new data submitted.  Separate
+	# it out into AL2's which are legitimate.  Anything left
+	# over is an ACL.
+	my @al2_new_formfields = ( );
+	@al2_new_formfields = @{ $form->{aclams_new_multiple} } if $form->{aclams_new_multiple};
+	my @al2_new_submitted = map { s/^aclam_//; $_ } @al2_new_formfields;
+	my @al2_new = grep { exists $all_al2types->{$_} } @al2_new_submitted;
+	my %al2_new = ( map { ($_, 1) } @al2_new );
+	my @acl_new = grep { !$al2_new{$_} } @al2_new_submitted;
+	my %acl_new = ( map { ($_, 1) } @acl_new );
+
+	# Find out what changed for AL2's.
+	for my $al2 (@al2_old, @al2_new) {
+		next if defined($al2_old{$al2}) && defined($al2_new{$al2})
+			&& $al2_old{$al2} == $al2_new{$al2};
+		$al2_change->{$al2} = $al2_new{$al2} ? 1 : 0;
+	}
+#print STDERR "al2_change for '$srcid': " . Dumper($al2_change);
+	# If there's a comment, throw that in.
+	if ($form->{al2_new_comment}) {
+		$al2_change->{comment} = $form->{al2_new_comment};
+	}
+	$al2_change = undef if !keys %$al2_change;
+
+	# Find out what changed for ACL's.
+	my $acl_change = { };
+	for my $acl (@acl_old, @acl_new) {
+		next if $acl_old{$acl} == $acl_new{$acl};
+		$acl_change->{$acl} = $acl_new{$acl} ? 1 : 0;
+	}
+	$acl_change = undef if !keys %$acl_change;
+
+	if ($user->{is_admin} && $srcid) {
+		$slashdb->setAL2($srcid, $al2_change);
+	}
+	if ($user->{is_admin} && ($user_editfield_flag eq 'uid' ||
+		$user_editfield_flag eq 'nickname')) {
+
+		# This admin user cannot assign a seclev higher than he/she
+		# already has.
+		my $seclev = $form->{seclev};
+		$seclev = $user->{seclev} if $seclev > $user->{seclev};
+		$user_edits_table->{seclev} = $seclev;
+
+		$user_edits_table->{section} = $form->{section};
+		$user_edits_table->{author} = $form->{author} ? 1 : 0 ;
+		$user_edits_table->{defaultpoints} = $form->{defaultpoints};
+		$user_edits_table->{tokens} = $form->{tokens};
+		$user_edits_table->{tag_clout} = $form->{tag_clout};
+		$user_edits_table->{m2info} = $form->{m2info};
+		$user_edits_table->{acl} = $acl_change if $acl_change;
+
+		my $author = $slashdb->getAuthor($id);
+		my $was_author = ($author && $author->{author}) ? 1 : 0;
+
+		$slashdb->setUser($id, $user_edits_table);
+
+		$note .= getMessage('saveuseradmin_saveduser', { field => $user_editfield_flag, id => $id });
+
+		if ($was_author xor $user_edits_table->{author}) {
+			# A frequently-asked question for new Slash admins is
+			# why their authors aren't showing up immediately.
+			# Give them some help here with an informative message.
+			$note .= getMessage('saveuseradmin_authorchg', {
+				basedir =>	$slashdb->getDescriptions("site_info")
+					->{base_install_directory},
+				virtuser =>	$slashdb->{virtual_user},
+			});
+					
+		}
+	}
+
+	if (!$user_edit->{nonuid}) {
+		if ($form->{expired} && $form->{expired} eq 'on') {
+#			$slashdb->setExpired($user_edit->{uid});
+
+		} else {
+#			$slashdb->setUnexpired($user_edit->{uid});
+		}
+	}
+
+	my $data = { uid => $id };
+	$data->{note} = $note if defined $note;
+	showInfo($data);
+}
+
+#################################################################
+sub savePasswd {
+	my($hr) = @_;
+	my $note = $hr->{noteref} || undef;
+
+	my $slashdb = getCurrentDB();
+	my $form = getCurrentForm();
+	my $user = getCurrentUser();
+	my $constants = getCurrentStatic();
+
+	my $error_flag = 0;
+	my $user_edit = {};
+	my $uid;
+
+	my $user_edits_table = {};
+
+	if ($user->{is_admin}) {
+		$uid = $form->{uid} || $user->{uid};
+	} else {
+		$uid = ($user->{uid} == $form->{uid}) ? $form->{uid} : $user->{uid};
+	}
+
+	$user_edit = $slashdb->getUser($uid);
+
+	if (!$user_edit->{nickname}) {
+		$$note .= getError('cookie_err', { titlebar => 0 }, 0, 1)
+			if $note;
+		$error_flag++;
+	}
+
+	if ($form->{pass1} ne $form->{pass2}) {
+		$$note .= getError('saveuser_passnomatch_err', { titlebar => 0 }, 0, 1)
+			if $note;
+		$error_flag++;
+	}
+
+	if (!$form->{pass1} || length $form->{pass1} < 6) {
+		$$note .= getError('saveuser_passtooshort_err', { titlebar => 0 }, 0, 1)
+			if $note;
+		$error_flag++;
+	}
+
+	if (!$user->{is_admin}){
+		# not an admin -- check old password before changing passwd
+		my $return_uid = $slashdb->getUserAuthenticate($uid, $form->{oldpass}, 1);
+		if (!$return_uid || $return_uid != $uid) {
+			$$note .= getError('saveuser_badoldpass_err', { titlebar => 0 }, 0, 1) 
+				if $note;
+			$error_flag++;
+
+		}
+	}
+
+	if (! $error_flag) {
+		$user_edits_table->{passwd} = $form->{pass1} if $form->{pass1};
+		$user_edits_table->{session_login} = $form->{session_login};
+		$user_edits_table->{cookie_location} = $form->{cookie_location};
+
+		# changed pass, so delete all logtokens
+		$slashdb->deleteLogToken($form->{uid}, 1);
+
+		if ($user->{admin_clearpass}
+			&& !$user->{state}{admin_clearpass_thisclick}) {
+			# User is an admin who sent their password in the clear
+			# some time ago; now that it's been changed, we'll forget
+			# about that incident, unless this click was sent in the
+			# clear as well.
+			$user_edits_table->{admin_clearpass} = '';
+		}
+
+		getOtherUserParams($user_edits_table);
+		$slashdb->setUser($uid, $user_edits_table) ;
+		$$note .= getMessage('saveuser_passchanged_msg',
+			{ nick => $user_edit->{nickname}, uid => $user_edit->{uid} },
+		0, 1) if $note;
+
+		# only set cookie if user is current user
+		if ($form->{uid} eq $user->{uid}) {
+			$user->{logtoken} = bakeUserCookie($uid, $slashdb->getLogToken($form->{uid}, 1));
+			setCookie('user', $user->{logtoken}, $user_edits_table->{session_login});
+		}
+	}
+
+	return $error_flag;
+}
+
+#################################################################
+sub saveUser {
+	my($hr) = @_;
+	my $slashdb = getCurrentDB();
+	my $form = getCurrentForm();
+	my $user = getCurrentUser();
+	my $constants = getCurrentStatic();
+	my $gSkin = getCurrentSkin();
+	my $plugins = $slashdb->getDescriptions('plugins');
+	my $uid;
+	my $user_editfield_flag;
+
+	$uid = $user->{is_admin} && $form->{uid} ? $form->{uid} : $user->{uid};
+	my $user_edit = $slashdb->getUser($uid);
+
+	my($note, $formname);
+
+	$note .= getMessage('savenickname_msg', {
+		nickname => $user_edit->{nickname},
+	}, 1);
+
+	if (!$user_edit->{nickname}) {
+		$note .= getError('cookie_err', 0, 1);
+	}
+
+	# Check to ensure that if a user is changing his email address, that
+	# it doesn't already exist in the userbase.
+	if ($user_edit->{realemail} ne $form->{realemail}) {
+		if ($slashdb->existsEmail($form->{realemail})) {
+			$note .= getError('emailexists_err', 0, 1);
+			$form->{realemail} = $user_edit->{realemail}; # can't change!
+		}
+	}
+
+	my(%extr, $err_message, %limit);
+	$limit{sig} = 120;  # schema is 200, give an extra buffer for domain tags
+	$limit{bio} = $constants->{users_bio_length} || 1024; # can be up to 2^16
+
+	for my $key (keys %limit) {
+		my $dat = chopEntity($form->{$key}, $limit{$key});
+		$dat = strip_html($dat);
+		$dat = balanceTags($dat, { deep_nesting => 2, length => $limit{$key} });
+		$dat = addDomainTags($dat) if $dat;
+
+		# If the sig becomes too long to fit (domain tagging causes
+		# string expansion and tag balancing can too), warn the user to
+		# use shorter domain names and don't save their change.
+		if ($key eq 'sig' && defined($dat) && length($dat) > 200) {
+			print getError('sig_too_long_err');
+			$extr{sig} = undef;
+		}
+
+		# really, comment filters should ignore short length IMO ... oh well.
+		if (length($dat) > 1 && ! filterOk('comments', 'postersubj', $dat, \$err_message)) {
+			print getError('filter message', {
+				err_message	=> $err_message,
+				item		=> $key,
+			});
+			$extr{$key} = undef;
+		} elsif (! compressOk('comments', 'postersubj', $dat)) {
+			print getError('compress filter', {
+				ratio		=> 'postersubj',
+				item		=> $key,
+			});
+			$extr{$key} = undef;
+		} else {
+			$extr{$key} = $dat;
+		}
+	}
+
+	# We should do some conformance checking on a user's pubkey,
+	# make sure it looks like one of the known types of public
+	# key.  Until then, just make sure it doesn't have HTML.
+	$form->{pubkey} = $plugins->{'PubKey'} ? strip_nohtml($form->{pubkey}, 1) : '';
+
+	my $homepage = $form->{homepage};
+	$homepage = '' if $homepage eq 'http://';
+	$homepage = fudgeurl($homepage);
+	$homepage = URI->new_abs($homepage, $gSkin->{absolutedir})
+			->canonical
+			->as_string if $homepage ne '';
+	$homepage = substr($homepage, 0, 100) if $homepage ne '';
+
+	my $calendar_url = $form->{calendar_url};
+	if (length $calendar_url) {
+		# fudgeurl() doesn't like webcal; will remove later anyway
+		$calendar_url =~ s/^webcal/http/i;
+		$calendar_url = fudgeurl($calendar_url);
+		$calendar_url = URI->new_abs($calendar_url, $gSkin->{absolutedir})
+			->canonical
+			->as_string if $calendar_url ne '';
+
+		$calendar_url =~ s|^http://||i;
+		$calendar_url = substr($calendar_url, 0, 200) if $calendar_url ne '';
+	}
+
+	# for the users table
+	my $user_edits_table = {
+		homepage	=> $homepage,
+		realname	=> $form->{realname},
+		pubkey		=> $form->{pubkey},
+		copy		=> $form->{copy},
+		quote		=> $form->{quote},
+		calendar_url	=> $calendar_url,
+		yahoo		=> $form->{yahoo},
+		jabber		=> $form->{jabber},
+		aim		=> $form->{aim},
+		aimdisplay	=> $form->{aimdisplay},
+		icq		=> $form->{icq},
+		playing		=> $form->{playing},
+                mobile_text_address => $form->{mobile_text_address},
+	};
+
+	for (keys %extr) {
+		$user_edits_table->{$_} = $extr{$_} if defined $extr{$_};
+	}
+
+	# don't want undef, want to be empty string so they
+	# will overwrite the existing record
+	for (keys %$user_edits_table) {
+		$user_edits_table->{$_} = '' unless defined $user_edits_table->{$_};
+	}
+
+	if ($user_edit->{realemail} ne $form->{realemail}) {
+		$user_edits_table->{realemail} =
+			chopEntity($form->{realemail}, 50);
+		my $new_fakeemail = ''; # at emaildisplay 0, don't show any email address
+		if ($user->{emaildisplay}) {
+			$new_fakeemail = getArmoredEmail($uid, $user_edits_table->{realemail})
+				if $user->{emaildisplay} == 1;
+			$new_fakeemail = $user_edits_table->{realemail}
+				if $user->{emaildisplay} == 2;
+		}
+		$user_edits_table->{fakeemail} = $new_fakeemail;
+
+		$note .= getMessage('changeemail_msg', {
+			realemail => $user_edit->{realemail}
+		}, 1);
+
+		my $saveuser_emailtitle = getTitle('saveUser_email_title', {
+			nickname  => $user_edit->{nickname},
+			realemail => $form->{realemail}
+		}, 1);
+		my $saveuser_email_msg = getMessage('saveuser_email_msg', {
+			nickname  => $user_edit->{nickname},
+			realemail => $form->{realemail}
+		}, 1);
+
+		sendEmail($form->{realemail}, $saveuser_emailtitle, $saveuser_email_msg);
+		doEmail($uid, $saveuser_emailtitle, $saveuser_email_msg);
+	}
+
+	getOtherUserParams($user_edits_table);
+	$slashdb->setUser($uid, $user_edits_table);
+
+	editUser({ uid => $uid, note => $note });
+}
+
+
+#################################################################
+sub saveComm {
+	my($hr) = @_;
+	my $slashdb = getCurrentDB();
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $constants = getCurrentStatic();
+	my($uid, $user_fakeemail);
+
+	if ($user->{is_admin}) {
+		$uid = $form->{uid} || $user->{uid};
+	} else {
+		$uid = ($user->{uid} == $form->{uid}) ?
+			$form->{uid} : $user->{uid};
+	}
+
+	# Do the right thing with respect to the chosen email display mode
+	# and the options that can be displayed.
+	my $user_edit = $slashdb->getUser($uid);
+	my $new_fakeemail = '';		# at emaildisplay 0, don't show any email address
+	if ($form->{emaildisplay}) {
+		$new_fakeemail = getArmoredEmail($uid)		if $form->{emaildisplay} == 1;
+		$new_fakeemail = $user_edit->{realemail}	if $form->{emaildisplay} == 2;
+	}
+
+	my $name = $user->{seclev} && $form->{name} ?
+		$form->{name} : $user->{nickname};
+
+	my $note = getMessage('savenickname_msg',
+		{ nickname => $name });
+
+	print getError('cookie_err') if isAnon($uid) || !$name;
+
+	# Take care of the lists
+	# Enforce Ranges for variables that need it
+	$form->{commentlimit} = 0 if $form->{commentlimit} < 1;
+	my $cl_max = $constants->{comment_commentlimit} || 0;
+	$form->{commentlimit} = $cl_max if $cl_max > 0 && $form->{commentlimit} > $cl_max;
+	$form->{commentspill} = 0 if $form->{commentspill} < 1;
+
+	# For some of these values, namely the ones that we happen to
+	# know get stored in users_param, we change them to 'undef'
+	# if they are the default value.  This deletes them from the
+	# users_param table, which has the same effect as storing the
+	# default except it's faster all around.  If we ever change
+	# the schema to promote these fields from params into a
+	# proper users_* table, then this will no longer be correct.
+	# See prepareUser().
+	my $max = $constants->{comment_maxscore} - $constants->{comment_minscore};
+	my $min = -$max;
+	my $karma_bonus = ($form->{karma_bonus} !~ /^[\-+]?\d+$/) ? "+1" : $form->{karma_bonus};
+	my $subscriber_bonus = ($form->{subscriber_bonus} !~ /^[\-+]?\d+$/) ? "+1" : $form->{subscriber_bonus};
+	my $new_user_bonus = ($form->{new_user_bonus} !~ /^[\-+]?\d+$/) ? 0 : $form->{new_user_bonus};
+	my $new_user_percent = (($form->{new_user_percent} <= 100 && $form->{new_user_percent} >= 0) 
+			? $form->{new_user_percent}
+			: 100); 
+	my $clsmall_bonus = ($form->{clsmall_bonus} !~ /^[\-+]?\d+$/) ? 0 : $form->{clsmall_bonus};
+	my $clbig_bonus = ($form->{clbig_bonus} !~ /^[\-+]?\d+$/) ? 0 : $form->{clbig_bonus};
+
+	my $user_edits_table = {
+		discussion2		=> $form->{discussion2} || undef,
+		d2_comment_q		=> $form->{d2_comment_q} || undef,
+		d2_comment_order	=> $form->{d2_comment_order} || undef,
+		clsmall			=> $form->{clsmall},
+		clsmall_bonus		=> ($clsmall_bonus || undef),
+		clbig			=> $form->{clbig},
+		clbig_bonus		=> ($clbig_bonus || undef),
+		commentlimit		=> $form->{commentlimit},
+		bytelimit		=> $form->{bytelimit},
+		commentsort		=> $form->{commentsort},
+		commentspill		=> $form->{commentspill},
+		domaintags		=> ($form->{domaintags} != 2 ? $form->{domaintags} : undef),
+		emaildisplay		=> $form->{emaildisplay} || undef,
+		fakeemail		=> $new_fakeemail,
+		highlightthresh		=> $form->{highlightthresh},
+		maxcommentsize		=> $form->{maxcommentsize},
+		mode			=> $form->{umode},
+		posttype		=> $form->{posttype},
+		threshold		=> $form->{uthreshold},
+		nosigs			=> ($form->{nosigs}     ? 1 : 0),
+		reparent		=> ($form->{reparent}   ? 1 : 0),
+		noscores		=> ($form->{noscores}   ? 1 : 0),
+		hardthresh		=> ($form->{hardthresh} ? 1 : 0),
+		no_spell		=> ($form->{no_spell}   ? 1 : undef),
+		nobonus			=> ($form->{nobonus} ? 1 : undef),
+		nosubscriberbonus	=> ($form->{nosubscriberbonus} ? 1 : undef),
+		postanon		=> ($form->{postanon} ? 1 : undef),
+		new_user_percent	=> ($new_user_percent && $new_user_percent != 100
+						? $new_user_percent : undef),
+		new_user_bonus		=> ($new_user_bonus || undef),
+		karma_bonus		=> ($karma_bonus ne '+1' ? $karma_bonus : undef),
+		subscriber_bonus	=> ($subscriber_bonus || undef),
+		textarea_rows		=> ($form->{textarea_rows} != $constants->{textarea_rows}
+						? $form->{textarea_rows} : undef),
+		textarea_cols		=> ($form->{textarea_cols} != $constants->{textarea_cols}
+						? $form->{textarea_cols} : undef),
+		user_comment_sort_type	=> ($form->{user_comment_sort_type} != 2
+						? $form->{user_comment_sort_type} : undef ),
+		mod_with_comm		=> ($form->{mod_with_comm} ? 1 : undef),
+		m2_with_mod		=> ($form->{m2_with_mod} ? 1 : undef),
+        	m2_with_comm_mod		=> ($form->{m2_with_mod_on_comm} ? 1 : undef),
+
+	};
+	
+	# set our default values for the items where an empty-string won't do 
+	my $defaults = {
+		posttype        => 2,
+		highlightthresh => 4,
+		maxcommentsize  => 4096,
+		reparent        => 1,
+		commentlimit    => 100,
+		commentspill    => 50,
+		mode            => 'thread'
+	};
+
+	my $mod_reader = getObject("Slash::$constants->{m1_pluginname}", { db_type => 'reader' });
+	my @reasons = ( );
+	my $reasons = $mod_reader->getReasons();
+	for my $id (sort { $a <=> $b } keys %$reasons) {
+		push @reasons, $reasons->{$id}{name};
+	}
+
+	for my $reason_name (@reasons) {
+		my $key = "reason_alter_$reason_name";
+		my $answer = $form->{$key} || 0;
+		$answer = 0 if !$answer || $answer !~ /^[\-+]?\d+$/;
+		$user_edits_table->{$key} = ($answer == 0) ? '' : $answer;
+	}
+
+	for (qw| friend foe anonymous fof eof freak fan |) {
+		my $answer = $form->{"people_bonus_$_"};
+		$answer = 0 if $answer !~ /^[\-+]?\d+$/;
+		$user_edits_table->{"people_bonus_$_"} = ($answer == 0) ? '' : $answer;
+	}
+	getOtherUserParams($user_edits_table);
+	setToDefaults($user_edits_table, {}, $defaults) if $form->{restore_defaults};
+	$slashdb->setUser($uid, $user_edits_table);
+
+	editComm({ uid => $uid, note => $note });
+}
+
+#################################################################
+sub saveHome {
+	my($hr) = @_;
+	my $slashdb = getCurrentDB();
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $constants = getCurrentStatic();
+	my($uid, $error);
+
+	if ($user->{is_admin}) {
+		$uid = $form->{uid} || $user->{uid} ;
+	} else {
+		$uid = ($user->{uid} == $form->{uid}) ?
+			$form->{uid} : $user->{uid};
+	}
+	my $edit_user = $slashdb->getUser($uid);
+
+	my $name = $user->{seclev} && $form->{name} ?
+		$form->{name} : $user->{nickname};
+	$name = substr($name, 0, 20);
+
+	my $note = getMessage('savenickname_msg',
+		{ nickname => $name });
+
+	if (isAnon($uid) || !$name) {
+		my $cookiemsg = getError('cookie_err');
+		print $cookiemsg;
+	}
+
+	# Using the existing list of slashboxes and the set of
+	# what's checked and not, build up the new list.
+	# (New arrivals go at the end.)
+	my $slashboxes = $edit_user->{slashboxes};
+	# Only go through all this if the user clicked save,
+	# not "Restore Slashbox Defaults"!
+	my($boxes, $skinBoxes) = $slashdb->getPortalsCommon();
+	my $default_slashboxes_textlist = join ",",
+		@{$skinBoxes->{$constants->{mainpage_skid}}};
+	if (!$form->{restore_slashbox_defaults}) {
+		$slashboxes = $default_slashboxes_textlist if !$slashboxes;
+		my @slashboxes = split /,/, $slashboxes;
+		my %slashboxes = ( );
+		for my $i (0..$#slashboxes) {
+			$slashboxes{$slashboxes[$i]} = $i;
+		}
+		# Add new boxes in.
+		for my $key (sort grep /^showbox_/, keys %$form) {
+			my($bid) = $key =~ /^showbox_(\w+)$/;
+			next if length($bid) < 1 || length($bid) > 30 || $bid !~ /^\w+$/;
+			if (! exists $slashboxes{$bid}) {
+				$slashboxes{$bid} = 999; # put it at the end
+			}
+		}
+		# Remove any boxes that weren't checked.
+		for my $bid (@slashboxes) {
+			delete $slashboxes{$bid} unless $form->{"showbox_$bid"};
+		}
+		@slashboxes = sort {
+			$slashboxes{$a} <=> $slashboxes{$b}
+			||
+			$a cmp $b
+		} keys %slashboxes;
+		# This probably should be a var (and appear in tilded_customize_msg)
+		$#slashboxes = 19 if $#slashboxes > 19;
+		$slashboxes = join ",", @slashboxes;
+	}
+	# If we're right back to the default, that means the
+	# empty string.
+	if ($slashboxes eq $default_slashboxes_textlist) {
+		$slashboxes = "";
+	}
+
+	# Set the story_never and story_always fields.
+	my $author_hr = $slashdb->getDescriptions('authors');
+	my $tree = $slashdb->getTopicTree();
+	my(@story_never_topic,  @story_never_author,  @story_never_nexus);
+	my(@story_always_topic, @story_always_author);
+	my(@story_always_nexus, @story_full_brief_nexus, @story_brief_always_nexus, @story_full_best_nexus, @story_brief_best_nexus);
+	my($story_topic_all, $story_author_all, $story_nexus_all) = (0, 0, 0);
+	
+	# Topics are either present (value=2) or absent (value=0).  If absent,
+	# push them onto the never list.  Otherwise, do nothing.  (There's no
+	# way to have an "always" topic, at the moment.)  If the hidden
+	# field topictids_present is false, then there are no topic tids,
+	# skip this.
+	if ($form->{topictids_present}) {
+		for my $tid (
+			sort { $a <=> $b }
+			grep { !$tree->{$_}{nexus} }
+			keys %$tree
+		) {
+			my $key = "topictid$tid";
+			$story_topic_all++;
+			if (!$form->{$key}) {		push @story_never_topic, $tid	}
+		}
+	}
+	# Authors are either present (value=2) or absent (value=0).  If
+	# absent, push them onto the never list.  Otherwise, do nothing.
+	# (There's no way to have an "always" author, at the moment.)
+	for my $aid (sort { $a <=> $b } keys %$author_hr) {
+		my $key = "aid$aid";
+		$story_author_all++;
+		if (!$form->{$key}) {			push @story_never_author, $aid	}
+	}
+	# Nexuses can have value 0, 1, 2, 3, 4, 5.  
+	# 0 means the never list,
+	# 1 means brief view of mainpage articles only
+	# 2 means full view of mainpage articles only
+	# 3 means brief view of all content
+	# 4 means full view of mainpage content, brief view of sectional
+	# 5 means full view of all content
+	for my $tid (
+		sort { $a <=> $b }
+		map { /^nexustid(\d+)$/; $1 }
+		grep { /^nexustid\d+$/ }
+		keys %$form
+	) {
+		my $key = "nexustid$tid";
+		next unless $tid && $tree->{$tid} && $tree->{$tid}{nexus};
+		$story_nexus_all++;
+		   if (!$form->{$key}) {		push @story_never_nexus, $tid	}
+		elsif ($form->{$key} == 5 ) {		push @story_always_nexus, $tid	}
+		elsif ($form->{$key} == 4 ) {		push @story_full_brief_nexus, $tid }
+		elsif ($form->{$key} == 3 ) {		push @story_brief_always_nexus, $tid }
+		elsif ($form->{$key} == 2 ) {		push @story_full_best_nexus, $tid }
+		elsif ($form->{$key} == 1 ) {		push @story_brief_best_nexus, $tid }
+		
+						
+	}
+#use Data::Dumper; $Data::Dumper::Sortkeys = 1; print STDERR scalar(localtime) . " s_n_t '@story_never_topic' s_n_a '@story_never_author' s_n_n '@story_never_nexus' s_a_n '@story_always_nexus' form: " . Dumper($form);
+	# Sanity check.
+	$#story_never_topic		= 299 if $#story_never_topic   > 299;
+	$#story_never_author		= 299 if $#story_never_author  > 299;
+	$#story_never_nexus 		= 299 if $#story_never_nexus   > 299;
+	$#story_always_topic		= 299 if $#story_always_topic  > 299;
+	$#story_always_author 		= 299 if $#story_always_author > 299;
+	$#story_always_nexus 		= 299 if $#story_always_nexus  > 299;
+	$#story_full_brief_nexus 	= 299 if $#story_full_brief_nexus > 299;
+	$#story_brief_always_nexus	= 299 if $#story_brief_always_nexus > 299;
+	$#story_brief_best_nexus 	= 299 if $#story_brief_best_nexus > 299;
+	$#story_full_best_nexus		= 299 if $#story_full_best_nexus > 299;
+	
+	my $story_never_topic   = join ",", @story_never_topic;
+	$story_never_topic = ($constants->{subscribe} && $user->{is_subscriber})
+		? checkList($story_never_topic, 1024)
+		: checkList($story_never_topic);
+	my $story_never_author  	= checkList(join ",", @story_never_author);
+	my $story_never_nexus   	= checkList(join ",", @story_never_nexus);
+	my $story_always_topic  	= checkList(join ",", @story_always_topic);
+	$story_always_topic = ($constants->{subscribe} && $user->{is_subscriber})
+		? checkList($story_always_topic, 1024)
+		: checkList($story_always_topic);
+	my $story_always_author 	= checkList(join ",", @story_always_author);
+
+	my $story_always_nexus  	= checkList(join ",", @story_always_nexus);
+	my $story_full_brief_nexus	= checkList(join ",", @story_full_brief_nexus);
+	my $story_brief_always_nexus    = checkList(join ",", @story_brief_always_nexus);
+	my $story_brief_best_nexus	= checkList(join ",", @story_brief_best_nexus);
+	my $story_full_best_nexus	= checkList(join ",", @story_full_best_nexus);
+	
+
+	my $user_edits_table = {
+		story_never_topic		=> $story_never_topic,
+		story_never_author		=> $story_never_author,
+		story_never_nexus		=> $story_never_nexus,
+		story_always_topic		=> $story_always_topic,
+		story_always_author		=> $story_always_author,
+		story_always_nexus		=> $story_always_nexus,
+		story_brief_always_nexus	=> $story_brief_always_nexus,
+		story_full_brief_nexus 		=> $story_full_brief_nexus,
+		story_full_best_nexus		=> $story_full_best_nexus,
+		story_brief_best_nexus		=> $story_brief_best_nexus,
+
+		slashboxes	=> checkList($slashboxes, 1024),
+
+		maxstories	=> 30, # XXXSKIN fix this later
+		noboxes		=> ($form->{useslashboxes} ? 0 : 1),
+		lowbandwidth	=> ($form->{lowbandwidth} ? 1 : 0),
+		simpledesign	=> ($form->{simpledesign} ? 1 : 0),
+		noicons		=> ($form->{noicons} ? 1 : 0),
+		willing		=> ($form->{willing} ? 1 : 0),
+	};
+
+	if (defined $form->{tzcode} && defined $form->{tzformat}) {
+		$user_edits_table->{tzcode} = $form->{tzcode};
+		$user_edits_table->{dfid}   = $form->{tzformat};
+		$user_edits_table->{dst}    = $form->{dst};
+	}
+
+	# Force the User Space area to contain only known-good HTML tags.
+	# Unfortunately the cookie login model makes it just too risky
+	# to allow scripts in here;  CSS's steal passwords.  There are
+	# no known vulnerabilities at this time, but a combination of the
+	# social engineering taking place (inviting users to put Javascript
+	# from websites in here, and making available script URLs for that
+	# purpose), plus the fact that this could be used to amplify the
+	# seriousness of any future vulnerabilities, means it's way past
+	# time to shut this feature down.  - Jamie 2002/03/06
+
+	# it's a VARCHAR ...
+	my $mylinks_limit = 255;
+	$user_edits_table->{mylinks} = balanceTags(strip_html(
+		chopEntity($form->{mylinks} || '', $mylinks_limit)
+	), { deep_nesting => 2, length => $mylinks_limit });
+
+	$user_edits_table->{mylinks} = '' unless defined $user_edits_table->{mylinks};
+
+	$error = 1;
+	# must select at least 1/4 of nexuses, topics, authors
+	if      ( scalar(@story_never_author) > ($story_author_all * 3/4) ) {
+		$note = getError('editHome_too_many_disabled');
+	} elsif ( scalar(@story_never_nexus) > ($story_nexus_all * 3/4) ) {
+		$note = getError('editHome_too_many_disabled');
+	} elsif ( scalar(@story_never_topic) > ($story_topic_all * 3/4) ) {
+		$note = getError('editHome_too_many_disabled');
+	} else {
+		$error = 0;
+	}
+
+	unless ($error) {
+		# If a user is unwilling to moderate, we should cancel all points, lest
+		# they be preserved when they shouldn't be.
+		if (!isAnon($uid) && !$form->{willing}) {
+			$slashdb->setUser($uid, { points => 0 });
+		}
+
+		getOtherUserParams($user_edits_table);
+		if ($form->{restore_defaults}) {
+			setToDefaults($user_edits_table, {}, {
+				maxstories	=> 30,
+				tzcode		=> "EST",
+				# XXX shouldn't this reset ALL the defaults,
+				# not just these two?
+			});
+		}
+		if ($form->{restore_slashbox_defaults}) {
+			setToDefaults($user_edits_table, {
+				'story_never_topic' => 1,
+				'story_never_author' => 1,
+				'story_never_nexus' => 1,
+				'story_always_topic' => 1,
+				'story_always_author' => 1,
+				'story_always_nexus' => 1,
+				'story_full_brief_nexus' => 1,
+				'story_brief_always_nexus' => 1,
+				'story_full_best_nexus' => 1,
+				'story_brief_best_nexus' => 1,
+				'maxstories' => 1,
+				'noboxes' => 1,
+				'light' => 1,
+				'noicons' => 1,
+				'willing' => 1
+			}, { slashboxes => "" });
+	}
+
+#print scalar(localtime) . " uet: " . Dumper($user_edits_table);
+		$slashdb->setUser($uid, $user_edits_table);
+	}
+
+	editHome({ uid => $uid, note => $note });
+}
+
+#################################################################
+# A generic way for a site to allow users to edit data about themselves.
+# Most useful when your plugin or theme wants to let the user change
+# minor settings but you don't want to write a whole new version
+# of users.pl to provide a user interface.  The user can save any
+# param of the format "opt_foo", as long as "foo" shows up in
+# getMiscUserOpts which lists all the misc opts that this user can edit.
+# This is *not* protected by formkeys (yet), so assume attackers can make
+# users click and accidentally edit their own settings: no really important
+# data should be stored in this way.
+sub editMiscOpts {
+	my($hr) = @_;
+	my $slashdb = getCurrentDB();
+	my $user = getCurrentUser(); 
+	my $constants = getCurrentStatic();
+	my $note = $hr->{note} || "";
+
+	return if $user->{is_anon}; # shouldn't be, but can't hurt to check
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $hr->{tab_selected_1} || "",
+	});
+
+	my $edit_user = $slashdb->getUser($user->{uid});
+	my $title = getTitle('editMiscOpts_title');
+
+	my $opts = $slashdb->getMiscUserOpts();
+	for my $opt (@$opts) {
+		my $opt_name = "opt_" . $opt->{name};
+		$opt->{checked} = $edit_user->{$opt_name} ? 1 : 0;
+	}
+
+	slashDisplay('editMiscOpts', {
+#		useredit	=> $user_edit,
+		title		=> $title,
+		opts		=> $opts,
+		note		=> $note,
+	});
+}
+
+#################################################################
+#
+sub saveMiscOpts {
+	my($hr) = @_;
+	my $slashdb = getCurrentDB();
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $constants = getCurrentStatic();
+
+	return if $user->{is_anon}; # shouldn't be, but can't hurt to check
+
+	my $edit_user = $slashdb->getUser($user->{uid});
+	my %opts_ok_hash = ( );
+	my $opts = $slashdb->getMiscUserOpts();
+	for my $opt (@$opts) {
+		$opts_ok_hash{"opt_$opt->{name}"} = 1;
+	}
+
+	my $update = { };
+	for my $opt (grep /^opt_/, keys %$form) {
+		next unless $opts_ok_hash{$opt};
+		$update->{$opt} = $form->{$opt} ? 1 : 0;
+	}
+
+	# Make the changes.
+	$slashdb->setUser($edit_user->{uid}, $update);
+
+	# Inform the user the change was made.  Since we don't
+	# require formkeys, we always want to print a message to
+	# make sure the user sees what s/he did.  This is done
+	# by passing in a note which ends up passed to the
+	# editMiscOpts template, which displays it.
+	editMiscOpts({ note => getMessage('savemiscopts_msg') });
+}
+
+#################################################################
+sub listReadOnly {
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+
+	my $readonlylist = $reader->getAL2List('nopost');
+
+	slashDisplay('listReadOnly', {
+		readonlylist => $readonlylist,
+	});
+
+}
+
+#################################################################
+sub listBanned {
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+
+	my $bannedlist = $reader->getAL2List('ban');
+
+	slashDisplay('listBanned', {
+		bannedlist => $bannedlist,
+	});
+
+}
+
+#################################################################
+sub topAbusers {
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+
+	my $topabusers = $reader->getTopAbusers();
+
+	slashDisplay('topAbusers', {
+		topabusers => $topabusers,
+	});
+}
+
+#################################################################
+sub listAbuses {
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $constants = getCurrentStatic();
+
+	my $abuses = $reader->getAbuses($form->{key}, $form->{abuseid});
+
+	slashDisplay('listAbuses', {
+		abuseid	=> $form->{abuseid},
+		abuses	=> $abuses,
+	});
+}
+
+#################################################################
+sub forceAccountVerify {
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $slashdb = getCurrentDB();
+	my $constants = getCurrentStatic();
+
+	my $uid = $form->{uid};
+	my $useredit = $slashdb->getUser($uid);
+	
+	if ($useredit->{uid}) {
+		my $newpasswd = $slashdb->resetUserAccount($uid);
+		$slashdb->deleteLogToken($uid, 1);
+		my $emailtitle = getTitle('reset_acct_email_title', {
+			nickname	=> $useredit->{nickname}
+		}, 1);
+
+		my $msg = getMessage('reset_acct_msg', {
+			newpasswd	=> $newpasswd,
+			tempnick	=> $useredit->{nickname},
+		}, 1);
+		
+		$slashdb->setUser($useredit->{uid}, {
+			waiting_for_account_verify => 1,
+			account_verify_request_time => $slashdb->getTime()
+		});
+		
+		doEmail($useredit->{uid}, $emailtitle, $msg) if $useredit->{uid};
+	}
+	
+	print getMessage("reset_acct_complete", { useredit => $useredit }, 1);	
+}
+
+#################################################################
+sub displayForm {
+	my($hr) = @_;
+
+	my $user = getCurrentUser();
+	my $form = getCurrentForm();
+	my $slashdb = getCurrentDB();
+	my $constants = getCurrentStatic();
+
+	my $suadmin_flag = $user->{seclev} >= 10000 ? 1 : 0;
+
+	print createMenu("users", {
+		style		=> 'tabbed',
+		justify		=> 'right',
+		color		=> 'colored',
+		tab_selected	=> $hr->{tab_selected_1} || "",
+	});
+
+	my $op = $hr->{op} || $form->{op} || 'displayform';
+
+	my $ops = {
+		displayform 	=> 'loginForm',
+		edithome	=> 'loginForm',
+		editcomm	=> 'loginForm',
+		edituser	=> 'loginForm',
+#		mailpasswdform 	=> 'sendPasswdForm',
+#		newuserform	=> 'newUserForm',
+		userclose	=> 'loginForm',
+		userlogin	=> 'loginForm',
+		editmiscopts	=> 'loginForm',
+		savemiscopts	=> 'loginForm',
+		default		=> 'loginForm'
+	};
+
+	$op = 'default' if !defined($ops->{$op});
+
+	my($title, $title2, $msg1, $msg2) = ('', '', '', '');
+
+	if ($op eq 'userclose') {
+		$title = getMessage('userclose');
+
+	} elsif ($op eq 'displayForm') {
+		$title = $form->{unickname}
+			? getTitle('displayForm_err_title')
+			: getTitle('displayForm_title');
+	} elsif ($op eq 'mailpasswdform') {
+		$title = getTitle('mailPasswdForm_title');
+	} elsif ($op eq 'newuserform') {
+		$title = getTitle('newUserForm_title');
+	} else {
+		$title = getTitle('displayForm_title');
+	}
+
+	$form->{unickname} ||= $form->{newusernick};
+
+	if ($form->{newusernick}) {
+		$title2 = getTitle('displayForm_dup_title');
+	} else {
+		$title2 = getTitle('displayForm_new_title');
+	}
+
+	$msg1 = getMessage('dispform_new_msg_1');
+	if (! $form->{newusernick} && $op eq 'newuserform') {
+		$msg2 = getMessage('dispform_new_msg_2');
+	} elsif ($op eq 'displayform' || $op eq 'userlogin') {
+		$msg2 = getMessage('newuserform_msg');
+	}
+
+	slashDisplay($ops->{$op}, {
+		newnick		=> nickFix($form->{newusernick}),
+		suadmin_flag 	=> $suadmin_flag,
+		title 		=> $title,
+		title2 		=> $title2,
+		logged_in	=> $user->{is_anon} ? 0 : 1,
+		msg1 		=> $msg1,
+		msg2 		=> $msg2
+	});
+}
+
+#################################################################
+# this groups all the messages together in
+# one template, called "messages;users;default"
+sub getMessage {
+	my($value, $hashref, $nocomm) = @_;
+	$hashref ||= {};
+	$hashref->{value} = $value;
+	return slashDisplay('messages', $hashref,
+		{ Return => 1, Nocomm => $nocomm });
+}
+
+#################################################################
+# this groups all the errors together in
+# one template, called "errors;users;default"
+sub getError {
+	my($value, $hashref, $nocomm) = @_;
+	$hashref ||= {};
+	$hashref->{value} = $value;
+	return slashDisplay('errors', $hashref,
+		{ Return => 1, Nocomm => $nocomm });
+}
+
+#################################################################
+# this groups all the titles together in
+# one template, called "users-titles"
+sub getTitle {
+	my($value, $hashref, $nocomm) = @_;
+	$hashref ||= {};
+	$hashref->{value} = $value;
+	return slashDisplay('titles', $hashref,
+		{ Return => 1, Nocomm => $nocomm });
+}
+
+#################################################################
+# getUserAdmin - returns a block of HTML text that provides
+# information and editing capabilities for admin users.
+# Most of this data is already in the getUserAdmin template,
+# but really, we should try to get more of this logic into
+# that template.
+sub getUserAdmin {
+	my($id, $field, $seclev_field) = @_;
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $logdb = getObject('Slash::DB', { db_type => 'log_slave' });
+
+
+	my $user	= getCurrentUser();
+	my $form	= getCurrentForm();
+	my $constants	= getCurrentStatic();
+	my $slashdb	= getCurrentDB();
+	$id ||= $user->{uid};
+
+	my($expired, $uidstruct, $readonly);
+	my($user_edit, $user_editfield, $ipstruct, $ipstruct_order, $authors, $author_flag, $topabusers, $thresh_select,$section_select);
+	my $srcid;
+	my $proxy_check = {};
+	my @accesshits;
+	my $user_editinfo_flag = (!$form->{op} || $form->{op} eq 'userinfo'
+		|| $form->{userinfo} || $form->{saveuseradmin}
+		) ? 1 : 0;
+	my $authoredit_flag = ($user->{seclev} >= 10000) ? 1 : 0;
+	my $sectionref = $reader->getDescriptions('skins');
+	$sectionref->{''} = getData('all_sections');
+
+	$field ||= 'uid';
+	if ($field eq 'uid') {
+		$user_edit = $slashdb->getUser($id);
+		$user_editfield = $user_edit->{uid};
+		$srcid = convert_srcid( uid => $id );
+		#$expired = $slashdb->checkExpired($user_edit->{uid}) ? $constants->{markup_checked_attribute} : '';
+		$ipstruct = $slashdb->getNetIDStruct($user_edit->{uid});
+		@accesshits = $logdb->countAccessLogHitsInLastX($field, $user_edit->{uid}) if defined($logdb);
+		$section_select = createSelect('section', $sectionref, $user_edit->{section}, 1);
+
+	} elsif ($field eq 'nickname') {
+		$user_edit = $slashdb->getUser($slashdb->getUserUID($id));
+		$user_editfield = $user_edit->{nickname};
+		#$expired = $slashdb->checkExpired($user_edit->{uid}) ? $constants->{markup_checked_attribute} : '';
+		$ipstruct = $slashdb->getNetIDStruct($user_edit->{uid});
+		@accesshits = $logdb->countAccessLogHitsInLastX('uid', $user_edit->{uid}) if defined($logdb);
+		$section_select = createSelect('section', $sectionref, $user_edit->{section}, 1);
+
+	} elsif ($field eq 'md5id') {
+		$user_edit->{nonuid} = 1;
+		$user_edit->{md5id} = $id;
+		if ($form->{fieldname} && $form->{fieldname} =~ /^(ipid|subnetid)$/) {
+			$uidstruct = $slashdb->getUIDStruct($form->{fieldname}, $user_edit->{md5id});
+			@accesshits = $logdb->countAccessLogHitsInLastX($form->{fieldname}, $user_edit->{md5id}) if defined($logdb);
+		} else {
+			$uidstruct = $slashdb->getUIDStruct('md5id', $user_edit->{md5id});
+			@accesshits = $logdb->countAccessLogHitsInLastX($field, $user_edit->{md5id}) if defined($logdb);
+		}
+
+	} elsif ($field eq 'ipid') {
+		$user_edit->{nonuid} = 1;
+		$user_edit->{ipid} = $id;
+		$srcid = convert_srcid( ipid => $id );
+		$user_editfield = $id;
+		$uidstruct = $slashdb->getUIDStruct('ipid', $user_edit->{ipid});
+		@accesshits = $logdb->countAccessLogHitsInLastX('host_addr', $user_edit->{ipid}) if defined($logdb);
+
+		if ($form->{userfield} =~/^\d+\.\d+\.\d+\.(\d+)$/) {
+			if ($1 ne "0"){
+				$proxy_check->{available} = 1;
+				$proxy_check->{results} = $slashdb->checkForOpenProxy($form->{userfield}) if $form->{check_proxy};
+			}
+		}
+
+	} elsif ($field eq 'subnetid') {
+		$user_edit->{nonuid} = 1;
+		$srcid = convert_srcid( ipid => $id );
+		if ($id =~ /^(\d+\.\d+\.\d+)(?:\.\d)?/) {
+			$id = $1 . ".0";
+			$user_edit->{subnetid} = $id;
+		} else {
+			$user_edit->{subnetid} = $id;
+		}
+
+		$user_editfield = $id;
+		$uidstruct = $slashdb->getUIDStruct('subnetid', $user_edit->{subnetid});
+		@accesshits = $logdb->countAccessLogHitsInLastX($field, $user_edit->{subnetid}) if defined($logdb);
+
+	} elsif ($field eq "srcid") {
+		$user_edit->{nonuid} = 1;
+		$user_edit->{srcid}  = $id;
+		$srcid = $id;
+		
+	} else {
+		$user_edit = $id ? $slashdb->getUser($id) : $user;
+		$user_editfield = $user_edit->{uid};
+		$ipstruct = $slashdb->getNetIDStruct($user_edit->{uid});
+		@accesshits = $logdb->countAccessLogHitsInLastX('uid', $user_edit->{uid}) if defined($logdb);
+	}
+
+	##########
+	# Put together the array and hashref that the template will need
+	# to construct the list for display to the admin.
+	# Note that currently a srcid which is not a uid (i.e. which
+	# represents an IP address or masked IP network) cannot have
+	# any ACLs assigned to it.  This may change in the future.
+	# The term "aclam" is used because it has a field set for both
+	# every ACL and every access modifier (i.e. AL2 bit) that is set
+	# for this user or srcid.
+	# For now, ACLs will be listed for IPs as well.
+	# First get the list of ACLs that can be used.
+	my $all_acls_ar = $reader->getAllACLNames();
+	my $all_acls_hr = { map { ( $_, 1 ) } @$all_acls_ar };
+	# Add in any ACLs selected for this user (just in case
+	# getAllACLNames is cached and stale).
+	for my $acl (keys %{$user_edit->{acl}}) {
+		$all_acls_hr->{$acl} = 1;
+	}
+	# Start creating the $all_aclam_hr data, in which the keys are
+	# the HTML selection names that all begin with aclam_ and the
+	# the values are their names/descriptions shown to the admin.
+	# First put all the ACLs into the hash, if we're editing a user.
+	my $all_aclam_hr = { };
+	if (!$user_edit->{nonuid}) {
+		$all_aclam_hr = { map { ( "aclam_$_", "ACL: $_" ) } keys %$all_acls_hr };
+	}
+	# Next put in all the al2 types.
+	my $all_al2types = $reader->getAL2Types;
+	for my $key (keys %$all_al2types) {
+		next if $key eq 'comment'; # skip the 'comment' type
+		$all_aclam_hr->{"aclam_$key"} = $all_al2types->{$key}{title};
+	}
+	# Finally, sort the keys of the hash into the order that we
+	# want them displayed to the admin (ACLs first).
+	my $all_acls_longkeys_hr = { map { ( "aclam_$_", 1 ) } keys %$all_acls_hr };
+	my $all_aclam_ar = [
+		sort {
+			(exists($all_acls_longkeys_hr->{$a}) ? -1 : 1) <=> (exists($all_acls_longkeys_hr->{$b}) ? -1 : 1)
+			||
+			$all_aclam_hr->{$a} cmp $all_aclam_hr->{$b}
+		} keys %$all_aclam_hr
+	];
+	# Now put together the hashref that identifies which of those
+	# items are selected for this user.
+	my $user_aclam_hr = { };
+	for my $acl (keys %{ $user_edit->{acl} }) {
+		$user_aclam_hr->{"aclam_$acl"} = 1;
+	}
+	my $al2_tid_comment = $all_al2types->{comment}{al2tid} || 0;
+	my $al2_log_ar = [ ];
+	my $al2_hr = { };
+	# XXXSRCID Once we get rid of the silly 'md5id' field and all the
+	# other bizarre backward-compatibility code paths early in this
+	# function, this won't be necessary, but until then we need this
+	# sanity check...
+	if ($srcid) {
+		# getAL2 works with either a srcids hashref or a single srcid
+		$al2_hr = $slashdb->getAL2($srcid);
+		for my $al2 (keys %{ $al2_hr }) {
+			$user_aclam_hr->{"aclam_$al2"} = 1;
+		}
+		$al2_log_ar = $slashdb->getAL2Log($srcid);
+	}
+	# Generate al2_nick_hr, which will be populated with keys of all
+	# the (presumably) admin uids who have logged rows for this al2,
+	# and values of their nicks.
+	my $al2_nick_hr = { };
+	for my $al2_log (@$al2_log_ar) {
+		my $uid = $al2_log->{adminuid};
+		next if !$uid; # odd error, might want to flag this
+		$al2_nick_hr->{$uid} ||= $slashdb->getUser($uid, 'nickname');
+	}
+	##########
+
+	$user_edit->{author} = ($user_edit->{author} && $user_edit->{author} == 1)
+		? $constants->{markup_checked_attribute} : '';
+	if (! $user->{nonuid}) {
+		my $threshcodes = $reader->getDescriptions('threshcode_values','',1);
+		$thresh_select = createSelect('defaultpoints', $threshcodes, $user_edit->{defaultpoints}, 1);
+	}
+
+	if (!ref $ipstruct) {
+		undef $ipstruct;
+	} else {
+		@$ipstruct_order = sort { $ipstruct->{$b}{dmin} cmp $ipstruct->{$a}{dmin} } keys %$ipstruct;
+	}
+
+	my $m2total = ($user_edit->{m2fair} || 0) + ($user_edit->{m2unfair} || 0);
+	if ($m2total) {
+		$user_edit->{m2unfairpercent} = sprintf("%.2f",
+			$user_edit->{m2unfair}*100/$m2total);
+	}
+	my $mod_total = ($user_edit->{totalmods} || 0) + ($user_edit->{stirred} || 0);
+	if ($mod_total) {
+		$user_edit->{stirredpercent} = sprintf("%.2f",
+			$user_edit->{stirred}*100/$mod_total);
+	}
+	if ($constants->{subscribe} and my $subscribe = getObject('Slash::Subscribe')) {
+		$user_edit->{subscribe_payments} =
+			$subscribe->getSubscriptionsForUser($user_edit->{uid});
+		$user_edit->{subscribe_purchases} =
+			$subscribe->getSubscriptionsPurchasedByUser($user_edit->{uid},{ only_types => [ "grant", "gift" ] });
+	}
+	my $ipid = $user_edit->{ipid};
+	my $subnetid = $user_edit->{subnetid};
+	my $post_restrictions = {};
+	my ($subnet_karma, $ipid_karma);
+
+	if ($ipid && !$subnetid) {
+		$ipid = md5_hex($ipid) if length($ipid) != 32;
+		$proxy_check->{ipid} = $ipid;
+		$proxy_check->{currently} = $slashdb->getKnownOpenProxy($ipid, "ipid");
+		# This next call is very slow.
+		$subnetid = $reader->getSubnetFromIPIDBasedOnComments($ipid);
+	}
+
+	if ($subnetid) {
+		$subnetid = md5_hex($subnetid) if length($subnetid) != 32;
+		# These next three calls can be very slow.  In fact, getNetIDKarma
+		# is actually called twice on the same subnetid;  if we can cache
+		# that data somehow that wouldn't be a bad idea.
+		$post_restrictions = $reader->getNetIDPostingRestrictions("subnetid", $subnetid);
+		$subnet_karma = $reader->getNetIDKarma("subnetid", $subnetid);
+		$ipid_karma = $reader->getNetIDKarma("ipid", $ipid) if $ipid;
+	}
+
+	return slashDisplay('getUserAdmin', {
+		field			=> $field,
+		useredit		=> $user_edit,
+		srcid			=> $srcid,
+		all_aclam_ar		=> $all_aclam_ar,
+		all_aclam_hr		=> $all_aclam_hr,
+		user_aclam_hr		=> $user_aclam_hr,
+		al2_old			=> $al2_hr,
+		al2_log			=> $al2_log_ar,
+		al2_tid_comment		=> $al2_tid_comment,
+		al2_nick		=> $al2_nick_hr,
+
+		userinfo_flag		=> $user_editinfo_flag,
+		userfield		=> $user_editfield,
+		ipstruct		=> $ipstruct,
+		ipstruct_order		=> $ipstruct_order,
+		uidstruct		=> $uidstruct,
+		accesshits		=> \@accesshits,
+		seclev_field		=> $seclev_field,
+		expired 		=> $expired,
+		topabusers		=> $topabusers,
+		readonly		=> $readonly,
+		thresh_select		=> $thresh_select,
+		authoredit_flag 	=> $authoredit_flag,
+		section_select		=> $section_select,
+		all_acls		=> $all_acls_hr,
+		proxy_check		=> $proxy_check,
+		subnet_karma		=> $subnet_karma,
+		ipid_karma		=> $ipid_karma,
+		post_restrictions	=> $post_restrictions
+	}, 1);
+}
+
+#################################################################
+# this is to allow alternate parameters to be specified.  pass in
+# your hash reference to be passed to setUser(), and this will
+# add in those extra parameters.  add the parameters to string_param,
+# type = otherusersparam, code = name of the param.  they will
+# be checked for the main user prefs editing screens, and on
+# user creation -- pudge
+sub getOtherUserParams {
+	my($data) = @_;
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+
+	my $user    = getCurrentUser();
+	my $form    = getCurrentForm();
+	my $params  = $reader->getDescriptions('otherusersparam');
+
+	for my $param (keys %$params) {
+		if (exists $form->{$param}) {
+			# set user too for output in this request
+			$data->{$param} = $user->{$param} = $form->{$param} || undef;
+		}
+	}
+}
+
+###############################################################
+# This modifies a hashref to default values -- if nothing
+# else we assume the empty string which clears items in the
+# user_param table 
+#
+# takes 3 hashrefs currently
+# $data     - hashref to change to defaults
+# $skip     - hashref of keys to skip modifying
+# $defaults - hashref of defaults to set to something other 
+#             than the empty string
+sub setToDefaults {
+	my($data, $skip, $defaults) = @_;
+	foreach my $key (keys %$data) {
+		next if $skip->{$key};
+		$data->{$key} = exists $defaults->{$key} ? $defaults->{$key} : "";
+ 	}
+}
+
+#################################################################
+sub getCommentListing {
+	my ($type, $value,
+		$min_comment, $time_period, $cc_all, $cc_time_period, $cid_for_time_period,
+		$non_admin_limit, $admin_time_limit, $admin_non_time_limit,
+		$options) = @_;
+	my $reader = getObject('Slash::DB', { db_type => 'reader' });
+	my $slashdb = getCurrentDB();
+	my $constants = getCurrentStatic();
+	my $user = getCurrentUser();
+	my $store_cutoff = $options->{use_uid_cid_cutoff} ? $constants->{store_com_page1_min_cid_for_user_com_cnt} : 0;
+	
+	my $s_opt = {};
+	my $num_wanted = 0;
+	if ($min_comment) {
+		if ($user->{is_admin}) {
+			$num_wanted = $admin_non_time_limit;
+		} else {
+			$num_wanted = $non_admin_limit;
+		}
+	} else {
+	
+		if ($user->{is_admin}) {
+			if ($cc_time_period >= $admin_non_time_limit) {
+				$s_opt->{cid_at_or_after} = $cid_for_time_period;
+				$num_wanted = $admin_time_limit;
+			} else {
+				$num_wanted = $admin_non_time_limit;
+				if($store_cutoff){
+					my $min_cid = $reader->getUser($value,
+						"com_num_".$num_wanted."_at_or_after_cid");
+					$s_opt->{cid_at_or_after} = $min_cid
+						if $min_cid && $min_cid =~ /^\d+$/;
+				}
+			}
+		} else {
+			if ($cc_time_period >= $non_admin_limit ) {
+				$s_opt->{cid_at_or_after} = $cid_for_time_period;
+				$num_wanted = $non_admin_limit;
+			} else {
+				$num_wanted = $non_admin_limit;
+				if($store_cutoff){
+					my $min_cid = $reader->getUser($value,
+						"com_num_".$num_wanted."_at_or_after_cid");
+					$s_opt->{cid_at_or_after} = $min_cid
+						if $min_cid && $min_cid =~ /^\d+$/;
+				}
+			}
+		}
+	}
+	if ($type eq "uid") {
+
+		my $comments = $reader->getCommentsByUID($value, $num_wanted, $min_comment, $s_opt) if $cc_all;
+		if ($store_cutoff
+			&& $comments && $cc_all >= $store_cutoff && $min_comment == 0 
+			&& scalar(@$comments) == $num_wanted) {
+			my $min_cid = 0;
+			for my $comment (@$comments) {
+				$min_cid = $comment->{cid}
+					if !$min_cid || ($comment->{cid} < $min_cid); 
+			}
+			if ($min_cid && $min_cid =~/^\d+$/) {
+				$slashdb->setUser($value, {
+					"com_num_".$num_wanted."_at_or_after_cid" => $min_cid
+				});
+			}
+			
+		}
+		return $comments;
+	} elsif ($type eq "ipid"){
+		return $reader->getCommentsByIPID($value, $num_wanted, $min_comment, $s_opt) if $cc_all;
+	} elsif ($type eq "subnetid"){
+		return $reader->getCommentsBySubnetID($value, $num_wanted, $min_comment, $s_opt) if $cc_all;
+	} else {
+		return $reader->getCommentsByIPIDOrSubnetID($value, $num_wanted, $min_comment, $s_opt) if $cc_all;
+	}
+}
+createEnvironment();
+main();
+
+1;

Modified: slashjp/trunk/themes/slashcode/templates/dispComment;misc;default
===================================================================
--- slashjp/trunk/themes/slashcode/templates/dispComment;misc;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/themes/slashcode/templates/dispComment;misc;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -23,7 +23,7 @@
 dispComment
 __template__
 [% IF !options.noshow_show %]
-<li id="tree_[% cid %]" class="comment">
+<li id="tree_[% cid %]" class="comment[% IF class == 'full' %] contain[% END %]">
 <div id="comment_status_[% cid %]" class="commentstatus"></div>
 <div id="comment_[% cid %]"[% IF discussion2 %] class="[% class %]"[% END %]>
 [% END; IF !options.noshow %]
@@ -34,7 +34,7 @@
 			[% ELSE %]
 			<h4><a name="[% cid %]">[% subject %]</a>
 			[%- END %]
-			[% UNLESS user.noscores %]<span id="comment_score_[% cid %]" class="score">([% IF constants.modal_prefs_active && !user.is_anon %]<a href="#" onclick="getModalPrefs('modcommentlog', 'Moderation Comment Log', [% cid %]); return false">[% END %]Score:[% points.length ? points : "?" %][% IF constants.modal_prefs_active && !user.is_anon %]</a>[% END %][% IF reasons && reason %], [% reasons.$reason.name %][% END %])</span>[% END %]</h4>
+			[% UNLESS user.noscores %]<span id="comment_score_[% cid %]" class="score">([% IF constants.modal_prefs_active %]<a href="#" onclick="getModalPrefs('modcommentlog', 'Moderation Comment Log', [% cid %]); return false">[% END %]Score:[% points.length ? points : "?" %][% IF constants.modal_prefs_active %]</a>[% END %][% IF reasons && reason %], [% reasons.$reason.name %][% END %])</span>[% END %]</h4>
 		</div>
 		<div class="details">
 			by

Modified: slashjp/trunk/themes/slashcode/templates/dispLinkComment;misc;default
===================================================================
--- slashjp/trunk/themes/slashcode/templates/dispLinkComment;misc;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/themes/slashcode/templates/dispLinkComment;misc;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -13,38 +13,32 @@
 __name__
 dispLinkComment
 __template__
-[% IF user.mode != 'metamod' %]
+[% IF user.mode != 'metamod' && user.mode != 'archive' %]
 	[% IF user.is_admin || original_pid || !user.state.discussion_archived %]
 		[% do_parent = ( original_pid ); #&& !(discussion2 && (!form.cid || form.cid != cid)) );
 		   can_del   = ( (constants.authors_unlimited && user.seclev >= constants.authors_unlimited) || user.acl.candelcomments_always ) %]
 		[% IF !options.show_pieces %]<div class="commentSub" id="comment_sub_[% cid %]">[% END; IF !options.pieces %]
 
-		[
-		
 		[% IF !user.state.discussion_archived && !user.state.discussion_future_nopost %]
-			<span id="reply_link_[% cid %]">[% Slash.linkComment({
+			<span id="reply_link_[% cid %]" class="nbutton"><p><b>[% Slash.linkComment({
 				sid	=> sid,
 				pid	=> cid,
 				op	=> 'Reply',
 				subject	=> 'Reply to This',
 				subject_only => 1,
-				onclick	=> ((discussion2 && (!constants.subscribe || user.is_subscriber)) ? "replyTo($cid); return false;" : '')
-			}) %]</span>
+				onclick	=> ((discussion2 && !user.is_anon) ? "replyTo($cid); return false;" : '')
+			}) %]</b></p></span>
 		[% END %]
 
-		[% IF !(user.state.discussion_archived) && ( do_parent || can_mod || can_del ) %] | [% END %]
-
-		[% IF do_parent %][% Slash.linkComment({
+		[% IF do_parent %]<span class="nbutton"><p><b>[% Slash.linkComment({
 			sid	=> sid,
 			cid	=> original_pid,
 			pid	=> original_pid,
 			subject	=> 'Parent',
 			subject_only => 1,
 			onclick	=> (discussion2 ? "return selectParent($original_pid)" : '')
-		}, 1) %][% END %]
+		}, 1) %]</b></p></span>[% END %]
 		
-		[% IF do_parent && ( can_mod || can_del ) %] | [% END %]
-
 		[% IF can_mod %]
 		<div id="reasondiv_[% cid %]" class="modsel">[% Slash.createSelect("reason_$cid", reasons, {
 			'return'	=> 1,
@@ -52,14 +46,10 @@
 			onchange	=> (discussion2 ? 'return doModerate(this)' : '')
 		}) %]</div>[% END %]
 
-		[% IF can_mod && can_del %] | [% END %]
-
 		[% IF can_del %]
 		<input type="checkbox" name="del_[% cid %]">
 		[% END %]
 
-		]
-
 		[% END; IF !options.show_pieces %]</div>
 	[% END; END %]
 

Modified: slashjp/trunk/themes/slashcode/templates/editComm;users;default
===================================================================
--- slashjp/trunk/themes/slashcode/templates/editComm;users;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/themes/slashcode/templates/editComm;users;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -54,10 +54,7 @@
 		<td valign="middle">
         	<p><b>Discussion Style</b><p>
 		<blockquote><div>
-        	<input type="radio" name="discussion2" value="none"[% constants.markup_checked_attribute IF !user_edit.discussion2 || (user_edit.discussion2 && user_edit.discussion2 != "uofm" && user_edit.discussion2 != "slashdot") %]> Slashdot Classic Discussion System<br>
-		[% IF user.is_admin || user.acl.discussion2_uofm %]
-        	<input type="radio" name="discussion2" value="uofm"[% constants.markup_checked_attribute IF user_edit.discussion2 == "uofm" %]> University of Michigan Testing<br>
-		[% END %]
+        	<input type="radio" name="discussion2" value="none"[% constants.markup_checked_attribute IF !user_edit.discussion2 || (user_edit.discussion2 && user_edit.discussion2 != "slashdot") %]> Slashdot Classic Discussion System<br>
         	<input type="radio" name="discussion2" value="slashdot"[% constants.markup_checked_attribute IF user_edit.discussion2 == "slashdot" %]> Slashdot Interactive Discussion System (a.k.a. D2)<br>
 		</div></blockquote>
 

Modified: slashjp/trunk/themes/slashcode/templates/printCommComments;misc;default
===================================================================
--- slashjp/trunk/themes/slashcode/templates/printCommComments;misc;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/themes/slashcode/templates/printCommComments;misc;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -47,7 +47,7 @@
 	</script>
 [% END %]
 
-[% IF cid %]
+[% IF cid && !discussion2 %]
 	<ul id="commentlisting" class="[% user.mode %]">
 	[% Slash.dispComment(comment) %]
 	<div class="comment_footer">
@@ -88,21 +88,20 @@
 [% END %]
 
 	[% lcp %]
-	[% IF lvl %]
-	[% END %]
-	[% thread = Slash.displayThread(sid, pid, lvl, comments) %]
+	[% IF lvl; END %]
+	[% this_pid = discussion2 ? 0 : pid;
+	   thread = Slash.displayThread(sid, this_pid, lvl, comments) %]
 	[% IF thread || discussion2 %]
-		[% UNLESS cid %]<ul id="commentlisting">[% END %]
+		[% IF !cid || discussion2 %]<ul id="commentlisting"[% IF discussion2 %] class="d2"[% END %]>[% END %]
 			[% thread || '' %]
 			<li id="roothiddens" class="hide"></li>
-		[% UNLESS cid %]</ul>[% END %]
+		[% IF !cid || discussion2 %]</ul>[% END %]
 	[% END %]
-	[% IF cid %]</ul>[% END %]
-	[% IF lvl %]
-	[% END %]
+	[% IF cid && !discussion2 %]</ul>[% END %]
+	[% IF lvl; END %]
 	[% lcp %]
 
-[% IF discussion2 && !cid %]
+[% IF discussion2 %]
 <div id="replyto_0"></div>
 <div class="prev-next"><a href="#" onclick="ajaxFetchComments(0,1); return false"><span id="more_comments_num_a" class="hide">Check for more</span></a>
 	[% UNLESS user.state.discussion_archived || user.state.discussion_future_nopost %]
@@ -112,7 +111,7 @@
 			op           => 'reply',
 			subject      => 'Reply',
 			subject_only => 1,
-			onclick      => ((discussion2 && (!constants.subscribe || user.is_subscriber)) ? "replyTo(0); return false;" : '')
+			onclick      => ((discussion2 && !user.is_anon) ? "replyTo(0); return false;" : '')
 		});
 	END %]
 </div>
@@ -150,8 +149,7 @@
 		pieces_comments      = [[% user.state.comments.pieces.join(',') %]];
 		init_hiddens         = [[% user.state.comments.hiddens.join(',') %]];
 
-		[% IF discussion2 == "slashdot" %]d2act();
-		[% END -%]finishLoading();
+		finishLoading();
 //-->
 	</script>
 [% END %]

Modified: slashjp/trunk/themes/slashcode/templates/printCommentsMain;misc;default
===================================================================
--- slashjp/trunk/themes/slashcode/templates/printCommentsMain;misc;default	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/themes/slashcode/templates/printCommentsMain;misc;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -44,7 +44,7 @@
 
 <a name="acomments"></a>
 <div class="commentwrap" id="commentwrap">
-[% IF (!user.is_anon && !user.acl.discussion2_uofm) || user.is_admin %]
+[% IF !user.is_anon %]
 <div class="commentBox" style="font-size: 120%">
 	<form method="GET" action="[% gSkin.rootdir %]/comments.pl">
 	<div>
@@ -62,10 +62,6 @@
 	</div>
 	</form>
 </div>
-[% END; IF (user.acl.discussion2_uofm_signup && !user.acl.discussion2_uofm && (user.is_admin || !user.is_subscriber)) && constants.uofm_key && constants.uofm_iv && env.http_user_agent.search('Firefox/1\.5') %]
-<div class="commentBox" style="font-size: 120%">
-<a href="[% Slash.tempUofmLinkGenerate() %]">Want to test an experimental interface for comments?</a>
-</div>
 [% END %]
 
 [% UNLESS discussion2 %]
@@ -183,7 +179,7 @@
 				}) %]
 				[% END %]
 		<span class="ccw-header-links">
-    [% IF discussion2 && !cid && !pid %]
+    [% IF discussion2 %]
         <a href="#" onclick="ajaxFetchComments(0,1); return false"><span id="more_comments_num_b"></span> More</a> | 
     [% END; IF user.is_admin && user.d2prefs_debug %]
         <a href="#" onclick="showPrefs('reading'); return false">Prefs</a>
@@ -202,7 +198,7 @@
 				op           => 'reply',
 				subject      => 'Reply',
 				subject_only => 1,
-				onclick      => ((discussion2 && (!constants.subscribe || user.is_subscriber)) ? "replyTo(0); return false;" : '')
+				onclick      => ((discussion2 && !user.is_anon) ? "replyTo(0); return false;" : '')
 			}) %]
 		[% END %]
 		</span>
@@ -218,6 +214,7 @@
     init: function() {
       gCommentControlWidget = new YAHOO.slashdot.ThresholdWidget([% "'X'" IF horiz %]);
       gCommentControlWidget.setTHT(user_threshold, user_highlightthresh);
+      updateTotals();
     }
   };
 }();
@@ -269,7 +266,7 @@
       </div>
 		</div>
 		<div class="commentControlFooter">
-    [% IF discussion2 && !cid && !pid %]
+    [% IF discussion2 %]
         <a href="#" onclick="ajaxFetchComments(0,1); return false"><span id="more_comments_num_c"></span> More</a> | 
     [% END; IF user.is_admin && user.d2prefs_debug %]
         <a href="#" onclick="showPrefs('reading'); return false">Prefs</a>
@@ -287,7 +284,7 @@
 				op           => 'reply',
 				subject      => 'Reply',
 				subject_only => 1,
-				onclick      => ((discussion2 && (!constants.subscribe || user.is_subscriber)) ? "replyTo(0); return false;" : '')
+				onclick      => ((discussion2 && !user.is_anon) ? "replyTo(0); return false;" : '')
 			}) %]
 		[% END %]
 			<div id="bindings-legend">Keybindings Beta<br>
@@ -296,10 +293,16 @@
 <a href="#" onclick="keyHandler('','E'); return false" title="next comment by load order"    >E</a><br>
 <a href="#" onclick="keyHandler('','A'); return false" title="previous comment in thread"    >A</a>
 <a href="#" onclick="keyHandler('','S'); return false" title="next thread"                   >S</a>
-<a href="#" onclick="keyHandler('','D'); return false" title="next comment in thread"        >D</a>
+<a href="#" onclick="keyHandler('','D'); return false" title="next comment in thread"        >D</a><br>
+<a href="#" onclick="keyHandler('','R'); return false" title="reply to current comment"      >R</a>
+<a href="#" onclick="keyHandler('','P'); return false" title="parent of current comment"     >P</a>
+<a href="#" onclick="keyHandler('','M'); return false" title="history of current comment"    >M</a><br>
+<a href="#" onclick="keyHandler('','T'); return false" title="first comment"                 >T</a>
+<a href="#" onclick="keyHandler('','G'); return false" title="get more comments"             >G</a>
+<a href="#" onclick="keyHandler('','V'); return false" title="last comment"                  >V</a>
 			</div>
 		</div>
-		<div id="commentControlBoxStatus" class="hide"><b>Loading ... Please wait.</b></div>
+		<div id="commentControlBoxStatus" class="hide"><b>Loading... please wait.</b></div>
 	</div>
 </div>
 		</div>

Copied: slashjp/trunk/themes/slashcode/templates/userInfo2;users;default (from rev 571, slashjp/branches/upstream/current/themes/slashcode/templates/userInfo2;users;default)
===================================================================
--- slashjp/trunk/themes/slashcode/templates/userInfo2;users;default	                        (rev 0)
+++ slashjp/trunk/themes/slashcode/templates/userInfo2;users;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -0,0 +1,44 @@
+__section__
+default
+__description__
+Display user's info
+
+* title = passed to titlebar
+* useredit = hashref of info of the user being viewed
+* points = available moderation points
+* commentstruct = arrayref of comments
+* nickmatch_flag = current user is same as viewed user
+* mod_flag = is moderator
+	(no need for this anymore)
+* karma_flag = boolean for display karma
+* admin_block = admin stuff
+* admin_flag = boolean for whether to display admin stuff
+	(no real need for this, could just use user.is_admin)
+* fieldkey = the field key used to decide what we're looking at
+* reasons = hashref from $moddb->getReasons()
+* lastjournal = last journal posted
+* hr_hours_back = number of hours back to show a <hr> for
+* cids_to_mods = hashref keyed by cid containing arrays of moderations done to that cid
+* comment_time = number of days back we are limiting the comments shown to.  If 0 or undefined we're showing comments sequentially w/o time limits
+
+__title__
+
+__page__
+users
+__lang__
+en_US
+__name__
+userInfo2
+__template__
+[% orig_title = title %]
+
+<div id="slashboxes">
+[% PROCESS userboxes2 %]
+</div>
+
+[% title = orig_title %]
+
+__seclev__
+500
+__version__
+$Id: userInfo2;users;default,v 1.1 2008/04/02 14:42:24 entweichen Exp $

Copied: slashjp/trunk/themes/slashcode/templates/userboxes2;misc;default (from rev 571, slashjp/branches/upstream/current/themes/slashcode/templates/userboxes2;misc;default)
===================================================================
--- slashjp/trunk/themes/slashcode/templates/userboxes2;misc;default	                        (rev 0)
+++ slashjp/trunk/themes/slashcode/templates/userboxes2;misc;default	2008-04-07 00:26:30 UTC (rev 572)
@@ -0,0 +1,147 @@
+__section__
+default
+__description__
+Displays the three user boxes (fancybox's).
+
+* useredit = user being viewed ("edit" is for historical reasons)
+  (if not given, the standard "user" will be used)
+
+__title__
+
+__page__
+misc
+__lang__
+en_US
+__name__
+userboxes2
+__template__
+[%
+
+IF !useredit; useredit = user; END;
+
+IF !useredit.is_anon;
+
+	# First box:  general user info
+
+	title = 'User Bio';
+	contents = BLOCK;
+		'<a href="'; gSkin.rootdir; '/~';
+			useredit.nickname | strip_paramattr; '/">';
+                        useredit.nickname | strip_literal; '</a> ';
+		PROCESS zoo_icons person=useredit.uid implied="";
+		IF user.uid == useredit.uid OR user.is_admin;
+			# Looking at ourselves; show our real name and email info.
+			'<br>'; IF useredit.realname; useredit.realname | strip_literal; ELSE; '(no real name given)'; END;
+			'<br><a href="mailto:';
+				useredit.realemail | strip_paramattr; '">';
+				Slash.ellipsify(Slash.strip_literal(useredit.realemail)); '</a>';
+			'<br>&nbsp;&nbsp;';
+			IF useredit.fakeemail;
+				IF useredit.fakeemail == useredit.realemail;
+					'(shown without obfuscation)';
+				ELSE;
+					'shown as <a href="mailto:';
+					useredit.fakeemail | strip_paramattr; '">';
+					Slash.ellipsify(Slash.strip_literal(useredit.fakeemail)); '</a>';
+				END;
+			ELSE;
+				'(email not shown publicly)';
+			END;
+		ELSE;
+			# Looking at someone else; show fake email info.
+			'<br>&nbsp;&nbsp;';
+			IF useredit.fakeemail;
+				'<a href="mailto:';
+					useredit.fakeemail | strip_paramattr; '">';
+					Slash.ellipsify(Slash.strip_literal(useredit.fakeemail)); '</a>';
+			ELSE;
+				'(email not shown publicly)';
+			END;
+		END;
+
+		IF useredit.homepage;
+			'<br><a href="';
+			useredit.homepage | strip_attribute;
+			'"';
+			IF useredit.karma <= constants.goodkarma;
+				' rel="nofollow"';
+			END;
+			'>';
+			Slash.ellipsify(Slash.strip_literal(useredit.homepage)); '</a>';
+		END;
+
+		IF user.uid == useredit.uid OR user.is_admin;
+			'<br>Karma: ';
+			PROCESS karma karma=useredit.karma admin_flag=user.is_admin;
+		END;
+
+		IF useredit.aim && !useredit.aimdisplay;
+			'<br><b>AOL IM:</b> ';
+			useredit.aim | strip_literal;
+			' (<b><a href="aim:addbuddy?screenname=';
+			useredit.aim | strip_attribute;
+			'">Add Buddy</a>, ';
+			'<a href="aim:goim?screenname=';
+			useredit.aim | strip_attribute;
+			'&amp;message=Greetings!">Send Message</a></b>)';
+		END;
+
+		IF useredit.yahoo;
+			'<br><b>Yahoo! ID:</b> ';
+			'<a href="http://profiles.yahoo.com/';
+			useredit.yahoo | strip_attribute;
+			'">';
+			useredit.yahoo | strip_literal;
+			'</a> (<b><a href="http://edit.yahoo.com/config/set_buddygrp?';
+			'.src=&amp;.cmd=a&amp;.bg=Friends&amp;.bdl=';
+			useredit.yahoo | strip_attribute;
+			'">Add User</a>, ';
+			'<a href="http://edit.yahoo.com/config/send_webmesg?.target=';
+			useredit.yahoo | strip_attribute;
+			'">Send Message</a></b>)';
+		END;
+
+		IF useredit.jabber;
+			'<br><b>Jabber:</b> ';
+			useredit.jabber | strip_literal;
+		END;
+
+		IF useredit.calendar_url;
+			'<br><b>Public Calendar:</b> ';
+			'<a href="webcal://';
+			useredit.calendar_url | strip_attribute;
+			'">Subscribe</a>, <a href="http://';
+			useredit.calendar_url | strip_attribute;
+			'">Download</a>';
+		END;
+
+                IF useredit.bio;
+                        '<br><hr>';
+			Slash.parseDomainTags(useredit.bio);
+                END;
+
+	END;
+	Slash.sidebox(title, contents, "user-info", 1);
+
+        # Latest comments box
+        contents = '';
+        FOREACH cid = latest_comments.keys.sort;
+                contents = contents _ '<a href="' _ constants.absolutedir_secure _ '/comments.pl?sid=' _ latest_comments.$cid.sid _ '&cid=' _ cid _'">' _ latest_comments.$cid.subject _ '</a><br>';
+        END;
+        title = 'Latest Comments';
+        Slash.sidebox(title, contents, "user-info", 1);
+
+        # Latest journals box
+        contents = '';
+        FOREACH jid = latest_journals.keys.sort;
+                contents = contents _ '<a href="' _ constants.absolutedir_secure _ '/~' _ useredit.nickname _ '/journal/' _ latest_journals.$jid.id _ '">' _ latest_journals.$jid.desc _ '</a><br>';
+        END; 
+        title = 'Latest Journal Entries';
+        Slash.sidebox(title, contents, "user-info", 1);
+
+END %]
+
+__seclev__
+1000
+__version__
+$Id: userboxes2;misc;default,v 1.1 2008/04/02 14:41:12 entweichen Exp $

Modified: slashjp/trunk/utils/createTestTags
===================================================================
--- slashjp/trunk/utils/createTestTags	2008-04-06 23:20:44 UTC (rev 571)
+++ slashjp/trunk/utils/createTestTags	2008-04-07 00:26:30 UTC (rev 572)
@@ -63,7 +63,7 @@
 
 	my $stories = $slashdb->getStoriesEssentials({ future_secs => 0, limit => 10, limit_extra => 0,
 		sectioncollapse => (rand(1) < 0.3) ? 1 : 0 });
-
+	my $priv_tagnames = $tagsdb->getPrivateTagnames();
 	my $firehose = getObject('Slash::FireHose');
 	my $minfhcolor = (rand(1) < 0.4 ? 5 : 7);
 	my($firehose_items, $firehose_results) =
@@ -88,11 +88,14 @@
 			$uid = _get_uid($tagsdb, $stoid);
 
 			$tagnameid = _get_tagnameid($tagsdb, $tagnameid, $stoid, $uid);
+			my $tagname = $tagsdb->getTagnameDataFromId($tagnameid)->{tagname};
+			my $is_private = $priv_tagnames->{$tagname};
 			my $tagid = $tagsdb->createTag({
 				uid => $uid,
 				tagnameid => $tagnameid,
 				table => 'stories',
 				id => $stoid,
+				private => $is_private,
 			});
 			print "tagid=$tagid tagnameid=$tagnameid uid=$uid stoid=$stoid\n";
 		} else {
@@ -103,10 +106,13 @@
 				"karma BETWEEN $kmin AND $kmax",
 				'ORDER BY RAND() LIMIT 1') if !$uid || rand(1) > 0.2;
 			$tagnameid = _get_tagnameid($tagsdb, $tagnameid, $globjid, $uid);
+			my $tagname = $tagsdb->getTagnameDataFromId($tagnameid)->{tagname};
+			my $is_private = $priv_tagnames->{$tagname};
 			my $tagid = $tagsdb->createTag({
 				uid => $uid,
 				tagnameid => $tagnameid,
 				globjid => $globjid,
+				private => $is_private,
 			});
 			print "tagid=$tagid tagnameid=$tagnameid uid=$uid globjid=$globjid\n";
 		}


Slashdotjp-dev メーリングリストの案内
アーカイブの一覧に戻る