You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
520 lines
18 KiB
520 lines
18 KiB
11 years ago
|
// twister_following.js
|
||
|
// 2013 Miguel Freitas
|
||
|
//
|
||
|
// Manage list of following users. Load/Save to localstorage and DHT.
|
||
|
// Provides random user suggestions to follow.
|
||
|
|
||
|
var followingUsers = [];
|
||
|
var _isFollowPublic = {};
|
||
|
var _followsPerPage = 200;
|
||
|
var _maxFollowingPages = 50;
|
||
|
var _followingSeqNum = 0;
|
||
|
var _followSuggestions = [];
|
||
|
var _searchingPartialUsers = "";
|
||
|
var _searchKeypressTimer = undefined;
|
||
|
var _lastLoadFromDhtTime = 0;
|
||
|
|
||
|
// load followingUsers from localStorage
|
||
|
function loadFollowingFromStorage() {
|
||
|
var ns=$.initNamespaceStorage(defaultScreenName);
|
||
|
if( ns.localStorage.isSet("followingUsers") )
|
||
|
followingUsers = ns.localStorage.get("followingUsers");
|
||
|
if( ns.localStorage.isSet("isFollowPublic") )
|
||
|
_isFollowPublic = ns.localStorage.get("isFollowPublic");
|
||
|
if( ns.localStorage.get("followingSeqNum") > _followingSeqNum)
|
||
|
_followingSeqNum = ns.localStorage.get("followingSeqNum");
|
||
|
if( ns.localStorage.isSet("lastLoadFromDhtTime") )
|
||
|
_lastLoadFromDhtTime = ns.localStorage.get("lastLoadFromDhtTime");
|
||
|
// follow ourselves
|
||
|
if(followingUsers.indexOf(defaultScreenName) < 0) {
|
||
|
followingUsers.push(defaultScreenName);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// save list of following to localStorage
|
||
|
function saveFollowingToStorage() {
|
||
|
var ns=$.initNamespaceStorage(defaultScreenName);
|
||
|
ns.localStorage.set("followingUsers", followingUsers);
|
||
|
ns.localStorage.set("isFollowPublic", _isFollowPublic);
|
||
|
ns.localStorage.set("followingSeqNum", _followingSeqNum);
|
||
|
ns.localStorage.set("lastLoadFromDhtTime", _lastLoadFromDhtTime);
|
||
|
}
|
||
|
|
||
|
// load public list of following users from dht resources
|
||
|
// "following1", "following2" etc.
|
||
|
// it will stop loading when resource is empty
|
||
|
// callback is called as: doneCb(doneArg, followingList, seqNum)
|
||
|
function loadFollowingFromDht(username, pageNumber, followingList, seqNum, doneCb, doneArg) {
|
||
|
if( !pageNumber ) pageNumber = 1;
|
||
|
|
||
|
dhtget( username, "following" + pageNumber, "s",
|
||
|
function(args, following, rawdata) {
|
||
|
if( rawdata ) {
|
||
|
var seq = parseInt(rawdata[0]["p"]["seq"]);
|
||
|
if( seq > args.seqNum ) args.seqNum = seq;
|
||
|
}
|
||
|
|
||
|
if( following ) {
|
||
|
for( var i = 0; i < following.length; i++ ) {
|
||
|
if( args.followingList.indexOf(following[i]) < 0 ) {
|
||
|
args.followingList.push(following[i]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if( following && following.length && args.pageNumber < _maxFollowingPages) {
|
||
|
loadFollowingFromDht(username, args.pageNumber,
|
||
|
args.followingList, args.seqNum,
|
||
|
args.doneCb, args.doneArg);
|
||
|
} else {
|
||
|
if( args.doneCb )
|
||
|
args.doneCb(args.doneArg, args.followingList, args.seqNum);
|
||
|
}
|
||
|
}, {pageNumber:pageNumber+1, followingList:followingList, seqNum:seqNum,
|
||
|
doneCb:doneCb, doneArg:doneArg});
|
||
|
}
|
||
|
|
||
|
// get number of following from dht and set item.text()
|
||
|
function getNumFollowing( username, item ) {
|
||
|
loadFollowingFromDht( username, 1, [], 0,
|
||
|
function(args, following, seqNum) {
|
||
|
item.text( following.length );
|
||
|
}, null);
|
||
|
}
|
||
|
|
||
|
// load following list from localStorage and then from the dht resource
|
||
|
function loadFollowing(cbFunc, cbArg) {
|
||
|
loadFollowingFromStorage();
|
||
|
updateFollowing();
|
||
|
|
||
|
var curTime = new Date().getTime() / 1000;
|
||
|
|
||
|
// optimization to avoid costly dht lookup everytime the home is loaded
|
||
|
if( curTime > _lastLoadFromDhtTime + 3600*24 ||
|
||
|
document.URL.indexOf("following") >= 0 ) {
|
||
|
var numFollow = followingUsers.length;
|
||
|
loadFollowingFromDht( defaultScreenName, 1, [], _followingSeqNum,
|
||
|
function(args, following, seqNum) {
|
||
|
var curTime = new Date().getTime() / 1000;
|
||
|
_lastLoadFromDhtTime = curTime;
|
||
|
|
||
|
for( var i = 0; i < following.length; i++ ) {
|
||
|
if( followingUsers.indexOf(following[i]) < 0 ) {
|
||
|
followingUsers.push(following[i]);
|
||
|
}
|
||
|
_isFollowPublic[following[i]] = true;
|
||
|
}
|
||
|
|
||
|
if( args.numFollow != followingUsers.length ||
|
||
|
seqNum != _followingSeqNum ) {
|
||
|
_followingSeqNum = seqNum;
|
||
|
// new following loaded from dht
|
||
|
saveFollowingToStorage();
|
||
|
updateFollowing();
|
||
|
}
|
||
|
|
||
|
if( args.cbFunc )
|
||
|
args.cbFunc(args.cbArg);
|
||
|
}, {numFollow:numFollow, cbFunc:cbFunc, cbArg:cbArg} );
|
||
|
} else {
|
||
|
if( cbFunc )
|
||
|
cbFunc(cbArg);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// save list of following to dht resource. each page ("following1", following2"...)
|
||
|
// constains up to _followsPerPage elements. alternatively we might keep track
|
||
|
// of total strings size to optimize the maximum storage (8kb in node.cpp, but 4kb is
|
||
|
// probably a good target).
|
||
|
function saveFollowingToDht() {
|
||
|
var following = [];
|
||
|
var pageNumber = 1;
|
||
|
for( var i = 0; i < followingUsers.length; i++ ) {
|
||
|
if( followingUsers[i] in _isFollowPublic &&
|
||
|
_isFollowPublic[followingUsers[i]] ) {
|
||
|
following.push(followingUsers[i]);
|
||
|
}
|
||
|
if( following.length == _followsPerPage || i == followingUsers.length-1) {
|
||
|
dhtput( defaultScreenName, "following" + pageNumber, "s",
|
||
|
following, defaultScreenName, _followingSeqNum+1 );
|
||
|
pageNumber++;
|
||
|
following = [];
|
||
|
}
|
||
|
}
|
||
|
dhtput( defaultScreenName, "following" + pageNumber, "s",
|
||
|
following, defaultScreenName, _followingSeqNum+1 );
|
||
|
|
||
|
_followingSeqNum++;
|
||
|
}
|
||
|
|
||
|
// save following to local storage, dht and json rpc
|
||
|
function saveFollowing(cbFunc, cbArg) {
|
||
|
saveFollowingToStorage();
|
||
|
saveFollowingToDht();
|
||
|
updateFollowing(cbFunc, cbArg);
|
||
|
}
|
||
|
|
||
|
// update json rpc with current list of following
|
||
|
function updateFollowing(cbFunc, cbArg) {
|
||
|
twisterRpc("follow", [defaultScreenName,followingUsers],
|
||
|
function(args, ret) {
|
||
|
if( args.cbFunc )
|
||
|
args.cbFunc(args.cbArg, true);
|
||
|
}, {cbFunc:cbFunc, cbArg:cbArg},
|
||
|
function(args, ret) {
|
||
|
console.log("ajax error:" + ret);
|
||
|
if( args.cbFunc )
|
||
|
args.cbFunc(args.cbArg, false);
|
||
|
}, cbArg);
|
||
|
}
|
||
|
|
||
|
// follow a new single user.
|
||
|
// it is safe to call this even if username is already in followingUsers.
|
||
|
// may also be used to set/clear publicFollow.
|
||
|
function follow(user, publicFollow, cbFunc, cbArg) {
|
||
|
if( followingUsers.indexOf(user) < 0 ) {
|
||
|
followingUsers.push(user);
|
||
|
}
|
||
|
if( publicFollow == undefined || publicFollow )
|
||
|
_isFollowPublic[user] = true;
|
||
|
else
|
||
|
delete _isFollowPublic[user];
|
||
|
saveFollowing(cbFunc, cbArg);
|
||
|
}
|
||
|
|
||
|
// unfollow a single user
|
||
|
function unfollow(user, cbFunc, cbArg) {
|
||
|
var i = followingUsers.indexOf(user);
|
||
|
if( i >= 0 ) {
|
||
|
followingUsers.splice(i,1);
|
||
|
}
|
||
|
delete _isFollowPublic[user];
|
||
|
saveFollowing();
|
||
|
|
||
|
twisterRpc("unfollow", [defaultScreenName,[user]],
|
||
|
function(args, ret) {
|
||
|
if( args.cbFunc )
|
||
|
args.cbFunc(args.cbArg, true);
|
||
|
}, {cbFunc:cbFunc, cbArg:cbArg},
|
||
|
function(args, ret) {
|
||
|
console.log("ajax error:" + ret);
|
||
|
if( args.cbFunc )
|
||
|
args.cbFunc(args.cbArg, false);
|
||
|
}, {cbFunc:cbFunc, cbArg:cbArg});
|
||
|
}
|
||
|
|
||
|
// check if public following
|
||
|
function isPublicFollowing(user) {
|
||
|
if( followingUsers.indexOf(user) < 0 ) {
|
||
|
return false;
|
||
|
}
|
||
|
if( (user in _isFollowPublic) && _isFollowPublic[user] == true )
|
||
|
return true;
|
||
|
else
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// check if following list is empty
|
||
|
function followingEmptyOrMyself() {
|
||
|
if( followingUsers.length == 0 ||
|
||
|
followingUsers.length == 1 && followingUsers[0] == defaultScreenName )
|
||
|
return true;
|
||
|
else
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// randomly choose a user we follow, get "following1" from him and them
|
||
|
// choose a suggestion from their list. this function could be way better, but
|
||
|
// that's about the simplest we may get to start with.
|
||
|
function getRandomFollowSuggestion(cbFunc, cbArg) {
|
||
|
|
||
|
if( followingEmptyOrMyself() ) {
|
||
|
cbFunc(cbArg, null, null);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var i;
|
||
|
do{
|
||
|
var i = parseInt( Math.random() * followingUsers.length );
|
||
|
} while( i < followingUsers.length && followingUsers[i] == defaultScreenName);
|
||
|
|
||
|
if( i < followingUsers.length ) {
|
||
|
loadFollowingFromDht( followingUsers[i], 1, [], 0,
|
||
|
function(args, following, seqNum) {
|
||
|
if( following ) {
|
||
|
var suggested = false;
|
||
|
var j = parseInt( Math.random() * following.length );
|
||
|
for( ; j < following.length; j++ ) {
|
||
|
if( followingUsers.indexOf(following[j]) < 0 &&
|
||
|
_followSuggestions.indexOf(following[j]) < 0 ) {
|
||
|
args.cbFunc(args.cbArg, following[j], args.followedBy);
|
||
|
_followSuggestions.push(following[j]);
|
||
|
suggested = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if( !suggested ) {
|
||
|
args.cbFunc(args.cbArg, null, null);
|
||
|
}
|
||
|
}
|
||
|
}, {cbFunc:cbFunc, cbArg:cbArg, followedBy:followingUsers[i]});
|
||
|
} else {
|
||
|
cbFunc(cbArg, null, null);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// adds following users to the interface (following.html)
|
||
|
function showFollowingUsers(){
|
||
|
var $notFollowing = $(".not-following-any");
|
||
|
if( followingEmptyOrMyself() ) {
|
||
|
$notFollowing.show();
|
||
|
} else {
|
||
|
$notFollowing.hide();
|
||
|
}
|
||
|
|
||
|
var $followingList = $(".following-list");
|
||
|
var $template = $("#following-user-template").detach();
|
||
|
|
||
|
$followingList.empty();
|
||
|
$followingList.append($template);
|
||
|
|
||
|
for( var i = 0; i < followingUsers.length; i++ ) {
|
||
|
if( followingUsers[i] == defaultScreenName )
|
||
|
continue;
|
||
|
|
||
|
var resItem = $template.clone(true);
|
||
|
resItem.removeAttr('id');
|
||
|
resItem.show();
|
||
|
resItem.find(".mini-profile-info").attr("data-screen-name", followingUsers[i]);
|
||
|
resItem.find(".following-screen-name").text(followingUsers[i]);
|
||
|
resItem.find("a.open-profile-modal").attr("href",$.MAL.userUrl(followingUsers[i]));
|
||
|
resItem.find("a.unfollow").attr("href",$.MAL.unfollowUrl(followingUsers[i]));
|
||
|
resItem.find("a.direct-messages-with-user").attr("href", $.MAL.dmchatUrl(followingUsers[i]));
|
||
|
resItem.find(".public-following").prop("checked",isPublicFollowing(followingUsers[i]));
|
||
|
getAvatar(followingUsers[i],resItem.find(".mini-profile-photo"));
|
||
|
getFullname(followingUsers[i],resItem.find(".mini-profile-name"));
|
||
|
|
||
|
resItem.appendTo($followingList);
|
||
|
}
|
||
|
$.MAL.followingListLoaded();
|
||
|
}
|
||
|
|
||
|
|
||
|
function processSuggestion(arg, suggestion, followedBy) {
|
||
|
var dashboard = $(".follow-suggestions");
|
||
|
if( suggestion ) {
|
||
|
var item = $("#follow-suggestion-template").clone(true);
|
||
|
item.removeAttr("id");
|
||
|
|
||
|
item.find(".twister-user-info").attr("data-screen-name", suggestion);
|
||
|
|
||
|
item.find(".twister-user-name").attr("href", $.MAL.userUrl(suggestion));
|
||
|
item.find(".twister-user-tag").text("@" + suggestion);
|
||
|
|
||
|
getAvatar(suggestion,item.find(".twister-user-photo"));
|
||
|
|
||
|
getFullname(suggestion,item.find(".twister-user"));
|
||
|
$spanFollowedBy = item.find(".followed-by");
|
||
|
$spanFollowedBy.text(followedBy);
|
||
|
getFullname(followedBy,$spanFollowedBy);
|
||
|
|
||
|
dashboard.append(item);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function closeSearchDialog()
|
||
|
{
|
||
|
var $this = $(".userMenu-search-field");//$( this );
|
||
|
$( this ).siblings().slideUp( "fast" );
|
||
|
}
|
||
|
|
||
|
function userSearchKeypress(item) {
|
||
|
var partialName = $(".userMenu-search-field").val().toLowerCase();
|
||
|
//var partialName = item.val();
|
||
|
|
||
|
if( !partialName.length ) {
|
||
|
closeSearchDialog();
|
||
|
} else {
|
||
|
if( _searchKeypressTimer !== undefined )
|
||
|
clearTimeout(_searchKeypressTimer);
|
||
|
|
||
|
if( _searchingPartialUsers.length ) {
|
||
|
_searchingPartialUsers = partialName;
|
||
|
} else {
|
||
|
_searchKeypressTimer = setTimeout( function() {
|
||
|
_searchKeypressTimer = undefined;
|
||
|
searchPartialUsername(partialName);
|
||
|
}, 600);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function searchPartialUsername(partialName) {
|
||
|
_searchingPartialUsers = partialName;
|
||
|
twisterRpc("listusernamespartial", [partialName,10],
|
||
|
function(partialName, ret) {
|
||
|
processDropdownUserResults(partialName, ret)
|
||
|
}, partialName,
|
||
|
function(cbArg, ret) {
|
||
|
console.log("ajax error:" + ret);
|
||
|
}, {});
|
||
|
}
|
||
|
|
||
|
function processDropdownUserResults(partialName, results){
|
||
|
|
||
|
if( partialName != _searchingPartialUsers ) {
|
||
|
searchPartialUsername( _searchingPartialUsers );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var typeaheadAccounts = $(".userMenu-search-profiles");
|
||
|
var template = $("#search-profile-template").detach();
|
||
|
|
||
|
typeaheadAccounts.empty();
|
||
|
typeaheadAccounts.append(template);
|
||
|
|
||
|
if( results.length ) {
|
||
|
for( var i = 0; i < results.length; i++ ) {
|
||
|
if( results[i] == defaultScreenName )
|
||
|
continue;
|
||
|
|
||
|
var resItem = template.clone(true);
|
||
|
resItem.removeAttr('id');
|
||
|
resItem.show();
|
||
|
resItem.find(".mini-profile-info").attr("data-screen-name", results[i]);
|
||
|
resItem.find(".mini-screen-name b").text(results[i]);
|
||
|
resItem.find("a.open-profile-modal").attr("href",$.MAL.userUrl(results[i]));
|
||
|
getAvatar(results[i],resItem.find(".mini-profile-photo"));
|
||
|
getFullname(results[i],resItem.find(".mini-profile-name"));
|
||
|
resItem.appendTo(typeaheadAccounts);
|
||
|
}
|
||
|
|
||
|
$.MAL.searchUserListLoaded();
|
||
|
} else {
|
||
|
closeSearchDialog();
|
||
|
}
|
||
|
_searchingPartialUsers = "";
|
||
|
}
|
||
|
|
||
|
function userClickFollow(e) {
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
var $this = $(this);
|
||
|
var $userInfo = $this.closest("[data-screen-name]");
|
||
|
var username = $userInfo.attr("data-screen-name");
|
||
|
|
||
|
follow(username, true, function() {
|
||
|
// delay reload so dhtput may do it's job
|
||
|
window.setTimeout("location.reload();",500);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function initUserSearch() {
|
||
|
var $userSearchField = $( ".userMenu-search-field" );
|
||
|
$userSearchField.keyup( userSearchKeypress );
|
||
|
$userSearchField.bind( "click", userSearchKeypress );
|
||
|
$userSearchField.clickoutside( closeSearchDialog );
|
||
|
|
||
|
$("button.follow").bind( "click", userClickFollow );
|
||
|
}
|
||
|
|
||
|
function followingListUnfollow(e) {
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
var $this = $(this);
|
||
|
var username = $this.closest(".mini-profile-info").attr("data-screen-name");
|
||
|
|
||
|
unfollow(username, function() {
|
||
|
showFollowingUsers();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function followingListPublicCheckbox(e) {
|
||
|
e.stopPropagation();
|
||
|
|
||
|
var $this = $(this);
|
||
|
var username = $this.closest(".mini-profile-info").attr("data-screen-name");
|
||
|
var public = false;
|
||
|
$this.toggleClass( "private" );
|
||
|
if( $this.hasClass( "private" ) ) {
|
||
|
$this.text( "Private" );
|
||
|
} else {
|
||
|
$this.text( "Public" );
|
||
|
public = true;
|
||
|
}
|
||
|
|
||
|
follow(username, public);
|
||
|
}
|
||
|
|
||
|
|
||
|
function requestSwarmProgress() {
|
||
|
twisterRpc("getlasthave", [defaultScreenName],
|
||
|
function(args, ret) {processSwarmProgressPartial(ret);}, null,
|
||
|
function(args, ret) {console.log("ajax error:" + ret);}, null);
|
||
|
}
|
||
|
|
||
|
function processSwarmProgressPartial(lastHaves)
|
||
|
{
|
||
|
if( defaultScreenName in lastHaves ) {
|
||
|
incLastPostId(lastHaves[defaultScreenName]);
|
||
|
}
|
||
|
|
||
|
twisterRpc("getnumpieces", [defaultScreenName],
|
||
|
function(args, ret) {processSwarmProgressFinal(args.lastHaves, ret);},
|
||
|
{lastHaves:lastHaves},
|
||
|
function(args, ret) {console.log("ajax error:" + ret);}, null);
|
||
|
}
|
||
|
|
||
|
function processSwarmProgressFinal(lastHaves, numPieces)
|
||
|
{
|
||
|
for( var user in lastHaves ) {
|
||
|
if( lastHaves.hasOwnProperty(user) && numPieces.hasOwnProperty(user) ) {
|
||
|
var $userDiv = $(".mini-profile-info[data-screen-name='" + user + "']");
|
||
|
if( $userDiv.length ) {
|
||
|
var $status = $userDiv.find(".swarm-status");
|
||
|
$status.text("Downloaded " + numPieces[user] + "/" + (lastHaves[user]+1) + " posts");
|
||
|
$status.fadeIn();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
window.setTimeout("requestSwarmProgress();",2000);
|
||
|
}
|
||
|
|
||
|
function followingChangedUser() {
|
||
|
followingUsers = [];
|
||
|
_isFollowPublic = {};
|
||
|
_followingSeqNum = 0;
|
||
|
_followSuggestions = [];
|
||
|
_lastLoadFromDhtTime = 0;
|
||
|
}
|
||
|
|
||
|
function initInterfaceFollowing() {
|
||
|
initInterfaceCommon();
|
||
|
initUserSearch();
|
||
|
initInterfaceDirectMsg();
|
||
|
|
||
|
$("button.unfollow").bind( "click", followingListUnfollow );
|
||
|
$(".public-following").bind( "click", followingListPublicCheckbox );
|
||
|
|
||
|
$(".mentions-from-user").bind( "click", openMentionsModal );
|
||
|
|
||
|
initUser( function() {
|
||
|
if( !defaultScreenName ) {
|
||
|
alert("Username undefined, login required.");
|
||
|
$.MAL.goLogin();
|
||
|
return;
|
||
|
}
|
||
|
checkNetworkStatusAndAskRedirect();
|
||
|
|
||
|
$(".postboard-loading").fadeIn();
|
||
|
loadFollowing( function(args) {
|
||
|
showFollowingUsers();
|
||
|
requestSwarmProgress();
|
||
|
});
|
||
|
initMentionsCount();
|
||
|
initDMsCount();
|
||
|
});
|
||
|
}
|