Merge pull request #323 from zeners/webui

New webui frontend based on Mithril.js. Thx to Zener.
This commit is contained in:
electron128 2016-03-28 10:19:10 +02:00
commit 68b4cd860e
77 changed files with 5986 additions and 40320 deletions

1
libresapi/src/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
webui/*

View File

@ -2,13 +2,13 @@ libresapi: resource_api and new webinterface
============================================
* ./api contains a C++ backend to control retroshare from webinterfaces or scripting
* ./webfiles contains compiled files for the webinterface
* ./webui contains HTML/CSS/JavaScript source files for the webinterface
* ./webui contains compiled files for the webinterface (after build)
* ./webui-src contains HTML/CSS/JavaScript source files for the webinterface (NEW, webinterface made with mithril.js)
Quickinfo for builders and packagers
====================================
* copy the files in ./webfiles to
* copy the files in ./webui to
* ./webui (Windows)
* /usr/share/RetroShare06/webui (Linux)
* other OS: see RsAccountsDetail::PathDataDirectory()

View File

@ -14,6 +14,7 @@
#include "StateTokenServer.h" // for the state token serialisers
#include "ApiPluginHandler.h"
#include "ChannelsHandler.h"
/*
data types in json http://json.org/
@ -226,13 +227,14 @@ class ApiServerMainModules
public:
ApiServerMainModules(ResourceRouter& router, StateTokenServer* sts, const RsPlugInInterfaces &ifaces):
mPeersHandler(sts, ifaces.mNotify, ifaces.mPeers, ifaces.mMsgs),
mIdentityHandler(ifaces.mIdentity),
mIdentityHandler(sts, ifaces.mNotify, ifaces.mIdentity),
mForumHandler(ifaces.mGxsForums),
mServiceControlHandler(ifaces.mServiceControl),
mFileSearchHandler(sts, ifaces.mNotify, ifaces.mTurtle, ifaces.mFiles),
mTransfersHandler(sts, ifaces.mFiles),
mChatHandler(sts, ifaces.mNotify, ifaces.mMsgs, ifaces.mPeers, ifaces.mIdentity, &mPeersHandler),
mApiPluginHandler(sts, ifaces)
mApiPluginHandler(sts, ifaces),
mChannelsHandler(ifaces.mGxsChannels)
{
// the dynamic cast is to not confuse the addResourceHandler template like this:
// addResourceHandler(derived class, parent class)
@ -254,6 +256,8 @@ public:
&ChatHandler::handleRequest);
router.addResourceHandler("apiplugin", dynamic_cast<ResourceRouter*>(&mApiPluginHandler),
&ChatHandler::handleRequest);
router.addResourceHandler("channels", dynamic_cast<ResourceRouter*>(&mChannelsHandler),
&ChannelsHandler::handleRequest);
}
PeersHandler mPeersHandler;
@ -264,6 +268,7 @@ public:
TransfersHandler mTransfersHandler;
ChatHandler mChatHandler;
ApiPluginHandler mApiPluginHandler;
ChannelsHandler mChannelsHandler;
};
ApiServer::ApiServer():

View File

@ -305,7 +305,7 @@ public:
// get content-type from extension
std::string ext = "";
unsigned int i = info.fname.rfind('.');
std::string::size_type i = info.fname.rfind('.');
if(i != std::string::npos)
ext = info.fname.substr(i+1);
MHD_add_response_header(resp, "Content-Type", ContentTypes::cTypeFromExt(ext).c_str());

View File

@ -0,0 +1,112 @@
#include "ChannelsHandler.h"
#include <retroshare/rsgxschannels.h>
#include <util/radix64.h>
#include <algorithm>
#include "Operators.h"
namespace resource_api
{
StreamBase& operator << (StreamBase& left, RsGxsFile& file)
{
left << makeKeyValueReference("name", file.mName)
<< makeKeyValueReference("hash", file.mHash);
if(left.serialise())
{
double size = file.mSize;
left << makeKeyValueReference("size", size);
}
else
{
double size = 0;
left << makeKeyValueReference("size", size);
file.mSize = size;
}
return left;
}
ChannelsHandler::ChannelsHandler(RsGxsChannels *channels):
mChannels(channels)
{
addResourceHandler("create_post", this, &ChannelsHandler::handleCreatePost);
}
ResponseTask* ChannelsHandler::handleCreatePost(Request &req, Response &resp)
{
RsGxsChannelPost post;
req.mStream << makeKeyValueReference("group_id", post.mMeta.mGroupId);
req.mStream << makeKeyValueReference("subject", post.mMeta.mMsgName);
req.mStream << makeKeyValueReference("message", post.mMsg);
StreamBase& file_array = req.mStream.getStreamToMember("files");
while(file_array.hasMore())
{
RsGxsFile file;
file_array.getStreamToMember() << file;
post.mFiles.push_back(file);
}
std::string thumbnail_base64;
req.mStream << makeKeyValueReference("thumbnail_base64_png", thumbnail_base64);
if(post.mMeta.mGroupId.isNull())
{
resp.setFail("groupd_id is null");
return 0;
}
if(post.mMeta.mMsgName.empty())
{
resp.setFail("subject is empty");
return 0;
}
if(post.mMsg.empty())
{
resp.setFail("msg text is empty");
return 0;
}
// empty file list is ok, but files have to be valid
for(std::list<RsGxsFile>::iterator lit = post.mFiles.begin(); lit != post.mFiles.end(); ++lit)
{
if(lit->mHash.isNull())
{
resp.setFail("at least one file hash is empty");
return 0;
}
if(lit->mName.empty())
{
resp.setFail("at leats one file name is empty");
return 0;
}
if(lit->mSize == 0)
{
resp.setFail("at least one file size is empty");
return 0;
}
}
std::vector<uint8_t> png_data = Radix64::decode(thumbnail_base64);
if(!png_data.empty())
{
if(png_data.size() < 8)
{
resp.setFail("Decoded thumbnail_base64_png is smaller than 8 byte. This can't be a valid png file!");
return 0;
}
uint8_t png_magic_number[] = {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
if(!std::equal(&png_magic_number[0],&png_magic_number[8],png_data.begin()))
{
resp.setFail("Decoded thumbnail_base64_png does not seem to be a png file. (Header is missing magic number)");
return 0;
}
post.mThumbnail.copy(png_data.data(), png_data.size());
}
uint32_t token;
mChannels->createPost(token, post);
// TODO: grp creation acknowledge
return 0;
}
} // namespace resource_api

View File

@ -0,0 +1,21 @@
#pragma once
#include "ResourceRouter.h"
class RsGxsChannels;
namespace resource_api
{
class ChannelsHandler : public ResourceRouter
{
public:
ChannelsHandler(RsGxsChannels* channels);
private:
ResponseTask* handleCreatePost(Request& req, Response& resp);
RsGxsChannels* mChannels;
};
} // namespace resource_api

View File

@ -97,6 +97,39 @@ StreamBase& operator << (StreamBase& left, ChatHandler::ChatInfo& info)
return left;
}
class SendLobbyParticipantsTask: public GxsResponseTask
{
public:
SendLobbyParticipantsTask(RsIdentity* idservice, ChatHandler::LobbyParticipantsInfo pi):
GxsResponseTask(idservice, 0), mParticipantsInfo(pi)
{
const std::map<RsGxsId, time_t>& map = mParticipantsInfo.participants;
for(std::map<RsGxsId, time_t>::const_iterator mit = map.begin(); mit != map.end(); ++mit)
{
requestGxsId(mit->first);
}
}
private:
ChatHandler::LobbyParticipantsInfo mParticipantsInfo;
protected:
virtual void gxsDoWork(Request &req, Response &resp)
{
resp.mDataStream.getStreamToMember();
const std::map<RsGxsId, time_t>& map = mParticipantsInfo.participants;
for(std::map<RsGxsId, time_t>::const_iterator mit = map.begin(); mit != map.end(); ++mit)
{
StreamBase& stream = resp.mDataStream.getStreamToMember();
double last_active = mit->second;
stream << makeKeyValueReference("last_active", last_active);
streamGxsId(mit->first, stream.getStreamToMember("identity"));
}
resp.mStateToken = mParticipantsInfo.state_token;
resp.setOk();
done();
}
};
ChatHandler::ChatHandler(StateTokenServer *sts, RsNotify *notify, RsMsgs *msgs, RsPeers* peers, RsIdentity* identity, UnreadMsgNotify* unread):
mStateTokenServer(sts), mNotify(notify), mRsMsgs(msgs), mRsPeers(peers), mRsIdentity(identity), mUnreadMsgNotify(unread), mMtx("ChatHandler::mMtx")
{
@ -111,11 +144,12 @@ ChatHandler::ChatHandler(StateTokenServer *sts, RsNotify *notify, RsMsgs *msgs,
addResourceHandler("lobbies", this, &ChatHandler::handleLobbies);
addResourceHandler("subscribe_lobby", this, &ChatHandler::handleSubscribeLobby);
addResourceHandler("unsubscribe_lobby", this, &ChatHandler::handleUnsubscribeLobby);
addResourceHandler("lobby_participants", this, &ChatHandler::handleLobbyParticipants);
addResourceHandler("messages", this, &ChatHandler::handleMessages);
addResourceHandler("send_message", this, &ChatHandler::handleSendMessage);
addResourceHandler("mark_chat_as_read", this, &ChatHandler::handleMarkChatAsRead);
addResourceHandler("info", this, &ChatHandler::handleInfo);
addResourceHandler("typing_label", this, &ChatHandler::handleTypingLabel);
addResourceHandler("receive_status", this, &ChatHandler::handleReceiveStatus);
addResourceHandler("send_status", this, &ChatHandler::handleSendStatus);
addResourceHandler("unread_msgs", this, &ChatHandler::handleUnreadMsgs);
}
@ -132,20 +166,21 @@ void ChatHandler::notifyChatMessage(const ChatMessage &msg)
mRawMsgs.push_back(msg);
}
// to be removed
/*
ChatHandler::Lobby ChatHandler::getLobbyInfo(ChatLobbyId id)
void ChatHandler::notifyChatStatus(const ChatId &chat_id, const std::string &status)
{
tick();
RS_STACK_MUTEX(mMtx); // ********* LOCKED **********
for(std::vector<Lobby>::iterator vit = mLobbies.begin(); vit != mLobbies.end(); ++vit)
if(vit->id == id)
return *vit;
std::cerr << "ChatHandler::getLobbyInfo Error: Lobby not found" << std::endl;
return Lobby();
RS_STACK_MUTEX(mMtx); /********** LOCKED **********/
locked_storeTypingInfo(chat_id, status);
}
void ChatHandler::notifyChatLobbyEvent(uint64_t lobby_id, uint32_t event_type,
const RsGxsId &nickname, const std::string& any_string)
{
RS_STACK_MUTEX(mMtx); /********** LOCKED **********/
if(event_type == RS_CHAT_LOBBY_EVENT_PEER_STATUS)
{
locked_storeTypingInfo(ChatId(lobby_id), any_string, nickname);
}
}
*/
void ChatHandler::tick()
{
@ -170,7 +205,44 @@ void ChatHandler::tick()
l.is_broadcast = false;
l.gxs_id = info.gxs_id;
lobbies.push_back(l);
// update the lobby participants list
// maybe it causes to much traffic to do this in every tick,
// because the client would get the whole list every time a message was received
// we could reduce the checking frequency
std::map<ChatLobbyId, LobbyParticipantsInfo>::iterator mit = mLobbyParticipantsInfos.find(*lit);
if(mit == mLobbyParticipantsInfos.end())
{
mLobbyParticipantsInfos[*lit].participants = info.gxs_ids;
mLobbyParticipantsInfos[*lit].state_token = mStateTokenServer->getNewToken();
}
else
{
LobbyParticipantsInfo& pi = mit->second;
if(!std::equal(pi.participants.begin(), pi.participants.end(), info.gxs_ids.begin()))
{
pi.participants = info.gxs_ids;
mStateTokenServer->replaceToken(pi.state_token);
}
}
}
}
// remove participants info of old lobbies
std::vector<ChatLobbyId> participants_info_to_delete;
for(std::map<ChatLobbyId, LobbyParticipantsInfo>::iterator mit = mLobbyParticipantsInfos.begin();
mit != mLobbyParticipantsInfos.end(); ++mit)
{
if(std::find(subscribed_ids.begin(), subscribed_ids.end(), mit->first) == subscribed_ids.end())
{
participants_info_to_delete.push_back(mit->first);
}
}
for(std::vector<ChatLobbyId>::iterator vit = participants_info_to_delete.begin(); vit != participants_info_to_delete.end(); ++vit)
{
LobbyParticipantsInfo& pi = mLobbyParticipantsInfos[*vit];
mStateTokenServer->discardToken(pi.state_token);
mLobbyParticipantsInfos.erase(*vit);
}
{
@ -439,6 +511,15 @@ void ChatHandler::getPlainText(const std::string& in, std::string &out, std::vec
}
}
void ChatHandler::locked_storeTypingInfo(const ChatId &chat_id, std::string status, RsGxsId lobby_gxs_id)
{
TypingLabelInfo& info = mTypingLabelInfo[chat_id];
info.timestamp = time(0);
info.status = status;
mStateTokenServer->replaceToken(info.state_token);
info.author_id = lobby_gxs_id;
}
void ChatHandler::handleWildcard(Request &/*req*/, Response &resp)
{
RS_STACK_MUTEX(mMtx); /********** LOCKED **********/
@ -496,11 +577,31 @@ void ChatHandler::handleSubscribeLobby(Request &req, Response &resp)
resp.setFail("lobby join failed. (See console for more info)");
}
void ChatHandler::handleUnsubscribeLobby(Request &req, Response &/*resp*/)
void ChatHandler::handleUnsubscribeLobby(Request &req, Response &resp)
{
ChatLobbyId id = 0;
req.mStream << makeKeyValueReference("id", id);
mRsMsgs->unsubscribeChatLobby(id);
resp.setOk();
}
ResponseTask* ChatHandler::handleLobbyParticipants(Request &req, Response &resp)
{
RS_STACK_MUTEX(mMtx); /********** LOCKED **********/
ChatId id(req.mPath.top());
if(!id.isLobbyId())
{
resp.setFail("Path element \""+req.mPath.top()+"\" is not a ChatLobbyId.");
return 0;
}
std::map<ChatLobbyId, LobbyParticipantsInfo>::const_iterator mit = mLobbyParticipantsInfos.find(id.toLobbyId());
if(mit == mLobbyParticipantsInfos.end())
{
resp.setFail("lobby not found");
return 0;
}
return new SendLobbyParticipantsTask(mRsIdentity, mit->second);
}
void ChatHandler::handleMessages(Request &req, Response &resp)
@ -573,89 +674,6 @@ void ChatHandler::handleMarkChatAsRead(Request &req, Response &resp)
mStateTokenServer->replaceToken(mUnreadMsgsStateToken);
}
// to be removed
// we do now cache chat info, to be able to include it in new message notify easily
/*
class InfoResponseTask: public GxsResponseTask
{
public:
InfoResponseTask(ChatHandler* ch, RsPeers* peers, RsIdentity* identity): GxsResponseTask(identity, 0), mChatHandler(ch), mRsPeers(peers), mState(BEGIN){}
enum State {BEGIN, WAITING};
ChatHandler* mChatHandler;
RsPeers* mRsPeers;
State mState;
bool is_broadcast;
bool is_gxs_id;
bool is_lobby;
bool is_peer;
std::string remote_author_id;
std::string remote_author_name;
virtual void gxsDoWork(Request& req, Response& resp)
{
ChatId id(req.mPath.top());
if(id.isNotSet())
{
resp.setFail("not a valid chat id");
done();
return;
}
if(mState == BEGIN)
{
is_broadcast = false;
is_gxs_id = false;
is_lobby = false;
is_peer = false;
if(id.isBroadcast())
{
is_broadcast = true;
}
else if(id.isGxsId())
{
is_gxs_id = true;
remote_author_id = id.toGxsId().toStdString();
requestGxsId(id.toGxsId());
}
else if(id.isLobbyId())
{
is_lobby = true;
remote_author_id = "";
remote_author_name = mChatHandler->getLobbyInfo(id.toLobbyId()).name;
}
else if(id.isPeerId())
{
is_peer = true;
remote_author_id = id.toPeerId().toStdString();
remote_author_name = mRsPeers->getPeerName(id.toPeerId());
}
else
{
std::cerr << "Error in InfoResponseTask::gxsDoWork(): unhandled chat_id=" << id.toStdString() << std::endl;
}
mState = WAITING;
}
else
{
if(is_gxs_id)
remote_author_name = getName(id.toGxsId());
resp.mDataStream << makeKeyValueReference("remote_author_id", remote_author_id)
<< makeKeyValueReference("remote_author_name", remote_author_name)
<< makeKeyValueReference("is_broadcast", is_broadcast)
<< makeKeyValueReference("is_gxs_id", is_gxs_id)
<< makeKeyValueReference("is_lobby", is_lobby)
<< makeKeyValueReference("is_peer", is_peer);
resp.setOk();
done();
}
}
};
ResponseTask *ChatHandler::handleInfo(Request &req, Response &resp)
{
return new InfoResponseTask(this, mRsPeers, mRsIdentity);
}
*/
void ChatHandler::handleInfo(Request &req, Response &resp)
{
RS_STACK_MUTEX(mMtx); /********** LOCKED **********/
@ -675,14 +693,99 @@ void ChatHandler::handleInfo(Request &req, Response &resp)
resp.setOk();
}
void ChatHandler::handleTypingLabel(Request &/*req*/, Response &/*resp*/)
class SendTypingLabelInfo: public GxsResponseTask
{
public:
SendTypingLabelInfo(RsIdentity* identity, RsPeers* peers, ChatId id, const ChatHandler::TypingLabelInfo& info):
GxsResponseTask(identity), mState(BEGIN), mPeers(peers),mId(id), mInfo(info) {}
private:
enum State {BEGIN, WAITING_ID};
State mState;
RsPeers* mPeers;
ChatId mId;
ChatHandler::TypingLabelInfo mInfo;
protected:
void gxsDoWork(Request& /*req*/, Response& resp)
{
if(mState == BEGIN)
{
// lobby and distant require to fetch a gxs_id
if(mId.isLobbyId())
{
requestGxsId(mInfo.author_id);
}
else if(mId.isDistantChatId())
{
DistantChatPeerInfo dcpinfo ;
rsMsgs->getDistantChatStatus(mId.toDistantChatId(), dcpinfo);
requestGxsId(dcpinfo.to_id);
}
mState = WAITING_ID;
}
else
{
std::string name = "BUG: case not handled in SendTypingLabelInfo";
if(mId.isPeerId())
{
name = mPeers->getPeerName(mId.toPeerId());
}
else if(mId.isDistantChatId())
{
DistantChatPeerInfo dcpinfo ;
rsMsgs->getDistantChatStatus(mId.toDistantChatId(), dcpinfo);
name = getName(dcpinfo.to_id);
}
else if(mId.isLobbyId())
{
name = getName(mInfo.author_id);
}
else if(mId.isBroadcast())
{
name = mPeers->getPeerName(mId.broadcast_status_peer_id);
}
uint32_t ts = mInfo.timestamp;
resp.mDataStream << makeKeyValueReference("author_name", name)
<< makeKeyValueReference("timestamp", ts)
<< makeKeyValueReference("status_string", mInfo.status);
resp.mStateToken = mInfo.state_token;
resp.setOk();
done();
}
}
};
ResponseTask* ChatHandler::handleReceiveStatus(Request &req, Response &resp)
{
RS_STACK_MUTEX(mMtx); /********** LOCKED **********/
ChatId id(req.mPath.top());
if(id.isNotSet())
{
resp.setFail("\""+req.mPath.top()+"\" is not a valid chat id");
return 0;
}
std::map<ChatId, TypingLabelInfo>::iterator mit = mTypingLabelInfo.find(id);
if(mit == mTypingLabelInfo.end())
{
locked_storeTypingInfo(id, "");
mit = mTypingLabelInfo.find(id);
}
return new SendTypingLabelInfo(mRsIdentity, mRsPeers, id, mit->second);
}
void ChatHandler::handleSendStatus(Request &/*req*/, Response &/*resp*/)
void ChatHandler::handleSendStatus(Request &req, Response &resp)
{
std::string chat_id;
std::string status;
req.mStream << makeKeyValueReference("chat_id", chat_id)
<< makeKeyValueReference("status", status);
ChatId id(chat_id);
if(id.isNotSet())
{
resp.setFail("chat_id is invalid");
return;
}
mRsMsgs->sendStatusString(id, status);
resp.setOk();
}
void ChatHandler::handleUnreadMsgs(Request &/*req*/, Response &resp)

View File

@ -26,6 +26,13 @@ public:
// note: this may get called from the own and from foreign threads
virtual void notifyChatMessage(const ChatMessage& msg);
// typing label for peer, broadcast and distant chat
virtual void notifyChatStatus (const ChatId& /* chat_id */, const std::string& /* status_string */);
//typing label for lobby chat, peer join and leave messages
virtual void notifyChatLobbyEvent (uint64_t /* lobby id */, uint32_t /* event type */ ,
const RsGxsId& /* nickname */,const std::string& /* any string */);
// from tickable
virtual void tick();
@ -81,6 +88,12 @@ public:
}
};
class LobbyParticipantsInfo{
public:
StateToken state_token;
std::map<RsGxsId, time_t> participants;
};
class ChatInfo{
public:
bool is_broadcast;
@ -91,20 +104,32 @@ public:
std::string remote_author_name;
};
class TypingLabelInfo{
public:
time_t timestamp;
std::string status;
StateToken state_token;
// only for lobbies
RsGxsId author_id;
};
private:
void handleWildcard(Request& req, Response& resp);
void handleLobbies(Request& req, Response& resp);
void handleSubscribeLobby(Request& req, Response& resp);
void handleUnsubscribeLobby(Request& req, Response& resp);
ResponseTask* handleLobbyParticipants(Request& req, Response& resp);
void handleMessages(Request& req, Response& resp);
void handleSendMessage(Request& req, Response& resp);
void handleMarkChatAsRead(Request& req, Response& resp);
void handleInfo(Request& req, Response& resp);
void handleTypingLabel(Request& req, Response& resp);
ResponseTask *handleReceiveStatus(Request& req, Response& resp);
void handleSendStatus(Request& req, Response& resp);
void handleUnreadMsgs(Request& req, Response& resp);
void getPlainText(const std::string& in, std::string &out, std::vector<Triple> &links);
// last parameter is only used for lobbies!
void locked_storeTypingInfo(const ChatId& chat_id, std::string status, RsGxsId lobby_gxs_id = RsGxsId());
StateTokenServer* mStateTokenServer;
RsNotify* mNotify;
@ -121,9 +146,13 @@ private:
std::map<ChatId, ChatInfo> mChatInfo;
std::map<ChatId, TypingLabelInfo> mTypingLabelInfo;
StateToken mLobbiesStateToken;
std::vector<Lobby> mLobbies;
std::map<ChatLobbyId, LobbyParticipantsInfo> mLobbyParticipantsInfos;
StateToken mUnreadMsgsStateToken;
};

View File

@ -4,6 +4,7 @@ class RsPlugInInterfaces;
namespace resource_api{
// populates the given RsPlugInInterfaces object with pointers from gloabl variables like rsPeers, rsMsgs, rsFiles...
bool getPluginInterfaces(RsPlugInInterfaces& interfaces);
} // namespace resource_api

View File

@ -62,6 +62,7 @@ bool GxsResponseTask::doWork(Request &req, Response &resp)
{
more = false; // pause when an id failed, to give the service time tim fetch the data
ready = false;
// TODO: remove identities which failed many times from list, to avoid blocking when ids fail
}
}
if(!ready)

View File

@ -15,7 +15,7 @@ class GxsResponseTask: public ResponseTask
{
public:
// token service is allowed to be null if no token functions are wanted
GxsResponseTask(RsIdentity* id_service, RsTokenService* token_service);
GxsResponseTask(RsIdentity* id_service, RsTokenService* token_service = 0);
virtual bool doWork(Request &req, Response& resp);
protected:

View File

@ -16,17 +16,18 @@ namespace resource_api
class SendIdentitiesListTask: public GxsResponseTask
{
public:
SendIdentitiesListTask(RsIdentity* idservice, std::list<RsGxsId> ids):
GxsResponseTask(idservice, 0)
SendIdentitiesListTask(RsIdentity* idservice, std::list<RsGxsId> ids, StateToken state):
GxsResponseTask(idservice, 0), mStateToken(state)
{
for(std::list<RsGxsId>::iterator vit = ids.begin(); vit != ids.end(); ++vit)
{
requestGxsId(*vit);
mIds.push_back(*vit);// convert fro list to vector
mIds.push_back(*vit);// convert from list to vector
}
}
private:
std::vector<RsGxsId> mIds;
StateToken mStateToken;
protected:
virtual void gxsDoWork(Request &req, Response &resp)
{
@ -35,17 +36,89 @@ protected:
{
streamGxsId(*vit, resp.mDataStream.getStreamToMember());
}
resp.mStateToken = mStateToken;
resp.setOk();
done();
}
};
IdentityHandler::IdentityHandler(RsIdentity *identity):
mRsIdentity(identity)
class CreateIdentityTask: public GxsResponseTask
{
public:
CreateIdentityTask(RsIdentity* idservice):
GxsResponseTask(idservice, idservice->getTokenService()), mState(BEGIN), mToken(0), mRsIdentity(idservice){}
private:
enum State {BEGIN, WAIT_ACKN, WAIT_ID};
State mState;
uint32_t mToken;
RsIdentity* mRsIdentity;
RsGxsId mId;
protected:
virtual void gxsDoWork(Request &req, Response &resp)
{
switch(mState)
{
case BEGIN:{
RsIdentityParameters params;
req.mStream << makeKeyValueReference("name", params.nickname) << makeKeyValueReference("pgp_linked", params.isPgpLinked);
if(params.nickname == "")
{
resp.setFail("name can't be empty");
done();
return;
}
mRsIdentity->createIdentity(mToken, params);
addWaitingToken(mToken);
mState = WAIT_ACKN;
break;
}
case WAIT_ACKN:{
RsGxsGroupId grpId;
if(!mRsIdentity->acknowledgeGrp(mToken, grpId))
{
resp.setFail("acknowledge of group id failed");
done();
return;
}
mId = RsGxsId(grpId);
requestGxsId(mId);
mState = WAIT_ID;
break;
}
case WAIT_ID:
streamGxsId(mId, resp.mDataStream);
resp.setOk();
done();
}
}
};
IdentityHandler::IdentityHandler(StateTokenServer *sts, RsNotify *notify, RsIdentity *identity):
mStateTokenServer(sts), mNotify(notify), mRsIdentity(identity),
mMtx("IdentityHandler Mtx"), mStateToken(sts->getNewToken())
{
mNotify->registerNotifyClient(this);
addResourceHandler("*", this, &IdentityHandler::handleWildcard);
addResourceHandler("own", this, &IdentityHandler::handleOwn);
addResourceHandler("create_identity", this, &IdentityHandler::handleCreateIdentity);
}
IdentityHandler::~IdentityHandler()
{
mNotify->unregisterNotifyClient(this);
}
void IdentityHandler::notifyGxsChange(const RsGxsChanges &changes)
{
RS_STACK_MUTEX(mMtx); // ********** LOCKED **********
// if changes come from identity service, invalidate own state token
if(changes.mService == mRsIdentity->getTokenService())
{
mStateTokenServer->replaceToken(mStateToken);
}
}
void IdentityHandler::handleWildcard(Request &req, Response &resp)
@ -54,6 +127,7 @@ void IdentityHandler::handleWildcard(Request &req, Response &resp)
if(req.isPut())
{
#ifdef REMOVE
RsIdentityParameters params;
req.mStream << makeKeyValueReference("name", params.nickname);
if(req.mStream.isOK())
@ -67,9 +141,14 @@ void IdentityHandler::handleWildcard(Request &req, Response &resp)
{
ok = false;
}
#endif
}
else
{
{
RS_STACK_MUTEX(mMtx); // ********** LOCKED **********
resp.mStateToken = mStateToken;
}
RsTokReqOptions opts;
opts.mReqType = GXS_REQUEST_TYPE_GROUP_DATA;
uint32_t token;
@ -126,12 +205,22 @@ void IdentityHandler::handleWildcard(Request &req, Response &resp)
ResponseTask* IdentityHandler::handleOwn(Request &req, Response &resp)
{
StateToken state;
{
RS_STACK_MUTEX(mMtx); // ********** LOCKED **********
state = mStateToken;
}
std::list<RsGxsId> ids;
if(mRsIdentity->getOwnIds(ids))
return new SendIdentitiesListTask(mRsIdentity, ids);
return new SendIdentitiesListTask(mRsIdentity, ids, state);
resp.mDataStream.getStreamToMember();
resp.setWarning("identities not loaded yet");
return 0;
}
ResponseTask* IdentityHandler::handleCreateIdentity(Request &req, Response &resp)
{
return new CreateIdentityTask(mRsIdentity);
}
} // namespace resource_api

View File

@ -1,19 +1,36 @@
#pragma once
#include <retroshare/rsnotify.h>
#include <util/rsthreads.h>
#include "ResourceRouter.h"
#include "StateTokenServer.h"
class RsIdentity;
namespace resource_api
{
class IdentityHandler: public ResourceRouter
class IdentityHandler: public ResourceRouter, NotifyClient
{
public:
IdentityHandler(RsIdentity* identity);
IdentityHandler(StateTokenServer* sts, RsNotify* notify, RsIdentity* identity);
virtual ~IdentityHandler();
// from NotifyClient
// note: this may get called from foreign threads
virtual void notifyGxsChange(const RsGxsChanges &changes);
private:
RsIdentity* mRsIdentity;
void handleWildcard(Request& req, Response& resp);
ResponseTask *handleOwn(Request& req, Response& resp);
ResponseTask *handleCreateIdentity(Request& req, Response& resp);
StateTokenServer* mStateTokenServer;
RsNotify* mNotify;
RsIdentity* mRsIdentity;
RsMutex mMtx;
StateToken mStateToken; // mutex protected
};
} // namespace resource_api

View File

@ -193,6 +193,8 @@ void PeersHandler::handleWildcard(Request &req, Response &resp)
ok &= mRsPeers->getPeerDetails(*lit, details);
detailsVec.push_back(details);
}
// mark response as list, in case it is empty
resp.mDataStream.getStreamToMember();
for(std::list<RsPgpId>::iterator lit = identities.begin(); lit != identities.end(); ++lit)
{
// if no own ssl id is known, then hide the own id from the friendslist

View File

@ -4,6 +4,8 @@
#include "Operators.h"
#include <stdlib.h>
namespace resource_api
{
// maybe move to another place later
@ -44,6 +46,7 @@ ServiceControlHandler::ServiceControlHandler(RsServiceControl* control):
mRsServiceControl(control)
{
addResourceHandler("*", this, &ServiceControlHandler::handleWildcard);
addResourceHandler("user", this, &ServiceControlHandler::handleUser);
}
void ServiceControlHandler::handleWildcard(Request &req, Response &resp)
@ -73,7 +76,42 @@ void ServiceControlHandler::handleWildcard(Request &req, Response &resp)
}
else if(req.isPut())
{
// change service default
std::string serviceidtext;
bool enabled;
req.mStream << makeKeyValueReference("service_id", serviceidtext)
<< makeKeyValueReference("default_allowed", enabled);
RsServicePermissions serv_perms ;
//uint32_t serviceid = fromString<uint32_t>(serviceidtext);
uint32_t serviceid = atoi(serviceidtext.c_str());
if (serviceid == 0) {
resp.setFail("service_id missed");
return;
}
if(!rsServiceControl->getServicePermissions(serviceid, serv_perms)){
resp.setFail("service_id " + serviceidtext + " is invalid");
return;
}
serv_perms.mDefaultAllowed = enabled;
if(serv_perms.mDefaultAllowed)
{
serv_perms.mPeersDenied.clear() ;
}
else
{
serv_perms.mPeersAllowed.clear() ;
}
ok = rsServiceControl->updateServicePermissions(serviceid,serv_perms);
if (!ok) {
resp.setFail("updateServicePermissions failed");
return;
}
}
}
if(ok)
@ -86,4 +124,62 @@ void ServiceControlHandler::handleWildcard(Request &req, Response &resp)
}
}
void ServiceControlHandler::handleUser(Request& req, Response& resp){
// no get, only put (post) to allow user or delete to remove user
std::string serviceidtext;
std::string peeridtext;
bool enabled;
bool ok;
req.mStream << makeKeyValueReference("service_id", serviceidtext)
<< makeKeyValueReference("peer_id", peeridtext)
<< makeKeyValueReference("enabled", enabled);
RsPeerId peer_id(peeridtext);
if (peer_id.isNull()) {
resp.setFail("peer_id missing or not found");
return;
}
RsServicePermissions serv_perms ;
uint32_t serviceid = atoi(serviceidtext.c_str());
if (serviceid == 0) {
resp.setFail("service_id missed");
return;
}
if(!rsServiceControl->getServicePermissions(serviceid, serv_perms)){
resp.setFail("service_id " + serviceidtext + " is invalid");
return;
}
if(req.isPut())
{
if (enabled && !serv_perms.peerHasPermission(peer_id))
{
serv_perms.setPermission(peer_id);
} else if (!enabled && serv_perms.peerHasPermission(peer_id)){
serv_perms.resetPermission(peer_id);
} else {
//nothing todo
resp.setOk();
return;
}
} else {
resp.setFail("only POST supported.");
return;
}
ok = rsServiceControl->updateServicePermissions(serviceid,serv_perms);
if (!ok) {
resp.setFail("updateServicePermissions failed");
return;
}
resp.setOk();
}
} // namespace resource_api

View File

@ -16,5 +16,6 @@ public:
private:
RsServiceControl* mRsServiceControl;
void handleWildcard(Request& req, Response& resp);
void handleUser(Request& req, Response& resp);
};
} // namespace resource_api

View File

@ -114,6 +114,7 @@ void StateTokenServer::handleWildcard(Request &req, Response &resp)
RsStackMutex stack(mMtx); /********** STACK LOCKED MTX ******/
// want to lookpup many tokens at once, return a list of invalid tokens
// TODO: make generic list serialiser/deserialiser
resp.mDataStream.getStreamToMember();
while(req.mStream.hasMore())
{
StateToken token;

View File

@ -13,6 +13,7 @@ CONFIG += libmicrohttpd
INCLUDEPATH += ../../libretroshare/src
unix {
webui_files.path = "$${DATA_DIR}/webui"
webui_files.files = webfiles/*
INSTALLS += webui_files
@ -20,6 +21,13 @@ unix {
webui_img_files.path = "$${DATA_DIR}/webui/img"
webui_img_files.files = ../../retroshare-gui/src/gui/images/logo/logo_splash.png
INSTALLS += webui_img_files
create_webfiles.commands = sh $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_
QMAKE_EXTRA_TARGETS += create_webfiles
PRE_TARGETDEPS += create_webfiles
# create dummy files
system(webui-src/make-src/init.sh .)
}
win32{
@ -66,7 +74,8 @@ SOURCES += \
api/LivereloadHandler.cpp \
api/TmpBlobStore.cpp \
util/ContentTypes.cpp \
api/ApiPluginHandler.cpp
api/ApiPluginHandler.cpp \
api/ChannelsHandler.cpp
HEADERS += \
api/ApiServer.h \
@ -91,4 +100,5 @@ HEADERS += \
api/LivereloadHandler.h \
api/TmpBlobStore.h \
util/ContentTypes.h \
api/ApiPluginHandler.h
api/ApiPluginHandler.h \
api/ChannelsHandler.h

File diff suppressed because one or more lines are too long

View File

@ -1,109 +0,0 @@
/**
* JS Api for Retroshare
* @constructor
* @param {object} connection - an object which implements a request() function.
* The request function should take two parameters: an object to be send as request and a callback.
* The callback should get called with an response object on success.
*/
function RsApi(connection)
{
var runnign = true;
/**
* Send a request to the server
* @param req - the request so send
* @param {Function} cb - callback function which takes the response as parameter
*/
this.request = function(req, cb)
{
connection.request(req, cb);
};
var tokenlisteners = [];
/**
* Register a callback to be called when the state token expired.
* @param {Function} listener - the callback function, which does not take arguments
* @param token - the state token to listen for
*/
this.register_token_listener = function(listener, token)
{
tokenlisteners.push({listener:listener, token:token});
};
/**
* Unregister a previously registered callback.
*/
this.unregister_token_listener = function(listener) // no token as parameter, assuming unregister from all listening tokens
{
var to_delete = [];
for(var i=0; i<tokenlisteners.length; i++){
if(tokenlisteners[i].listener === listener){
to_delete.push(i);
}
}
for(var i=0; i<to_delete.length; i++){
// copy the last element to the current index
var index = to_delete[i];
tokenlisteners[index] = tokenlisteners[tokenlisteners.length-1];
// remove last element
tokenlisteners.pop();
}
};
/**
* start polling for state changes
*/
this.start = function(){
running = true;
setTimeout(tick, TICK_INTERVAL);
}
/**
* stop polling for state changes
*/
this.stop = function(){
running = false;
}
// ************** interal stuff **************
var TICK_INTERVAL = 3000;
function received_tokenstates(resp)
{
if(resp.data){
for(var i=0; i<resp.data.length; i++){
var token = resp.data[i];
// search the listener for this token
for(var j=0; j<tokenlisteners.length; j++){
if(tokenlisteners[j].token === token){
// call the listener
tokenlisteners[j].listener();
}
}
}
}
// schedule new update
if(running)
setTimeout(tick, TICK_INTERVAL);
};
function received_error()
{
// try again, maybe want a better logic later
if(running)
setTimeout(tick, TICK_INTERVAL);
};
function tick()
{
var data = [];
// maybe cache the token list?
// profiler will tell us if we should
for(var i=0; i<tokenlisteners.length; i++){
data.push(tokenlisteners[i].token);
}
connection.request({
path: "statetokenservice",
data: data,
}, received_tokenstates, received_error);
};
};
// with this trick, we should be able to run in browser or nodejs
if(typeof window === 'undefined')
{
// we are running in nodejs, so have to add to export
module.exports = RsApi;
}

View File

@ -1,123 +0,0 @@
/**
* Connection to the RS backend using XHR
* (could add other connections later, for example WebSockets)
* @constructor
*/
function RsXHRConnection(server_hostname, server_port)
{
var debug;
//debug = function(str){console.log(str);};
debug = function(str){};
//server_hostname = "localhost";
//server_port = "9090";
var api_root_path = "/api/v2/";
var status_listeners = [];
function notify_status(status)
{
for(var i = 0; i < status_listeners.length; i++)
{
status_listeners[i](status);
}
}
/**
* Register a callback to be called when the state of the connection changes.
* @param {function} cb - callback which receives a single argument. The arguments value is "connected" or "not_connected".
*/
this.register_status_listener = function(cb)
{
status_listeners.push(cb);
};
/**
* Unregister a status callback function.
* @param {function} cb - a privously registered callback function
*/
this.unregister_status_listener = function(cb)
{
var to_delete = [];
for(var i = 0; i < status_listeners.length; i++)
{
if(status_listeners[i] === cb){
to_delete.push(i);
}
}
for(var i = 0; i < to_delete.length; i++)
{
// copy the last element to the current index
var index = to_delete[i];
status_listeners[i] = status_listeners[status_listeners.length-1];
// remove the last element
status_listeners.pop();
}
};
/**
* Send a request to the backend
* automatically encodes the request as JSON before sending it to the server
* @param {object} req - the request to send to the server
* @param {function} cb - callback function to be called to handle the response. The callback takes one object as parameter. Can be left undefined.
* @param {function} err_cb - callback function to signal a failed request. Can be undefined.
*/
this.request = function(req, cb, err_cb)
{
//var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
// TODO: window is not available in QML
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
//console.log("onreadystatechanged state"+xhr.readyState);
// TODO: figure out how to catch errors like connection refused
// maybe want to have to set a state variable like ok=false
// the gui could then display: "no connection to server"
if (xhr.readyState === 4) {
if(xhr.status !== 200)
{
console.log("RsXHRConnection: request failed with status: "+xhr.status);
console.log("request was:");
console.log(req);
notify_status("not_connected");
if(err_cb !== undefined)
err_cb();
return;
}
// received response
notify_status("connected");
debug("RsXHRConnection received response:");
debug(xhr.responseText);
if(false)//if(xhr.responseText === "")
{
debug("Warning: response is empty");
return;
}
try
{
var respObj = JSON.parse(xhr.responseText);
}
catch(e)
{
debug("Exception during response handling: "+e);
}
if(cb === undefined)
debug("No callback function specified");
else
cb(respObj);
}
}
// post is required for sending data
var method;
if(req.data){
method = "POST";
} else {
method = "GET";
}
xhr.open(method, "http://"+server_hostname+":"+server_port+api_root_path+req.path);
var data = JSON.stringify(req.data);
debug("RsXHRConnection sending data:");
debug(data);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(data);
};
};

View File

@ -1,134 +0,0 @@
body {
background-color: black;
color: lime;
font-family: monospace;
margin: 0em;
/*padding: 1.5em;*/
padding: 2mm;
font-size: 1.1em;
}
#overlay{
z-index: 10;
position: fixed;
top:0;
left:0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.8);
}
.paddingbox{
padding:2mm;
}
.nav{
list-style-type: none;
padding: 0em;
margin: 0em;
}
.nav li{
display: inline;
padding: 0.1em;
margin-right: 1em;
border-width: 0.1em;
border-color: blue;
border-bottom-style: solid;
cursor: pointer;
}
td{
padding: 0.3em;
border-style: solid;
border-width: 0.1em;
border-color: lime;
}
.btn{
border-style: solid;
border-color: lime;
border-width: 0.1em;
cursor: pointer;
padding: 0.1em;
}
.btn2, .box{
border-style: solid;
/*border-color: lime;*/
border-color: limeGreen;
/*border-width: 1px;*/
border-radius: 3mm;
padding: 2mm;
font-size: 10mm;
cursor: pointer;
margin-bottom: 2mm;
}
.btn2:hover{
background-color: midnightblue;
}
.filelink{
color: inherit;
}
input, textarea{
color: lime;
font-family: monospace;
background-color: black;
border-color: lime;
font-size: 10mm;
border-radius: 3mm;
border-width: 1mm;
padding: 2mm;
margin-bottom: 2mm;
margin-right: 2mm;
/* make the button the whole screen width */
width: 100%;
/* make the text input fit small screens*/
box-sizing: border-box;
}
input:hover{
background-color: midnightblue;
}
.checkbox {
width: auto;
}
.flexbox{
display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */
display: -ms-flexbox; /* TWEENER - IE 10 */
display: -webkit-flex; /* NEW - Chrome */
display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
.flexwidemember{
-webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */
-moz-box-flex: 1; /* OLD - Firefox 19- */
width: 20%; /* For old syntax, otherwise collapses. */
-webkit-flex: 1; /* Chrome */
-ms-flex: 1; /* IE 10 */
flex: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
#logo_splash{
-webkit-animation-fill-mode: forwards; /* Chrome, Safari, Opera */
animation-fill-mode: forwards;
-webkit-animation-name: logo_splash; /* Chrome, Safari, Opera */
-webkit-animation-duration: 3s; /* Chrome, Safari, Opera */
animation-name: logo_splash;
animation-duration: 3s;
text-align: center;
}
/* Chrome, Safari, Opera */
@-webkit-keyframes logo_splash {
from {opacity: 0;}
to {opacity: 1;}
}
/* Standard syntax */
@keyframes logo_splash {
from {opacity: 0;}
to {opacity: 1;}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>New webinterface for Retroshare</title>
<script src="RsXHRConnection.js"></script>
<script src="RsApi.js"></script>
<!-- it seems to work more reliable, if the jsx file is loaded before react -->
<script type="text/jsx" src="gui.jsx"></script>
<script src="react.js"></script>
<script src="JSXTransformer.js"></script>
<link href="green-black.css" rel="stylesheet">
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1">
</head>
<body>
<script>
document.write("<p>loading lots of stuff...</p>");
</script>
<p><noscript>The Retroshare web interface requires JavaScript. Please enable JavaScript in your browser.</noscript></p>
<!--<div id="logo_splash">
<img src="img/logo_splash.png"></img>
</div>-->
</body>
</html>

File diff suppressed because it is too large Load Diff

2
libresapi/src/webui-src/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/*
public/*

View File

@ -0,0 +1,78 @@
A new approach to build a webinterface for RS
=============================================
1. get JSON encoded data from the backend, data contains a state token
2. render data with mithril.js
3. ask the backend if the state token from step 1 expired. If yes, then start again with step 1.
Steps 1. and 3. are common for most things, only Step 2. differs. This allows to re-use code for steps 1. and 3.
BUILD / DEVELOPMENT
------------
- install tools
sudo apt-get install TODO (insert package names for nodejs, ruby, sass here)
- run this once in webui-src directory, to install more tools
npm install
- start build
npm run watch
- the build process watches files for changes, and rebuilds and reloads the page. Build output is in ./public
- use the --webinterface 9090 command line parameter to enable webui in retroshare-nogui
- set the --docroot parameter of retroshare-nogui to point to the "libresapi/src/webui-src/public" directory
(or symlink from /usr/share/RetroShare06/webui on Linux, ./webui on Windows)
- retroshare-gui does not have a --docroot parameter. Use symlinks then.
CONTRIBUTE
----------
- if you are a web developer or want to become one
get in contact!
- lots of work to do, i need you!
TODO
----
[ ] make stylesheets or find reusable sass/css components
google material design has nice rules for color, spacing and everything: https://www.google.de/design/spec/material-design/introduction.html
[ ] find icons, maybe use google material design iconfont
[X] use urls/mithril routing for the menu. urls could replace state stored in rs.content
[X] drag and drop private key upload and import
[X] link from peer location to chat (use urls and mithril routing)
[X] add/remove friend, own cert
[X] downloads, search
[ ] make reusable infinite list controller, the js part to load data from Pagination.h (tweak Pagination.h to make everything work)
should provide forward, backward and follow-list-end
[ ] backend: view/create identities
[ ] backend: chat lobby participants list
[X] chat: send_message
[ ] backend: chat typing notifications
[ ] make routines to handle retroshare links
[ ] backend: edit shared folders
[ ] backend: view shared files
[ ] redirect if a url is not usable in the current runstate (e.g. redirect from login page to home page, after login)
[X] sort friendslist
need 4 master
-------------
[X] unsubscribe lobby
[X] unread chat message counter in menu
[X] list chat-lobby participants
[X] creating app.js on build (no need for npm on regulary build)
url-handling (brainstorming)
----------------------------
* normal weblinks (bbcode? => only with gui support)
* rslinks
- files
- (chatrooms)
- forum retroshare://forum?name=Developers%27%20Discussions&id=8fd22bd8f99754461e7ba1ca8a727995
- own cert link (paste)
- cert-links
- searches
- [X] downloads pasten
- uploads?
* enter / display urls
- use urls in href like used for input (so it can be copy-link)
- handle RS-urls with javascript, other with target _blank
* smilies
* Bilder
* KEEP IT SIMPLE

View File

@ -0,0 +1,81 @@
.chat
$color: black
$header_height: 50px
$left_width: 200px
$right_width: 200px
$input_height: 100px
padding: 15px
&.container
height: 100%
padding: 0px
position: relative
box-sizing: border-box
&.header
position: absolute
top: 0px
left: 0px
right: 0px
height: $header_height
background-color: $color
border-bottom: solid 1px gray
box-sizing: border-box
&.left
position: absolute
top: $header_height
bottom: 0px
left: 0px
width: $left_width
//border-right: solid 1px gray
box-sizing: border-box
background-color: black
&.right
position: absolute
top: $header_height
right: 0px
bottom: 0px
width: $right_width
box-sizing: border-box
//border-left: solid 1px gray
&.middle
//background-color: blue
position: absolute
top: 0px
margin-top: $header_height
left: $left_width
right: $right_width
box-sizing: border-box
padding: 0px
height: 100%
overflow-y: scroll
&.bottom
position: absolute
bottom: 0px
right: $right_width
left: $left_width
padding: 5px
&.msg
padding: 0px
$author_width: 100px
&.container
position: relative
border-bottom: solid 1px lightgray
padding: 10px
height: unset
//background-color: lime
&.from
position: absolute
width: $author_width
top: 10px
left: 0px
color: white
text-align: right
&.when
float: right
color: lightgray
margin-bottom: 10px
&.text
padding-left: $author_width
top: 0px
left: $author_width
white-space: pre-wrap
height: initial

View File

@ -0,0 +1,48 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

View File

@ -0,0 +1,68 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
function cancel(){
rs.memory("control/locations").curraccount=null;
m.redraw();
}
function selAccount(account){
rs.memory("control/locations").curraccount=account;
m.redraw();
rs.request("control/login", {id: account.id}, function(){
console.log("login sent");
});
}
function curraccount() {
var mem;
mem = rs.memory("control/locations");
if (mem.curraccount === undefined) {
return null;
}
return mem.curraccount;
}
module.exports = {view: function(){
var accounts = rs("control/locations");
if(accounts === undefined || accounts == null){
return m("div", "accounts: waiting_server");
}
if (curraccount() == null) {
return m("div", [
m("h2","login:"),
m("hr"),
accounts.map(function(account){
return [
m("div.btn2", {
onclick: function(){
selAccount(account)
}
},
account.location + " (" + account.name + ")"),
m("br")
]
})
]);
} else {
// rs.untoken("control/password");
return m("div", [
m("div", [
"logging in ...",
m("br"),
"(waiting for password-request)",
]),
/*
m("hr"),
m(".btn2", {
onclick: function() {
cancel();
}
},"Cancel " + curraccount().name + " login "),
*/
]);
}
}
};

View File

@ -0,0 +1,299 @@
var m = require("mithril");
var rs = require("retroshare");
var me = {
toParse: [], // links to parse ( = pasted content)
toConfirm: [], // links to confirm
toAdd: [], // links to add
toResult: [], // Result to show
index: 0,
view: function(){
return m("div", {
style: {
height:"100%",
boxSizing: "border-box",
paddingBottom: "130px",
}
},[
m("h2","add downloads"),
m("hr"),
this.toParse.length
? step2()
: this.toConfirm.length
? step3()
: this.toAdd.length
? step4()
: this.toResult.length
? step5()
: step1()
,
]);
},
parseOne: function(){
if (me.index == null) {
return null;
}
var startindex = me.index;
while (me.toParse.length > me.index && me.index - startindex < 10) {
var src = me.toParse[me.index].split("?",2);
console.log(src);
if (src[0] == "retroshare://file" && src.length == 2) {
var target = {action: "begin"};
var errText = "Error: link missing name and/or hash"
src[1].split("&").map(function(parm){
var pos = parm.indexOf("=");
if (pos >0){
if (parm.substr(0,pos) == "name") {
var sname=decodeURIComponent(parm.substr(pos+1))
if (sname.match("[\\\\/]")) {
errText="name contains illegal char "
+ sname.match("[\\\\/]");
} else {
target.name=sname;
}
} else if (parm.substr(0,pos) == "size") {
target.size=parseFloat(parm.substr(pos+1));
} else if (parm.substr(0,pos) == "hash") {
target.hash=parm.substr(pos+1);
}
}
});
if (target['name'] && target['hash']){
me.toConfirm.push({
text: target.name,
target: target,
confirmed: true,
});
} else {
me.toConfirm.push({
text:errText,
});
}
} else {
me.toConfirm.push({ text: "Error: no Retroshare-file link"})
}
me.index++;
}
if (me.toParse.length > me.index) {
window.setTimeout("require(\"adddownloads\").parseOne()",1);
} else {
me.toParse = [];
console.log(me.toConfirm.length);
}
refresh();
},
addOne: function(){
if (me.index == null) {
cancel();
} else if (me.index >= me.toAdd.length) {
me.toResult=me.toAdd;
me.toAdd=[];
refresh();
} else {
console.log([
me.toAdd[me.index].action,
me.toAdd[me.index].name,
me.toAdd[me.index].size,
me.toAdd[me.index].hash,
]);
refresh();
rs.request("transfers/control_download", me.toAdd[me.index],
function(data,statetoken){
if (me.index != null) {
me.toAdd[me.index].ok=true;
me.index++;
me.addOne();
}
}, {
onfail: function(value){
me.toAdd[me.index].ok=false;
me.toAdd[me.index].debug_msg=value;
me.index++;
me.addOne();
},
onmismatch: function(response){
me.toAdd[me.index].ok=false;
me.toAdd[me.index].debug_msg=response.debug_msg;
me.index++;
me.addOne();
},
}
);
}
}
};
function cancel() {
me.toAdd=[];
me.toConfirm=[];
me.toParse=[];
me.toResult=[];
me.index=null;
refresh();
}
function parseDownloads(){
me.toParse = document.getElementById("txtInput").value.replace("\r\n","\n").split("\n");
var pos;
while ((pos=me.toParse.indexOf(""))>=0) {
me.toParse.splice(pos,1);
}
var parser = document.createElement('a');
me.toConfirm = [];
me.index = 0;
if (me.toParse.length > me.index){
window.setTimeout("require(\"adddownloads\").parseOne()",1);
}
}
function addDownloads(){
me.toConfirm.map(function(item){
if (item.confirmed) {
item.debug_msg="";
me.toAdd.push(item.target);
}
});
me.toConfirm=[];
if (me.toAdd.length > 0){
me.index=0;
window.setTimeout("require(\"adddownloads\").addOne()",1);
} else {
cancel();
}
refresh();
}
function refresh(){
m.startComputation();
m.endComputation();
}
function cancelBtn(){
return m("div.btn2", {
style:{
textAlign: "center",
color: "red",
borderColor: "red",
},
onclick:cancel,
},"cancel");
}
// paste links
function step1(){
m.initControl = "txtInput";
return [
m("h3","step 1 / 5: paste retroshare-links:"),
m("textarea[id=txtInput]", {
style: {
height:"100%",
},
onkeydown: function(event){
if (event.keyCode == 13){
parseDownloads();
}
}
}),
m("div.btn2", {
style:{
textAlign:"center",
},
onclick:parseDownloads,
},"add downloads")
]
}
// parsing links
function step2(){
return [
m("h3","step 2 / 5: parsing input ..."),
m("p",
"parsing " + (me.index) + " / " + me.toParse.length),
cancelBtn(),
]
}
// parsing confirm
function step3(){
return [
m("h3","step 3 / 5: confirm-links:"),
m("ul",
me.toConfirm.map(function(item){
return m("li", {
style:{
color: item.confirmed
? "lime"
: "red"
},
}, item.text);
})
),
m("div.btn2", {
style:{
textAlign:"center",
},
onclick:addDownloads,
},"add green listed downloads"),
cancelBtn(),
]
}
// adding links
function step4(){
return [
m("h3","step 4 / 5: adding downloads:"),
m("p",
"adding " + (me.index) + " / " + me.toParse.length),
m("ul",
me.toAdd.map(function(item){
return m("li", {
style:{
color: item.ok === undefined
? "white"
: item.ok == null
? "grey"
: item.ok
? "lime"
: "red"
},
}, (item.debug_msg ? item.debug_msg + ": " : "") + item.name
+ " " + item.size + " " + item.hash);
})
),
cancelBtn(),
]
}
// show result
function step5(){
return [
m("h3","step 5 / 5: Result:"),
m("p",
"verarbeitet: " + me.toResult.length),
m("ul",
me.toResult.map(function(item){
return m("li", {
style:{
color: item.ok === undefined
? "white"
: item.ok == null
? "grey"
: item.ok
? "lime"
: "red"
},
}, (item.debug_msg ? item.debug_msg + ": " : "") + item.name);
})
),
m("div.btn2", {
style:{
textAlign: "center",
},
onclick: cancel,
},"ok"),
]
}
module.exports = me;

View File

@ -0,0 +1,54 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
function createidentity(){
var data = {
name: document.getElementById("txtname").value,
pgp_linked: false,
//document.getElementById("chklinked").checked,
};
m.route("/waiting");
rs.request("identity/create_identity",data, function(){
m.route("/identities");
})
}
module.exports = {view: function(){
m.initControl = "txtname";
return m("div",
m("h2","create identity"),
m("hr"),
m("h3","name"),
m("input", {
type: "text",
id: "txtname",
/*
onkeydown: function(event){
if (event.keyCode == 13){
setPasswd(this.value);
sendPassword(needpasswd);
}
}
*/
}),
/*
m("b","linked with pgp-id: "),
m("input", {
type: "checkbox",
id: "chklinked",
style: {
fontweight:"bold",
width: "0%",
}
}),
*/
m("p"," "),
m("input.btn2", {
onclick: createidentity,
type: "button",
value: "create new identity",
})
)
}}

View File

@ -0,0 +1,151 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
var newkey = "";
var remote = "";
module.exports = {
view: function(){
var key = m.route.param("radix");
var pgp = m.route.param("pgp_id");
var peer_id =m.route.param("peer_id");
if (key===undefined && pgp === undefined) {
var owncert = rs("peers/self/certificate");
if (owncert === undefined ) {
owncert = {cert_string:"< waiting for server ... >"}
}
return m("div", [
m("h2","add new friend (Step 1/3)"),
m("p","Your own key, give it to your friends"),
m("pre", owncert.cert_string),
m("p","paste your friends key below"),
m("textarea", {
ref:"cert",
cols:"70",
rows:"16",
onchange: m.withAttr("value", function(value){newkey=value;})
}),
m("br"),
m("input.btn2",{
type:"button",
value:"read",
onclick: function (){
m.route("/addpeer",{radix:newkey})
},
})
]);
} else if (pgp === undefined) {
rs.request("peers/examine_cert",{cert_string:key},
function(data,responsetoken){
data.radix=key;
m.route("/addpeer", data);
}
);
return m("div", [
m("h2","add new friend (Step 2/3)"),
m("div", "analyse cert, please wait for server ...")
// { data: null, debug_msg: "failed to load certificate ", returncode: "fail" }
]);
} else {
var result = {
cert_string:key ,
flags:{
allow_direct_download:false,
allow_push:false,
// set to false, until the webinterface supports managment of the blacklist/whitelist
require_whitelist: false,
}
};
return m("div",[
m("h2","add new friend (Step 3/3)"),
m("p","Do you want to add "
+ m.route.param("name")
+ " (" + m.route.param("location") + ")"
+ " to your friendslist?"),
m("input.checkbox[type=checkbox]", {
onchange: m.withAttr("checked", function(checked){
result.flags.allow_direct_download=checked;
})
}), "Allow direct downloads from this node",
m("br"),
m("input.checkbox[type=checkbox]", {
onchange: m.withAttr("checked", function(checked){
result.flags.allow_push=checked;
})
}), "Auto download recommended files from this node",
m("div.btn2",{
onclick: function(){
m.route("/waiting");
rs.request("peers",result,function(data, responsetoken){
m.route("/peers");
})
}
},"add to friendslist")
])
}
}
};
/*
return "peers/self/certificate"
RS.request({path: "peers/examine_cert", data: {cert_string: cert_string}}, this.examine_cert_callback);
this.setState({page:"waiting", cert_string: cert_string});
RS.request(
{
path: "peers",
data: {
cert_string: this.state.cert_string,
flags:{
allow_direct_download: this.refs.cb_direct_dl.getDOMNode().checked,
allow_push: this.refs.cb_push.getDOMNode().checked,
// set to false, until the webinterface supports managment of the blacklist/whitelist
require_whitelist: false,
}
}
});
render: function(){
if(this.state.page === "start")
return(
<div>
<p>Your own key, give it to your friends</p>
<OwnCert/>
<p>paste your friends key below</p>
<textarea ref="cert" cols="70" rows="16"></textarea><br/>
<input
type="button"
value="read key"
onClick={this.add_friend_handler}
/>
</div>
);
if(this.state.page === "waiting")
return(
<div>
waiting for response from server...
</div>
);
if(this.state.page === "peer")
return(
<div>
<p>Do you want to add {this.state.data.name} to your friendslist?</p>
<input className="checkbox" type="checkbox" ref="cb_direct_dl"/> Allow direct downloads from this node<br/>
<input className="checkbox" type="checkbox" ref="cb_push"/> Auto download recommended files from this node<br/>
<div onClick={this.final_add_handler} className="btn2">add to friendslist</div>
</div>
);
},
*/

View File

@ -0,0 +1,23 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>rswebui5</title>
<link rel="stylesheet" href="app.css">
<script src="app.js"></script>
</head>
<body onload="load_ui();">
<div id="main">if app does not load, enable JavaScript!</div>
<script type="text/javascript">
function load_ui(){
var m = require("mithril");
var ui = require("main");
var main = document.getElementById("main");
ui.init(main);
if (m.initControl != undefined) {
m.initControl.focus();
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,265 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
var msg = null;
var particips = [];
function dspmsg(from, when, text){
return m(".chat.msg.container",[
m(".chat.msg.from", from),
m(".chat.msg.when", when),
m(".chat.msg.text", text),
]);
}
function lobbies(){
return [
rs.list("chat/lobbies",function(lobby){
return m("div.btn",{
title: "topic: " + lobby.topic + "\n"
+ "subscribed: " + lobby.subscribed,
style: {
backgroundColor: lobby.subscribed ? 'blue' : 'darkred',
},
onclick: function(){
m.route("/chat?lobby=" + lobby.chat_id);
}
},
lobby.name + (
lobby.unread_msg_count > 0
? ("(" + lobby.unread_msg_count + ")")
: "")
);
},
rs.sort.bool("is_broadcast",
rs.sort.bool("subscribed",
rs.sort("name")))
),
m("br"),
m("h3","peers:"),
rs.list("peers",function(peer){
return peer.locations.map(function(loc){
if (loc.location == "") {
return [];
};
return m("div.btn",{
style: {
backgroundColor: loc.is_online ? 'blue' : 'darkred',
},
onclick: function(){
m.route("/chat?lobby=" + loc.chat_id);
}
},
peer.name + " / " + loc.location + (
loc.unread_msgs > 0
? ("(" + loc.unread_msgs + ")")
: "")
);
})
})
];
}
function getLobbyDetails(lobbyid){
var lobs = rs("chat/lobbies");
if (lobs === undefined) {
return null;
};
for(var i = 0, l = lobs.length; i < l; ++i) {
if (lobs[i].chat_id == lobbyid) {
return lobs[i];
}
}
var peers = rs("peers");
if (peers === undefined) {
return null;
};
for(var i = 0, l = peers.length; i < l; ++i) {
var peer = peers[i];
for(var i1 = 0, l1 = peer.locations.length; i1 < l1; ++i1) {
if (peer.locations[i1].chat_id == lobbyid) {
return peer.locations[i1];
}
}
}
return null;
}
function sendmsg(msgid){
var txtmsg = document.getElementById("txtNewMsg");
rs.request("chat/send_message", {
chat_id: msgid,
msg: txtmsg.value
});
txtmsg.value="";
}
function lobby(lobbyid){
var msgs;
var lobdt = getLobbyDetails(lobbyid);
var info = rs("chat/info/" + lobbyid);
if (lobdt == null || info === undefined) {
return m("div","waiting ...");
}
var mem = rs.memory("chat/info/" + lobbyid);
if (mem.msg === undefined) {
mem.msg = [];
};
var reqData = {};
if (mem.lastKnownMsg != undefined) {
reqData.begin_after = mem.lastKnownMsg;
}
rs.request("chat/messages/" + lobbyid, reqData, function (data) {
mem.msg = mem.msg.concat(data);
if (mem.msg.length > 0) {
mem.lastKnownMsg = mem.msg[mem.msg.length -1].id;
}
if (data.length > 0 ) {
rs.request("chat/mark_chat_as_read/" + lobbyid,{}, null,
{allow: "ok|not_set"});
}
}, {
onmismatch: function (){},
log:function(){} //no logging (pulling)
});
var intro = [
m("h2",lobdt.name),
m("p",lobdt.topic ? lobdt.topic: lobdt.location),
m("hr")
]
if (lobdt.subscribed != undefined && !lobdt.subscribed) {
return [
intro,
m("b","select subscribe identity:"),
m("p"),
rs.list("identity/own", function(item){
return m("div.btn2",{
onclick:function(){
console.log("subscribe - id:" + lobdt.id +", "
+ "gxs_id:" + item.gxs_id)
rs.request("chat/subscribe_lobby",{
id:lobdt.id,
gxs_id:item.gxs_id
})
}
},"subscribe as " + item.name);
}),
];
} else {
msg = m(".chat.bottom",[
m("div","enter new message:"),
m("input",{
id:"txtNewMsg",
onkeydown: function(event){
if (event.keyCode == 13){
sendmsg(lobbyid);
}
}
}),
m("div.btn2", {
style: {textAlign:"center"},
onclick: function(){
sendmsg(lobbyid);
}
},"submit")
]);
}
if (lobdt.subscribed != undefined
&& lobdt.subscribed
&& !lobdt.is_broadcast
) {
//set participants
particips = [
m("div.btn", {
style: {
"text-align":"center"
},
onclick: function (){
rs.request("chat/unsubscribe_lobby",{
id:lobdt.id,
});
m.route("/chat");
}
},"unsubscribe"),
m("h3","participants:"),
rs.list(
"chat/lobby_participants/" + lobbyid,
function(item) {
return m("div",item.identity.name);
},
function (a,b){
return rs.stringSort(a.identity.name,b.identity.name);
}
)
]
}
return [
intro,
mem.msg.map(function(item){
var d = new Date(new Number(item.send_time)*1000);
return dspmsg(
item.author_name,
d.toLocaleDateString() + " " + d.toLocaleTimeString(),
item.msg
);
})
];
}
module.exports = {
frame: function(content, right){
return m("div", {
style: {
"height": "100%",
"box-sizing": "border-box",
"padding-bottom": "170px",
}
},[
m(".chat.container", [
m(".chat.header", [
m(
"h2",
{style:{margin:"0px"}},
"chat"
)
]),
m(".chat.left", [
m("div.chat.header[style=position:relative]","lobbies:"),
m("br"),
lobbies(),
]),
m(".chat.right", right),
m(".chat.middle", content),
m(".chat.clear", ""),
]),
msg != null
? msg
: [],
]);
},
view: function(){
var lobbyid = m.route.param("lobby");
msg = null;
if (lobbyid != undefined ) {
particips = [];
return this.frame(
lobby(lobbyid),
particips
);
};
return this.frame(
m(
"div",
{style: {margin:"10px"}},
"please select lobby"
),
m("div","")
);
}
}

View File

@ -0,0 +1,254 @@
var m = require("mithril");
var rs = require("retroshare");
var locationName = "";
var password ="";
var ssl_name = "";
var newName = "";
function listprofiles(){
var locations = rs("control/locations");
var knownProfileIds = [];
var result = [];
if(locations === undefined || locations == null){
return m("div", "profiles: waiting_server");
}
locations.map(function(location) {
if (knownProfileIds.indexOf(location.pgp_id)<0){
knownProfileIds.push(location.pgp_id);
result.push(m(
"div.btn2",{
onclick: function(){
m.route("/createlogin",{
id: location.pgp_id,
name: location.name,
})
}
},
location.name
));
}
});
return result;
}
function setLocationName(location) {
locationName = location;
}
function setPasswd(passwd) {
password = passwd;
}
function setSslName(ssl) {
ssl_name = ssl;
}
function setNewName(name) {
newName = name;
}
function checkpasswd(){
var status = "";
var color = "red";
var lbl = document.getElementById("lblpwdinfo");
var passwd2 = document.getElementById("txtpasswd2").value;
if (passwd2 == password && passwd2 != "") {
color = "lime";
status = "password ok";
} else if (passwd2 == "") {
color = "yellow";
status = "password required";
} else {
color = "red";
status = "passwords don't match";
}
lbl.textContent = status;
lbl.style.color=color;
}
function createLocation() {
var profile = m.route.param("id");
var profname = m.route.param("name");
var loc ={
ssl_name: document.getElementById("txtlocation").value,
pgp_password: password,
};
if (profile != undefined) {
loc.pgp_id= profile;
} else {
loc.pgp_name = newName;
};
rs.request("control/create_location",loc,function(data){
m.route("/accountselect", {});
});
m.route("/createlogin",{state:wait});
}
function certDrop(event)
{
console.log("onDrop()");
console.log(event.dataTransfer.files);
event.preventDefault();
var reader = new FileReader();
var widget = this;
reader.onload = function(evt) {
console.log("onDrop(): file loaded");
rs.request(
"control/import_pgp",{
key_string:evt.target.result,
}, importCallback);
};
reader.readAsText(event.dataTransfer.files[0]);
m.route("/createlogin",{state:"waiting"});
}
function importCallback(resp)
{
console.log("importCallback()" + resp);
m.route("/createlogin",{
id:resp.pgp_id,
name:"",
});
}
module.exports = {
view: function(){
var profile = m.route.param("id");
var state = m.route.param("state");
var profname = m.route.param("name");
var hidden = m.route.param("hidden");
if (state == "wait"){
return m("div","waiting ...");
} if (state == "newid"){
m.initControl = "txtnewname";
return m("div",[
m("h2","create login - Step 2 / 2: create location"),
m("h3","- for new profile "),
m("hr"),
m("h2","PGP-profile name:"),
m("input",{
id: "txtnewname",
type:"text",
onchange:m.withAttr("value", setNewName),
onkeydown: function(event){
if (event.keyCode == 13){
document.getElementById("txtpasswd").focus();
}
},
}),
m("h2","enter password:"),
m("input", {
id: "txtpasswd",
type:"password",
onchange: m.withAttr("value",setPasswd),
onkeydown: function(event){
if (event.keyCode == 13){
setPasswd(this.value);
document.getElementById("txtpasswd2").focus();
};
checkpasswd;
}
}),
m("h2", "re-enter password:"),
m("input", {
id: "txtpasswd2",
type:"password",
onfocus: checkpasswd,
onchange: checkpasswd,
onkeyup: function(event){
if (event.keyCode == 13){
document.getElementById("txtlocation").focus();
}
checkpasswd();
}
}),
m("h3",{
id: "lblpwdinfo",
style:"color:yellow",
}, "password required"),
m("h2","location name:"),
m("input",{
id: "txtlocation",
type:"text",
onchange:m.withAttr("value", setLocationName),
onkeydown: function(event){
if (event.keyCode == 13){
setSslName(this.value);
createLocation();
}
},
}),
m("br"),
m("input",{
type: "button",
onclick: createLocation,
value: "create location",
}),
]);
} else if (profile != undefined) {
m.initControl = "txtpasswd";
return m("div",[
m("h2","create login - Step 2 / 2: create location"),
m("h3","- for " + profname + " (" +profile + ")"),
m("hr"),
m("h2","enter password:"),
m("input", {
id: "txtpasswd",
type:"password",
onchange: m.withAttr("value",setPasswd),
onkeydown: function(event){
if (event.keyCode == 13){
setPasswd(this.value);
document.getElementById("txtlocation").focus();
}
}
}),
m("h2","location name:"),
m("input",{
id: "txtlocation",
type:"text",
onchange:m.withAttr("value", setLocationName),
onkeydown: function(event){
if (event.keyCode == 13){
setSslName(this.value);
createLocation();
}
},
}),
m("br"),
m("input",{
type: "button",
onclick: createLocation,
value: "create location",
}),
]);
} else {
return m("div",[
m("h2","create login - Step 1 / 2: select profile(PGP-ID)"),
m("hr"),
m("div.btn2",{
onclick: function(){
m.route("/createlogin", {state: "newid"});
},
} ,"<create new profile>"),
m("div.btn2",{
ondragover:function(event){
/*important: block default event*/
event.preventDefault();
},
ondrop: certDrop,
} ,"<import profile (drag and drop a profile here)>"),
listprofiles()
]);
};
}
}

View File

@ -0,0 +1,96 @@
var m = require("mithril");
var rs = require("retroshare");
function makeFriendlyUnit(bytes)
{
if(bytes < 1e3)
return bytes.toFixed(1) + "B";
if(bytes < 1e6)
return (bytes/1e3).toFixed(1) + "kB";
if(bytes < 1e9)
return (bytes/1e6).toFixed(1) + "MB";
if(bytes < 1e12)
return (bytes/1e9).toFixed(1) + "GB";
return (bytes/1e12).toFixed(1) + "TB";
}
function progressBar(file){
return m("div[style=border:5px solid lime;"
+ 'border-radius:3mm;'
+ 'padding:2mm;'
+ 'height:5mm'
+ "]", [
m("div[style="
+ 'background-color:lime;'
+ 'height:100%;'
+ 'width:' + (file.transfered / file.size * 100)+'%'
+ ']'
,"")
]);
};
function cntrlBtn(file, act) {
return(
m("div.btn",{
onclick: function(){
rs.request("transfers/control_download",{action: act, id: file.id});
}
},
act)
)
}
module.exports = {
view: function(){
var paths = rs("transfers/downloads");
var filestreamer_url = "/fstream/";
if (paths === undefined) {
return m("div", "Downloads ... please wait ...");
}
return m("div", [
m("h2","Downloads (" + paths.length +")"),
m("div.btn2", {
onclick: function(){
m.route("/downloads/add");
}
}, "add retrohare downloads"),
m("hr"),
m('table', [
m("tr",[
m("th","name"),
m("th","size"),
m("th","progress"),
m("th","transfer rate"),
m("th","status"),
m("th","progress"),
m("th","action")
]),
paths.map(function (file){
var ctrlBtn = m("div","");
var progress = file.transfered / file.size * 100;
return m("tr",[
m("td",[
m("a.filelink",
{
target: "blank",
href: filestreamer_url + file.hash + "/" + encodeURIComponent(file.name)
},
file.name
)
]),
m("td", makeFriendlyUnit(file.size)),
m("td", progress.toPrecision(3) + "%"),
m("td", makeFriendlyUnit(file.transfer_rate*1e3)+"/s"),
m("td", file.download_status),
m("td", progressBar(file)),
m("td", [
cntrlBtn(file, file.download_status==="paused"?"start":"pause"),
cntrlBtn(file, "cancel")]
)
])
})
])
]);
}
};

View File

@ -0,0 +1,65 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
module.exports = {view: function(){
return m("div",[
m("h2","forums"),
m("p","(work in progress, currently only listing)"),
m("hr"),
/*
m("div.btn2", {
onclick: function (){
m.route("/addforum");
}
},"< create new forum >"),
*/
m("ul",
rs.list("forums",
function(item){
return m("li",[
m("h2",item.name),
m("div",{style:{margin:"10px"}},
[
item.description != ""
? [
m("span", "Description: "
+ item.description),
m("br")]
: [],
m("span","messages visible: "
+ item.visible_msg_count),
]
),
/*
item.subscribed
? [
m(
"span.btn2",
{style:{padding:"0px"}},
"unsubscribe"
),
" ",
m(
"span.btn2",
{style:{padding:"0px", margin:"10px"}},
"enter"
),
m("hr", {style: {color:"silver"}}),
]
: [
m(
"span.btn2",
{style:{padding:"0px", margin:"10px"}},
"subscribe"
),
]
*/
]);
},
rs.sort("name")
)
)
]);
}}

View File

@ -0,0 +1,145 @@
body {
background-color: black;
color: lime;
font-family: monospace;
margin: 0em;
/*padding: 1.5em;*/
padding: 2mm;
font-size: 1.1em;
box-sizing: border-box;
}
#overlay{
z-index: 10;
position: fixed;
top:0;
left:0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.8);
}
.paddingbox{
padding:2mm;
}
.nav{
list-style-type: none;
padding: 0em;
margin: 0em;
}
.nav li{
display: inline;
padding: 0.1em;
margin-right: 1em;
border-width: 0.1em;
border-color: blue;
border-bottom-style: solid;
cursor: pointer;
}
td{
padding: 0.3em;
border-style: solid;
border-width: 0.1em;
border-color: lime;
}
hr {
color: lime;
}
.menu{
border-style: solid;
border-color: lime;
border-width: 0.1em;
cursor: pointer;
padding: 0.0em;
}
.btn{
border-style: solid;
border-color: lime;
border-width: 0.1em;
cursor: pointer;
padding: 0.1em;
}
.btn2, .box{
border-style: solid;
/*border-color: lime;*/
border-color: limeGreen;
/*border-width: 1px;*/
border-radius: 3mm;
padding: 2mm;
font-size: 10mm;
cursor: pointer;
margin-bottom: 2mm;
}
.btn2:hover{
background-color: midnightblue;
}
.filelink{
color: inherit;
}
input, textarea{
color: lime;
font-family: monospace;
background-color: black;
border-color: lime;
font-size: 10mm;
border-radius: 3mm;
border-width: 1mm;
padding: 2mm;
margin-bottom: 2mm;
margin-right: 2mm;
/* make the button the whole screen width */
width: 100%;
/* make the text input fit small screens*/
box-sizing: border-box;
}
input:hover{
background-color: midnightblue;
}
.checkbox {
width: auto;
}
.flexbox{
display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */
display: -ms-flexbox; /* TWEENER - IE 10 */
display: -webkit-flex; /* NEW - Chrome */
display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
.flexwidemember{
-webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */
-moz-box-flex: 1; /* OLD - Firefox 19- */
width: 20%; /* For old syntax, otherwise collapses. */
-webkit-flex: 1; /* Chrome */
-ms-flex: 1; /* IE 10 */
flex: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
#logo_splash{
-webkit-animation-fill-mode: forwards; /* Chrome, Safari, Opera */
animation-fill-mode: forwards;
-webkit-animation-name: logo_splash; /* Chrome, Safari, Opera */
-webkit-animation-duration: 3s; /* Chrome, Safari, Opera */
animation-name: logo_splash;
animation-duration: 3s;
text-align: center;
}
/* Chrome, Safari, Opera */
@-webkit-keyframes logo_splash {
from {opacity: 0;}
to {opacity: 1;}
}
/* Standard syntax */
@keyframes logo_splash {
from {opacity: 0;}
to {opacity: 1;}
}

View File

@ -0,0 +1,6 @@
var m = require("mithril");
module.exports = {view: function(){
return m("div","RetroShare - WebClient - Welcome");
}
};

View File

@ -0,0 +1,22 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
module.exports = {view: function(){
return m("div",[
m("h2","identities"),
m("hr"),
m("div.btn2", {
onclick: function (){
m.route("/addidentity");
}
},"< create new identity >"),
m("ul",
rs.list("identity/own", function(item){
return m("li",[m("h2",item.name)]);
},
rs.sort("name"))
)
]);
}}

View File

@ -0,0 +1,122 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
var menu =require("menu");
var currentpasswd = null;
function setPasswd(password) {
currentpasswd = password
}
function sendPassword(data) {
console.log("sending pwd for " + data.key_name + "...");
rs.request("control/password", {password: currentpasswd}, function(){
m.redraw();
});
}
function Page(menu){
this.menu = menu;
this.module = (menu.module != undefined) ? menu.module : menu.name;
this.path = menu.path != undefined ? menu.path : ("/" + menu.name);
var runst = menu.runstate;
var content = require(this.module);
var mm = require("menu");
this.view = function(){
var runstate = rs("control/runstate");
var needpasswd = rs("control/password");
//console.log("runstate: " + (runstate === undefined ? runstate : runstate.runstate));
if(runstate === undefined){
return m("h2", "waiting_server ... ");
} else if (runstate.runstate == null){
// try clean reboot ...
rs.clearCache();
rs("control/runstate"); //reboot detector
console.log("i'm down");
return m("h2", "server down");
} else if (needpasswd != undefined && needpasswd.want_password === true){
m.initControl = "txtpasswd";
return m("div",[
m("h2","password required"),
m("h3",needpasswd.key_name),
m("input",{
id: "txtpasswd",
type:"password",
onchange:m.withAttr("value", setPasswd),
onkeydown: function(event){
if (event.keyCode == 13){
setPasswd(this.value);
sendPassword(needpasswd);
}
}
}),
m("br"),
m("input[type=button][value=send password]",{
onclick: function(){
sendPassword(needpasswd);
}
}),
]);
} else {
if ("waiting_init|waiting_startup".match(runstate.runstate)) {
return m("h2","server starting ...")
} else if("waiting_account_select|running_ok.*".match(runstate.runstate)) {
if (runst === undefined || runst.match(runstate.runstate)) {
return m("div", {
style: {
height: "100%",
"box-sizing": "border-box",
"padding-bottom": "40px"
}
}, [
m("div", mm.view()),
m("hr"),
m("div", {
style: {
height: "100%",
"box-sizing": "border-box",
"padding-bottom":"40px"
}
}, content)
]);
} else {
// funktion currently not available
m.route("/");
return m("div", [
m("div", mm.view()),
m("hr"),
m("div", require("home").view())
]);
};
} else {
return m("div", "unknown runstate: " + runstate.runstate);
}
}
}
};
module.exports = {
init:function(main){
console.log("start init ...");
var menudef = require("menudef");
var maps = {};
var m = require("mithril");
menudef.nodes.map(function(menu){
if (menu.action === undefined) {
var p = new Page(menu)
console.log("adding route " + menu.name + " for " + p.path + " with module " + p.module);
maps[p.path] = p;
}
});
m.route.mode = "hash";
m.route(main,"/",maps);
console.log("init done.");
}
};

View File

@ -0,0 +1,12 @@
/*@import "reset" */
html, body, #main
height: 100%
/*body */
/* font-family: "Sans-serif" */
@import "chat"
@import "green-black"

View File

@ -0,0 +1,62 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
var mnodes = require("menudef");
function goback(){
rs.content=null;
m.redraw();
}
function buildmenu(menu, tagname, runstate, ignore){
if (
(menu.runstate === undefined || runstate.match(menu.runstate)!=null)
&& (!ignore.match(menu.name))
&& (menu.path === undefined || menu.path.match(":")==null)
&& (menu.show === undefined || menu.show)
) {
var name = menu.name;
if (menu.counter != undefined) {
name += menu.counter();
}
if (menu.action === undefined) {
return m(tagname , {
onclick: function(){
m.route(
menu.path != undefined ? menu.path : "/" + menu.name
)
}
}, name);
} else {
return m(tagname, {onclick: function(){menu.action(m)}}, name);
}
}
}
module.exports = {view: function(){
var runstate = rs("control/runstate");
if (runstate === undefined || runstate.runstate === undefined || runstate.runstate == null)
return m("div.nav","menu: waiting for server ...");
if (m.route() != "/")
return m("span",[
m("span"," | "),
mnodes.nodes.map(function(menu){
var item = buildmenu(menu,"span.menu", runstate.runstate, "");
if (item != null){
return [
item,
m("span"," | ")
]
}
})
]);
return m("div", [
m("h2","home"),
m("hr"),
mnodes.nodes.map(function(menu){
return buildmenu(menu,"div.btn2", runstate.runstate, "home");
})
]);
}
};

View File

@ -0,0 +1,119 @@
var rs=require("retroshare");
module.exports = { nodes: [
{
name: "home",
path: "/"
},
{
name: "login",
module: "accountselect",
runstate: "waiting_account_select",
counter: rs.counting("control/locations"),
},
{
name: "create login",
path: "/createlogin",
module: "createlogin",
runstate: "waiting_account_select",
},
{
name: "peers",
runstate: "running_ok.*",
counter: rs.counting("peers", function(data){
var onlinecount = 0;
data.map(function(peer) {
var is_online = false;
peer.locations.map(function (location){
if (location.is_online) {
is_online=true;
}
});
if (is_online) {
onlinecount +=1;
}
});
return onlinecount + "/" + data.length;
})
},
{
name: "addpeer",
runstate: "running_ok.*",
show: false,
},
{
name: "identities",
runstate: "running_ok.*",
counter: rs.counting("identity/own"),
},
{
name: "addidentity",
runstate: "running_ok.*",
show: false,
},
{
name:"searchresult",
path: "/search/:id",
runstate: "running_ok.*",
},
{
name: "search",
runstate: "running_ok.*",
},
{
name: "downloads",
runstate: "running_ok.*",
counter: rs.counting("transfers/downloads")
},
{
name: "adddownloads",
runstate: "running_ok.*",
path: "/downloads/add",
show: false,
},
{
name: "forums",
runstate: "running_ok.*",
},
{
name: "chat",
runstate: "running_ok.*",
counter: rs.counting2({
"peers": function(peer) {
var sum = 0;
peer.locations.map(function (loc) {
sum += parseInt(loc.unread_msgs);
});
return sum;
},
"chat/lobbies": function(lobby) {
return lobby.unread_msg_count;
}
})
},
{
name:"settings",
runstate: "running_ok.*",
},
{
name:"servicecontrol",
runstate: "running_ok.*",
path:"/settings/servicecontrol",
show: false,
},
{
name: "shutdown",
runstate: "running_ok|waiting_account_select",
action: function(m){
rs.request("control/shutdown",null,function(){
rs("control/runstate").runstate=null;
rs.forceUpdate("control/runstate");
m.redraw();
});
}
},
{
name: "waiting",
show: false,
},
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,89 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
module.exports = {view: function(){
var peers = rs("peers");
//console.log("peers:" + peers);
//waiting for peerlist ...
if(peers === undefined || peers == null){
return m("div",[
m("h2","peers"),
m("h3","waiting_server"),
]);
};
peers = peers.sort(rs.sort("name"));
//building peerlist (prebuild for counting)
var online = 0;
var peerlist = peers.map(function(peer){
var isonline = false;
var avatar_address ="";
//building location list (prebuild for state + icon)
var loclist = peer.locations.map(function(location){
if (location.is_online && ! isonline){
online +=1;
isonline = true;
}
if (location.avatar_address != "" && avatar_address =="") {
avatar_address=location.avatar_address;
}
return m("li",{
style:"color:" + (location.is_online ? "lime": "grey")
+ ";cursor:pointer",
onclick: function(){
m.route("/chat?lobby=" + location.chat_id)
}
}, location.location);
});
//return friend (peer + locations)
return m("div.flexbox[style=color:lime]",[
// avatar-icon
m("div", [
avatar_address == "" ? "" : (
m("img",{
src: rs.apiurl("peers" + avatar_address),
style:"border-radius:3mm;margin:2mm;",
})
)
]),
//peername + locations
m("div.flexwidemember",[
m("h1[style=margin-bottom:1mm;]",
{style:"color:" + (isonline ? "lime": "grey")} ,
peer.name
),
m("ul", loclist ),
]),
//remove-button
m("div", {
style: "color:red;" +
"font-size:1.5em;" +
"padding:0.2em;" +
"cursor:pointer",
onclick: function (){
var yes = window.confirm(
"Remove " + peer.name + " from friendslist?");
if(yes){
rs.request("peers/" + peer.pgp_id +"/delete");
}
}
}, "X")
]);
});
// return add-peer-button + peerlist
return m("div",[
m("div.btn2",{onclick: function(){m.route("/addpeer")}},"add new friend"),
m("h2","peers (online: " + online +" / " + peers.length + "):"),
m("div", [
peerlist,
]),
]);
}
};

View File

@ -0,0 +1,410 @@
/*
var rs = requires("rs");
var m = require("mithril");
function main(){
var state = rs("runstate");
if(state=== undefined){
return m("div", "waiting for server");
}
if(state === "waiting_login"){
return require("login")();
}
if(state === "running_ok"){
return require("mainwindow")();
}
}
*/
/*
idea: statetokenservice could just send the date instead of the token
*/
"use strict";
var m = require("mithril");
var api_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/api/v2/";
var filestreamer_url = window.location.protocol + "//" +window.location.hostname + ":" + window.location.port + "/fstream/";
var upload_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/upload/";
function for_key_in_obj(obj, callback){
var key;
for(key in obj){
callback(key, obj[key]);
}
}
var cache = {};
var last_update_ts = 0;
function check_for_changes(){
var tokens = [];
var paths_to_fetch = [];
// console.log("start-check " + Object.keys(cache));
for_key_in_obj(cache, function(path, item){
var token = item.statetoken;
if(token === undefined || token== null) {
paths_to_fetch.push(path)
} else if (tokens.indexOf(token)<0) {
tokens.push(token);
}
});
// console.log("tokens found: " + tokens);
var req = m.request({
method: "POST",
url: api_url + "statetokenservice",
background: true,
data: tokens,
});
req.then(function handle_statetoken_response(response){
// console.log("checking result " + response.data ? Object.keys(response.data) : "<null>") ;
for_key_in_obj(cache, function(path, item){
var found = false;
for(var i = 0; i < response.data.length; i++){
if(response.data[i] === item.statetoken){
found = true;
}
}
if(found){
paths_to_fetch.push(path);
}
});
// console.log("generating Results for paths " + paths_to_fetch);
var requests = [];
paths_to_fetch.map(function request_it(path){
var req2 = m.request({
method: "GET",
url: api_url + path,
background: true,
});
req2 = req2.then(function fill_in_result(response){
cache[path].data = response.data;
cache[path].statetoken = response.statetoken;
});
requests.push(req2);
});
if(requests.length > 0){
// console.log("requesting " + requests.length + " requests");
m.sync(requests).then(function trigger_render(){
m.startComputation();
m.endComputation();
checkFocus();
setTimeout(check_for_changes, 500);
});
}
else{
// console.log("no requests");
setTimeout(check_for_changes, 500);
}
}, function errhandling(value){
// console.log("server disconnected " + value);
setTimeout(check_for_changes, 500);
});
}
check_for_changes();
var update_scheduled = false;
function schedule_request_missing(){
if(update_scheduled)
return;
update_scheduled = true;
// place update logic outside of render loop, this way we can fetch multiple things at once
// (because after the render loop everything we should fetch is in the list)
// if we fetch multiple things at once, we can delay a re-rende runtil everything is done
// so we need only one re-render for multiple updates
setTimeout(function request_missing(){
update_scheduled = false;
var requests = [];
for_key_in_obj(cache, function(path, item){
if(!item.requested){
var req = m.request({
method: "GET",
url: api_url + path,
background: true,
});
req.then(function fill_data(response){
// TODO: add errorhandling
item.data = response.data;
item.statetoken = response.statetoken;
if (item.then != undefined && item.then != null) {
try {
item.then(response);
} catch (ex) {
if (item.errorCallback != undefined && item.errorCallback != null) {
item.errorCallback(ex);
};
}
};
}, function errhandling(value){
if (item.errorCallback != undefined && item.errorCallback != null) {
item.errorCallback(value);
}
});
requests.push(req);
}
item.requested = true;
});
m.sync(requests).then(function trigger_render(){
m.startComputation();
m.endComputation();
checkFocus();
});
});
}
function checkFocus(){
if (m.initControl != undefined) {
var ctrl = document.getElementById(m.initControl);
if (ctrl!= null) {
ctrl.focus();
m.initControl = undefined;
} else {
console.log("focus-control '" + m.initControl + "' not found!");
m.initControl = undefined;
}
}
}
// called every time, rs or rs.request failed, only response or value is set
function requestFail(path, response, value) {
rs.error = "error on " + path;
console.log("Error on " + path +
(response == null ? ", value: " + value : (", response: " +
(response.debug_msg === undefined ? response : response.debug_msg)
))
);
}
function rs(path, args, callback, options){
if(cache[path] === undefined){
options=optionsPrep(options,path);
var req = {
data: args,
statetoken: undefined,
requested: false,
allow: options.allow,
then: function(response){
options.log(path + ": response: " + response.returncode);
if (!this.allow.match(response.returncode)) {
options.onmismatch(response);
} else if (callback != undefined && callback != null) {
callback(response.data, response.statetoken);
}
},
errorCallback: options.onfail
};
cache[path] = req;
schedule_request_missing();
}
return cache[path].data;
}
module.exports = rs;
rs.for_key_in_obj = for_key_in_obj;
// single request for action
rs.request=function(path, args, callback, options){
options = optionsPrep(options, path);
var req = m.request({
method: options.method === undefined ? "POST" : options.method,
url: api_url + path,
data: args,
background: true
});
req.then(function checkResponseAndCallback(response){
options.log(path + ": response: " + response.returncode);
if (!options.allow.match(response.returncode)) {
options.onmismatch(response);
} else if (callback != undefined && callback != null) {
callback(response.data, response.statetoken);
}
}, options.onfail);
return req;
};
//set default-values for shared options in rs() and rs.request()
function optionsPrep(options, path) {
if (options === undefined) {
options = {};
}
if (options.onfail === undefined) {
options.onfail = function errhandling(value){
requestFail(path, null, value);
}
};
if (options.onmismatch === undefined) {
options.onmismatch = function errhandling(response){
requestFail(path, response,null);
}
};
if (options.log === undefined) {
options.log = function(message) {
console.log(message);
}
}
if (options.allow === undefined) {
options.allow = "ok";
};
return options;
}
// force reload for path
rs.forceUpdate = function(path, removeCache){
if (removeCache === undefined || !removeCache) {
cache[path].requested=false;
} else {
delete cache[path];
}
}
// force reload for all
rs.clearCache = function(path, removeCache){
console.log("clearing Cache ...")
cache = {};
console.log("update_scheduled: " + update_scheduled);
update_scheduled = false;
check_for_changes();
console.log("Cache cleared.")
}
// dismiss statetoken (= force reload)
rs.untoken = function(path) {
cache[path].statetoken = null;
}
//return api-path
rs.apiurl = function(path) {
if (path === undefined) {
path="";
}
if (path.length > 0 && "^\\\\|\\/".match(path)) {
path=path.substr(1);
}
return api_url + path;
}
// counting in menu
rs.counting = function(path, counterfnkt) {
return function () {
var data=rs(path);
if (data != undefined) {
if (counterfnkt === undefined) {
return " (" + data.length + ")";
}
return " (" + counterfnkt(data) + ")";
}
return "";
}
};
// counting in menu
rs.counting2 = function(targets) {
return function () {
var sum = 0;
for (var path in targets) {
var data=rs(path);
if (data != undefined) {
data.map(function(item){
sum += parseInt(targets[path](item));
});
};
};
if (sum > 0) {
return " (" + sum + ")";
}
return "";
}
};
// listing data-elements
rs.list = function(path, buildfktn, sortfktn){
var list = rs(path);
if (list === undefined|| list == null) {
return "< waiting for server ... >"
};
if (sortfktn != undefined && sortfktn != null) {
list=list.sort(sortfktn);
}
return list.map(buildfktn);
};
//remember additional data (feature of last resort)
rs.memory = function(path, args){
var item = cache[path];
if (item === undefined) {
rs(path, args);
item = cache[path];
}
if (item.memory === undefined) {
item.memory = {};
}
return item.memory;
};
// Sortierfunktion für Texte von Objekten,
// falls einfache Namen nicht funktionieren
rs.stringSort = function(textA,textB, innersort, objectA, objectB){
if (textA.toLowerCase() == textB.toLowerCase()) {
if (innersort === undefined) {
return 0
}
return innersort(objectA,objectB);
} else if (textA.toLowerCase() < textB.toLowerCase()) {
return -1
} else {
return 1
}
}
//return sorting-function for string, based on property name
//using: list.sort(rs.sort("name"));
// -----
//innersort: cascading sorting - using:
//list.sort(rs.sort("type",rs.sort("name")))
rs.sort = function(name, innersort){
return function(a,b) {
return rs.stringSort(a[name],b[name],innersort,a,b);
}
}
//return sorting-function for boolean, based on property name
rs.sort.bool = function(name, innersort){
return function(a,b){
if (a[name] == b[name]) {
if (innersort === undefined) {
return 0
}
return innersort(a,b);
} else if (a[name]) {
return -1
} else {
return 1
}
}
}
// searching a element in a list
// items: list to search in
// name: name of attribute to lookup
// value: attribute's value to compare
rs.find = function(items, name, value) {
if (items === undefined||items == null) {
return null;
};
for(var i = 0, l = items.length; i < l; ++i) {
if (items[i][name] == value) {
return items[i];
}
}
return null;
}

View File

@ -0,0 +1,54 @@
var m = require("mithril");
var rs = require("retroshare");
var state = {};
var searchText = "";
function updateText(newText) {
searchText = newText;
}
function dosearch(){
console.log("searching for: "+searchText);
rs.request(
"filesearch/create_search", {
distant: true,
search_string: searchText
},
function(resp){
m.route("/search/" + resp.search_id);
}
);
}
module.exports = {
view: function(){
var results = rs("filesearch");
if (results === undefined||results == null) {
results = [];
};
return m("div",[
m("h2","turtle file search"),
m("div", [
m("input[type=text]", {onchange:m.withAttr("value", updateText)}),
m("input[type=button][value=search]",{onclick:dosearch})
]),
m("hr"),
m("h2","previous searches:"),
m("div", [
results.map(function(item){
var res = rs("filesearch/" + item.id,{},null,{allow:"not_set|ok"});
if (res === undefined) {
res =[];
};
return m("div.btn2",{
onclick:function(){
m.route("/search/" + item.id);
}
}, item.search_string + " (" + res.length + ")");
})
])
])
}
}

View File

@ -0,0 +1,71 @@
var m = require("mithril");
var rs = require("retroshare");
module.exports = {
view: function(){
var id=m.route.param("id");
var results = rs("filesearch/" + id ,{},null,{allow:"not_set|ok"});
if (results === undefined || results.length == undefined) {
results = [];
}
var searches = rs("filesearch");
var searchdetail = "<unknown>";
if (!(searches === undefined) && !(searches.length === undefined)) {
searches.forEach(function(s){
if (s.id == id) {
searchdetail = s.search_string;
}
});
}
var dl_ids = [];
var downloads =rs("transfers/downloads");
if (downloads !== undefined) {
downloads.map(function(item){
dl_ids.push(item.hash);
})
}
return m("div",[
m("h2","turtle file search results"),
m("h3", "searchtext: " + searchdetail + " (" + results.length + ")"),
m("hr"),
m("table", [
m("tr" ,[
m("th","name"),
m("th","size"),
m("th",""),
]),
results.map(function(file){
if (dl_ids.indexOf(file.hash)>=0) {
file.state="in download queue"
}
return m("tr",[
m("th",file.name),
m("th",file.size),
m("th",[
file.state === undefined
? m("span.btn", {
onclick:function(){
rs.request("transfers/control_download", {
action: "begin",
name: file.name,
size: file.size,
hash: file.hash,
}, function(){
result="added";
});
m.startComputation();
m.endComputation();
}
}, "download")
: file.state
]),
])
})
])
])
}
}

View File

@ -0,0 +1,264 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
function setOption(id,value) {
return function(){
rs.request("servicecontrol", {
service_id: id,
default_allowed: value,
});
rs.forceUpdate("servicecontrol", true);
}
}
function setUserOption(serviceid, userid, value) {
return function(){
rs.request("servicecontrol/user", {
service_id: serviceid,
peer_id: userid,
enabled: value
}, function(){
rs.forceUpdate("servicecontrol", true)
});
}
}
function createSwitch(isOn, width) {
if (width === undefined) {
width = "2.1em";
}
return [
m("div.menu", {
style: {
float:"left",
width: width,
textAlign: "center",
color: "#303030",
borderColor: isOn
? "lime"
: "red",
backgroundColor: !isOn
? "black"
: "lime",
}
}, "ON"),
m("div.menu",{
style: {
float:"left",
width: width,
textAlign: "center",
marginRight:"5px",
color: "#303030",
borderColor: isOn
? "lime"
: "red",
backgroundColor: isOn
? "black"
: "red",
}
}, "OFF"),
];
}
function breadcrums(name, parts){
var result = [];
rs.for_key_in_obj(parts, function(partname,item){
result.push(
m("span.btn",{
onclick: function(){
m.route(item)
}
},partname)
);
result.push(" / ");
});
result.push(name);
return result;
}
function serviceView(serviceid) {
var service, liste;
service = rs.find(rs("servicecontrol"),"service_id",serviceid);
if (service == null) {
return m("h3","<please wait ... >");
}
liste = service.default_allowed
? service.peers_denied
: service.peers_allowed;
return m("div", [
m("h2", breadcrums(service.service_name, {
settings:"/settings",
rights: "/settings/servicecontrol",
})),
m("hr"),
m("h2",{
style:{
float:"left",
}
},[
m("div",{
style:{
float:"left",
}
},"user rights for: " + service.service_name + ", default: "),
m("div", {
onclick: setOption(
serviceid,
!service.default_allowed
),
style: {
float:"left",
marginLeft: "0.4em",
marginRight: "0.4em",
}
},createSwitch(service.default_allowed)),
]),
m("div", {
style: {
clear:"left",
}
}),
m("ul", rs.list("peers",function(peer){
var locs;
locs = peer.locations;
locs.sort(rs.sort("location"));
return peer.locations.map(function(location){
var isExcept, isOn;
isExcept = liste != null
&& liste.indexOf(location.peer_id)>=0;
isOn = service.default_allowed ? !isExcept: isExcept;
return m("li", {
style: {
margin: "5px",
color: isOn ? "lime" :"red",
}
}, [
m("div"),
m("div", {
onclick: setUserOption(
serviceid,
location.peer_id,
!isOn
),
style: {
float:"left",
},
},createSwitch(isOn)),
m("div",
{
style: {
//color: "lime",
float:"left",
marginLeft: "5px",
marginRight: "5px",
fontWeight: "bold",
}
},
peer.name + (location.location
? " (" + location.location + ")"
: "")
),
m("div", {
style: {
clear: "left"
}
}),
]);
})
}, rs.sort("name")))
]);
}
module.exports = {
view: function(){
if (m.route.param("service_id")) {
return serviceView(m.route.param("service_id"));
}
return m("div", [
m("h2", breadcrums("rights", {
settings:"/settings",
})),
m("hr"),
m("ul", rs.list("servicecontrol", function(item){
return m("li", {
style: {
margin: "5px",
color: item.default_allowed ? "lime" :"red",
}
}, [
m("div"),
m("div", {
onclick: setOption(
item.service_id,
!item.default_allowed
),
style: {
float:"left",
}
},createSwitch(item.default_allowed)),
m("div.menu",
{
style: {
// color: "lime",
borderColor: item.default_allowed
? "lime"
: "red",
float: "left",
marginLeft: "5px",
marginRight: "5px",
paddingLeft: "2px",
paddingRight: "2px",
},
onclick: function(){
m.route("/settings/servicecontrol/", {
service_id: item.service_id,
})
}
}, "more"
),
m("div",
{
style: {
// color: "lime",
float:"left",
marginLeft: "5px",
marginRight: "5px",
fontWeight: "bold",
}
},
item.service_name
),
m("div",
{
style: {
color: "lime",
float:"left",
marginLeft: "5px",
marginRight: "5px",
}
},
(
item.default_allowed
? ( item.peers_denied != null
? "(" + item.peers_denied.length + " denied)"
: "")
: ( item.peers_allowed != null
? "(" + item.peers_allowed.length + " allowed)"
: "")
)
),
m("div", {
style: {
clear: "left"
}
}),
]);
})
)
]);
}
}

View File

@ -0,0 +1,19 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
module.exports = {
view: function(){
return m("div", [
m("h2","settings"),
m("hr"),
m("div.btn2",{
onclick: function(){
m.route("/settings/servicecontrol");
},
}, "rights")
]);
}
}

View File

@ -0,0 +1,8 @@
"use strict";
var m = require("mithril");
var rs = require("retroshare");
module.exports = {view: function(){
return m("h2","please wait ...");
}}

View File

@ -0,0 +1,12 @@
module.exports = {
config:{
files:{
javascripts:{
joinTo: 'app.js'
},
stylesheets:{
joinTo: 'app.css'
}
}
}
};

View File

@ -0,0 +1,41 @@
@echo off
REM create webfiles from sources at compile time (works without npm/node.js)
set publicdest=%1\webui
set src=%1\webui-src
if "%1" == "" set publicdest=..\..\webui&&set src=..
if exist "%publicdest%" echo remove existing %publicdest%&&rd %publicdest% /S /Q
echo mkdir %publicdest%
md %publicdest%
echo building app.js
echo - copy template.js ...
copy %src%\make-src\template.js %publicdest%\app.js
for %%F in (%src%\app\*.js) DO (set "fname=%%~nF" && CALL :addfile)
echo building app.css
type %src%\app\green-black.scss >> %publicdest%\app.css
type %src%\make-src\main.css >> %publicdest%\app.css
type %src%\make-src\chat.css >> %publicdest%\app.css
echo copy index.html
copy %src%\app\assets\index.html %publicdest%\index.html
echo build.bat complete
goto :EOF
:addfile
echo - adding %fname% ...
echo require.register("%fname%", function(exports, require, module) { >> %publicdest%\app.js
echo %src%\app\%fname%.js
type %src%\app\%fname%.js >> %publicdest%\app.js
echo. >> %publicdest%\app.js
echo }); >> %publicdest%\app.js
:EOF

View File

@ -0,0 +1,43 @@
#!/usr/bin/sh
# create webfiles from sources at compile time (works without npm/node.js)
if [ "$1" == "" ];then
publicdest=../../webui
src=..
else
publicdest=$1/webui
src=$1/webui-src
fi
if [ -d "$publicdest" ]; then
echo remove existing $publicdest
rm $publicdest -R
fi
echo mkdir $publicdest
mkdir $publicdest
echo building app.js
echo - copy template.js ...
cp $src/make-src/template.js $publicdest/app.js
for filename in $src/app/*.js; do
fname=$(basename "$filename")
fname="${fname%.*}"
echo - adding $fname ...
echo require.register\(\"$fname\", function\(exports, require, module\) { >> $publicdest/app.js
cat $filename >> $publicdest/app.js
echo >> $publicdest/app.js
echo }\)\; >> $publicdest/app.js
done
echo building app.css
cat $src/app/green-black.scss >> $publicdest/app.css
cat $src/make-src/main.css >> $publicdest/app.css
cat $src/make-src/chat.css >> $publicdest/app.css
echo copy index.html
cp $src/app/assets/index.html $publicdest/index.html
echo build.sh complete

View File

@ -0,0 +1,72 @@
.chat {
padding: 15px; }
.chat.container {
height: 100%;
padding: 0px;
position: relative;
box-sizing: border-box; }
.chat.header {
position: absolute;
top: 0px;
left: 0px;
right: 0px;
height: 50px;
background-color: black;
border-bottom: solid 1px gray;
box-sizing: border-box; }
.chat.left {
position: absolute;
top: 50px;
bottom: 0px;
left: 0px;
width: 200px;
box-sizing: border-box;
background-color: black; }
.chat.right {
position: absolute;
top: 50px;
right: 0px;
bottom: 0px;
width: 200px;
box-sizing: border-box; }
.chat.middle {
position: absolute;
top: 0px;
margin-top: 50px;
left: 200px;
right: 200px;
box-sizing: border-box;
padding: 0px;
height: 100%;
overflow-y: scroll; }
.chat.bottom {
position: absolute;
bottom: 0px;
right: 200px;
left: 200px;
padding: 5px; }
.chat.msg {
padding: 0px; }
.chat.msg.container {
position: relative;
border-bottom: solid 1px lightgray;
padding: 10px;
height: unset; }
.chat.msg.from {
position: absolute;
width: 100px;
top: 10px;
left: 0px;
color: white;
text-align: right; }
.chat.msg.when {
float: right;
color: lightgray;
margin-bottom: 10px; }
.chat.msg.text {
padding-left: 100px;
top: 0px;
left: 100px;
white-space: pre-wrap;
height: initial; }

View File

@ -0,0 +1,15 @@
@echo off
REM create dummy webfiles at qmake run
set publicdest=%1\webui
if "%1" == "" set publicdest=..\..\webui
if exist %publicdest% echo remove %publicdest%&&rd %publicdest% /S /Q
echo create %publicdest%
md %publicdest%
echo create %publicdest%\app.js, %publicdest%\app.css, %publicdest%\index.html
echo. > %publicdest%\app.js
echo. > %publicdest%\app.css
echo. > %publicdest%\index.html

View File

@ -0,0 +1,22 @@
#!/usr/bin/sh
# create dummy webfiles at qmake run
if [ "$1" == "" ];then
publicdest=../../webui
else
publicdest=$1/webui
fi
if [ -d "$publicdest" ]; then
echo remove $publicdest
rm $publicdest -R
fi
echo create $publicdest
mkdir $publicdest
echo touch $publicdest/app.js, $publicdest/app.css, $publicdest/index.html
touch $publicdest/app.js
touch $publicdest/app.css
touch $publicdest/index.html

View File

@ -0,0 +1,2 @@
html, body, #main {
height: 100%; }

View File

@ -0,0 +1,7 @@
this folder contains files needed to create webfiles at compile-time and qmake
* init.sh creates dummy files at qmake
* build.sh creates files at compile time
* chat.css compiled version of _chat.sass (.sass should replaced by .scss)
* main.css simple template extracted from main.sass
* template.js start of compiled app.js, containing additional created content

View File

@ -0,0 +1,112 @@
(function() {
'use strict';
var globals = typeof window === 'undefined' ? global : window;
if (typeof globals.require === 'function') return;
var modules = {};
var cache = {};
var aliases = {};
var has = ({}).hasOwnProperty;
var endsWith = function(str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
};
var _cmp = 'components/';
var unalias = function(alias, loaderPath) {
var start = 0;
if (loaderPath) {
if (loaderPath.indexOf(_cmp) === 0) {
start = _cmp.length;
}
if (loaderPath.indexOf('/', start) > 0) {
loaderPath = loaderPath.substring(start, loaderPath.indexOf('/', start));
}
}
var result = aliases[alias + '/index.js'] || aliases[loaderPath + '/deps/' + alias + '/index.js'];
if (result) {
return _cmp + result.substring(0, result.length - '.js'.length);
}
return alias;
};
var _reg = /^\.\.?(\/|$)/;
var expand = function(root, name) {
var results = [], part;
var parts = (_reg.test(name) ? root + '/' + name : name).split('/');
for (var i = 0, length = parts.length; i < length; i++) {
part = parts[i];
if (part === '..') {
results.pop();
} else if (part !== '.' && part !== '') {
results.push(part);
}
}
return results.join('/');
};
var dirname = function(path) {
return path.split('/').slice(0, -1).join('/');
};
var localRequire = function(path) {
return function expanded(name) {
var absolute = expand(dirname(path), name);
return globals.require(absolute, path);
};
};
var initModule = function(name, definition) {
var module = {id: name, exports: {}};
cache[name] = module;
definition(module.exports, localRequire(name), module);
return module.exports;
};
var require = function(name, loaderPath) {
var path = expand(name, '.');
if (loaderPath == null) loaderPath = '/';
path = unalias(name, loaderPath);
if (has.call(cache, path)) return cache[path].exports;
if (has.call(modules, path)) return initModule(path, modules[path]);
var dirIndex = expand(path, './index');
if (has.call(cache, dirIndex)) return cache[dirIndex].exports;
if (has.call(modules, dirIndex)) return initModule(dirIndex, modules[dirIndex]);
throw new Error('Cannot find module "' + name + '" from '+ '"' + loaderPath + '"');
};
require.alias = function(from, to) {
aliases[to] = from;
};
require.register = require.define = function(bundle, fn) {
if (typeof bundle === 'object') {
for (var key in bundle) {
if (has.call(bundle, key)) {
modules[key] = bundle[key];
}
}
} else {
modules[bundle] = fn;
}
};
require.list = function() {
var result = [];
for (var item in modules) {
if (has.call(modules, item)) {
result.push(item);
}
}
return result;
};
require.brunch = true;
require._cache = cache;
globals.require = require;
})();

View File

@ -0,0 +1,11 @@
{
"name": "webui_neu",
"scripts": {
"watch": "brunch watch --server"
},
"devDependencies": {
"auto-reload-brunch": "^1.8.0",
"brunch": "^1.8.5",
"sass-brunch": "^1.9.1"
}
}

View File

@ -1,36 +0,0 @@
REACT_VERSION = 0.13.1
DISTDIR = ../webfiles
JSEXTLIBS = $(DISTDIR)/react.js $(DISTDIR)/JSXTransformer.js
JSLIBS = RsXHRConnection.js RsApi.js
HTML = index.html
JSGUI = gui.jsx
CSS = green-black.css
all: $(DISTDIR) $(JSEXTLIBS) $(addprefix $(DISTDIR)/, $(JSLIBS)) $(addprefix $(DISTDIR)/, $(HTML)) $(addprefix $(DISTDIR)/, $(JSGUI)) $(addprefix $(DISTDIR)/, $(CSS))
.PHONY: all
$(DISTDIR)/livereload: $(DISTDIR) $(JSEXTLIBS) $(addprefix $(DISTDIR)/, $(JSLIBS)) $(addprefix $(DISTDIR)/, $(HTML)) $(addprefix $(DISTDIR)/, $(JSGUI)) $(addprefix $(DISTDIR)/, $(CSS))
wget -qO- http://localhost:9090/api/v2/livereload/trigger
touch $(DISTDIR)/livereload
$(DISTDIR)/react.js:
cd $(DISTDIR) && wget --no-check-certificate --output-document react.js http://fb.me/react-$(REACT_VERSION).js
$(DISTDIR)/JSXTransformer.js:
cd $(DISTDIR) && wget --no-check-certificate --output-document JSXTransformer.js http://fb.me/JSXTransformer-$(REACT_VERSION).js
$(addprefix $(DISTDIR)/, $(JSLIBS)): $(DISTDIR)/%: %
cp $< $@
$(addprefix $(DISTDIR)/, $(HTML)): $(DISTDIR)/%: %
cp $< $@
$(addprefix $(DISTDIR)/, $(JSGUI)): $(DISTDIR)/%: %
cp $< $@
$(addprefix $(DISTDIR)/, $(CSS)): $(DISTDIR)/%: %
cp $< $@
$(DISTDIR):
mkdir $(DISTDIR)

View File

@ -1,142 +0,0 @@
var TypesMod = require("./Types.js");
var Type = TypesMod.Type;
var string = TypesMod.string;
var bool = TypesMod.bool;
var any = TypesMod.any;
if(require.main === module)
{
var RsNodeHttpConnection = require("./RsNodeHttpConnection.js");
debugger;
var connection = new RsNodeHttpConnection();
var RsApi = require("./RsApi.js");
var RS = new RsApi(connection);
var tests = [];
var doc = {
counter: 0,
toc: [],
content: [],
header: function(h){
this.toc.push(h);
this.content.push("<a name=\""+this.counter+"\"><h1>"+h+"</h1></a>");
this.counter += 1;
},
paragraph: function(p){
this.content.push("<p>"+p+"</p>");
},
};
PeersTest(tests, doc);
var docstr = "<!DOCTYPE html><html><body>";
docstr += "<h1>Table of Contents</h1>";
docstr += "<ul>";
for(var i in doc.toc)
{
docstr += "<li><a href=\"#"+i+"\">"+doc.toc[i]+"</a></li>";
}
docstr += "</ul>";
for(var i in doc.content)
{
docstr += doc.content[i];
}
docstr += "</body></html>";
var fs = require('fs');
fs.writeFile("dist/api_documentation.html", docstr);
tests.map(function(test){
test(RS);
});
}
function PeersTest(tests, doc)
{
// compound types
var location = new Type("location",
{
avatar_address: string,
groups: any,
is_online: bool,
location: string,
peer_id: any,
});
var peer_info = new Type("peer_info",
{
name: string,
pgp_id: any,
locations: [location],
});
var peers_list = new Type("peers_list",[peer_info]);
doc.header("peers");
doc.paragraph("<pre>"+graphToText(peers_list)+"</pre>");
tests.push(function(RS){
console.log("testing peers module...");
console.log("expected schema is:")
console.log(graphToText(peers_list));
RS.request({path: "peers"}, function(resp){
//console.log("got response:"+JSON.stringify(resp));
var ok = peers_list.check(function(str){console.log(str);}, resp.data, [])
if(ok)
console.log("success");
else
console.log("fail");
});
});
function graphToText(top_node)
{
//var dbg = function(str){console.log(str);};
var dbg = function(str){};
dbg("called graphToText with " + top_node);
var res = "";
_visit(top_node.getObj(), 0);
return res;
function _indent(count)
{
var str = "";
for(var i = 0; i < count; i++)
{
str = str + " ";
}
return str;
}
function _visit(node, indent)
{
dbg("_visit");
if(node instanceof Array)
{
dbg("is instanceof Array");
//res = res + "[";
res = res + "array\n";
_visit(node[0], indent);
//res = res + _indent(indent) + "]\n";
}
else if(node instanceof Type && node.isLeaf())
{
dbg("is instanceof Type");
res = res + node.getName() + "\n";
}
else // Object, have to check all children
{
dbg("is Object");
//res = res + "{\n";
for(m in node.getObj())
{
res = res + _indent(indent+1) + m + ": ";
_visit(node.getObj()[m], indent+1);
}
//res = res + _indent(indent) + "}\n";
}
}
}
}

View File

@ -1,48 +0,0 @@
A new approach to build a webinterface for RS
=============================================
1. get JSON encoded data from the backend, data contains a state token
2. render data with react.js
3. ask the backend if the state token from step 1 expired. If yes, then start again with step 1.
Steps 1. and 3. are common for most things, only Step 2. differs. This allows to re-use code for steps 1. and 3.
BUILD / INSTALLATION
------------
- run (requires wget, use MinGW shell on Windows)
make
- all output files are now in libresapi/src/webfiles
- use the --webinterface 9090 command line parameter to enable webui in retroshare-nogui
- set the --docroot parameter of retroshare-nogui to point to the "libresapi/src/webfiles" directory
(or symlink from /usr/share/RetroShare06/webui on Linux, ./webui on Windows)
- retroshare-gui does not have a --docroot parameter. Use symlinks then.
DEVELOPMENT
-----------
- Ubuntu: install nodejs package
sudo apt-get install nodejs
- Windows: download and install nodejs from http://nodejs.org
- Download development tools with the nodejs package manager (short npm)
npm install
- run Retroshare with webinterface on port 9090
- during development, run this command (use MinGW shell on Windows)
while true; do make ../webfiles/livereload --silent; sleep 1; done
- the command will copy the source files to libresapi/src/webfiles if they change
- it will trigger livereload at http://localhost:9090/api/v2/livereload/trigger
API DOCUMENTATION
-----------------
- run
node PeersTest.js
- this will print the expected schema of the api output, and it will try to test it with real data
- run retroshare-nogui with webinterface enabled on port 9090, to test if the real output of the api matches the expected schema
CONTRIBUTE
----------
- if you are a web developer or want to become one
get in contact!
- lots of work to do, i need you!

View File

@ -1,109 +0,0 @@
/**
* JS Api for Retroshare
* @constructor
* @param {object} connection - an object which implements a request() function.
* The request function should take two parameters: an object to be send as request and a callback.
* The callback should get called with an response object on success.
*/
function RsApi(connection)
{
var runnign = true;
/**
* Send a request to the server
* @param req - the request so send
* @param {Function} cb - callback function which takes the response as parameter
*/
this.request = function(req, cb)
{
connection.request(req, cb);
};
var tokenlisteners = [];
/**
* Register a callback to be called when the state token expired.
* @param {Function} listener - the callback function, which does not take arguments
* @param token - the state token to listen for
*/
this.register_token_listener = function(listener, token)
{
tokenlisteners.push({listener:listener, token:token});
};
/**
* Unregister a previously registered callback.
*/
this.unregister_token_listener = function(listener) // no token as parameter, assuming unregister from all listening tokens
{
var to_delete = [];
for(var i=0; i<tokenlisteners.length; i++){
if(tokenlisteners[i].listener === listener){
to_delete.push(i);
}
}
for(var i=0; i<to_delete.length; i++){
// copy the last element to the current index
var index = to_delete[i];
tokenlisteners[index] = tokenlisteners[tokenlisteners.length-1];
// remove last element
tokenlisteners.pop();
}
};
/**
* start polling for state changes
*/
this.start = function(){
running = true;
setTimeout(tick, TICK_INTERVAL);
}
/**
* stop polling for state changes
*/
this.stop = function(){
running = false;
}
// ************** interal stuff **************
var TICK_INTERVAL = 3000;
function received_tokenstates(resp)
{
if(resp.data){
for(var i=0; i<resp.data.length; i++){
var token = resp.data[i];
// search the listener for this token
for(var j=0; j<tokenlisteners.length; j++){
if(tokenlisteners[j].token === token){
// call the listener
tokenlisteners[j].listener();
}
}
}
}
// schedule new update
if(running)
setTimeout(tick, TICK_INTERVAL);
};
function received_error()
{
// try again, maybe want a better logic later
if(running)
setTimeout(tick, TICK_INTERVAL);
};
function tick()
{
var data = [];
// maybe cache the token list?
// profiler will tell us if we should
for(var i=0; i<tokenlisteners.length; i++){
data.push(tokenlisteners[i].token);
}
connection.request({
path: "statetokenservice",
data: data,
}, received_tokenstates, received_error);
};
};
// with this trick, we should be able to run in browser or nodejs
if(typeof window === 'undefined')
{
// we are running in nodejs, so have to add to export
module.exports = RsApi;
}

View File

@ -1,51 +0,0 @@
var http = require('http');
/**
* Connection to the RS backend using http for running under node.js
* Mainly for testing, but could also use it for general purpose scripting.
* @constructor
*/
module.exports = function()
{
var server_hostname = "localhost";
var server_port = "9090";
var api_root_path = "/api/v2/";
this.request = function(request, callback)
{
var data;
if(request.data)
data = JSON.stringify(request.data);
else
data = "";
// NODEJS specific
var req = http.request({
host: server_hostname,
port: server_port,
path: api_root_path + request.path,
headers: {
"Content-Type": "application/json",
"Content-Length": data.length, // content length is required, else Wt will not provide the data (maybe WT does not like chunked encoding?)
}
//method: "POST",
}, function(response){
var databuffer = [];
response.on("data", function(chunk){
//console.log("got some data");
databuffer = databuffer + chunk;
})
response.on("end", function(){
//console.log("finished receiving data");
//console.log("data:"+databuffer);
callback(JSON.parse(databuffer));
})
});
//console.log("uploading data:");
//console.log(data);
req.write(data);
req.end();
}
}

View File

@ -1,123 +0,0 @@
/**
* Connection to the RS backend using XHR
* (could add other connections later, for example WebSockets)
* @constructor
*/
function RsXHRConnection(server_hostname, server_port)
{
var debug;
//debug = function(str){console.log(str);};
debug = function(str){};
//server_hostname = "localhost";
//server_port = "9090";
var api_root_path = "/api/v2/";
var status_listeners = [];
function notify_status(status)
{
for(var i = 0; i < status_listeners.length; i++)
{
status_listeners[i](status);
}
}
/**
* Register a callback to be called when the state of the connection changes.
* @param {function} cb - callback which receives a single argument. The arguments value is "connected" or "not_connected".
*/
this.register_status_listener = function(cb)
{
status_listeners.push(cb);
};
/**
* Unregister a status callback function.
* @param {function} cb - a privously registered callback function
*/
this.unregister_status_listener = function(cb)
{
var to_delete = [];
for(var i = 0; i < status_listeners.length; i++)
{
if(status_listeners[i] === cb){
to_delete.push(i);
}
}
for(var i = 0; i < to_delete.length; i++)
{
// copy the last element to the current index
var index = to_delete[i];
status_listeners[i] = status_listeners[status_listeners.length-1];
// remove the last element
status_listeners.pop();
}
};
/**
* Send a request to the backend
* automatically encodes the request as JSON before sending it to the server
* @param {object} req - the request to send to the server
* @param {function} cb - callback function to be called to handle the response. The callback takes one object as parameter. Can be left undefined.
* @param {function} err_cb - callback function to signal a failed request. Can be undefined.
*/
this.request = function(req, cb, err_cb)
{
//var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
// TODO: window is not available in QML
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
//console.log("onreadystatechanged state"+xhr.readyState);
// TODO: figure out how to catch errors like connection refused
// maybe want to have to set a state variable like ok=false
// the gui could then display: "no connection to server"
if (xhr.readyState === 4) {
if(xhr.status !== 200)
{
console.log("RsXHRConnection: request failed with status: "+xhr.status);
console.log("request was:");
console.log(req);
notify_status("not_connected");
if(err_cb !== undefined)
err_cb();
return;
}
// received response
notify_status("connected");
debug("RsXHRConnection received response:");
debug(xhr.responseText);
if(false)//if(xhr.responseText === "")
{
debug("Warning: response is empty");
return;
}
try
{
var respObj = JSON.parse(xhr.responseText);
}
catch(e)
{
debug("Exception during response handling: "+e);
}
if(cb === undefined)
debug("No callback function specified");
else
cb(respObj);
}
}
// post is required for sending data
var method;
if(req.data){
method = "POST";
} else {
method = "GET";
}
xhr.open(method, "http://"+server_hostname+":"+server_port+api_root_path+req.path);
var data = JSON.stringify(req.data);
debug("RsXHRConnection sending data:");
debug(data);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(data);
};
};

View File

@ -1,146 +0,0 @@
/**
* Construct a new type from an array, object or function.
* Use instances of this class to build a schema graph.
* The schema graph must not contain data other than arrays and instances of this class.
* Use the check function to check if arbitrary JS objects matches the schema.
* @constructor
* @param {String} name - name for the new type
* @param obj - array, object or function
* array: array should contain one instance of class "Type"
* object: object members can be arrays or instances of class "Type".
* Can also have child objects, but the leaf members have to be instances of class "Type"
* function: a function which takes three parameters: log, other, stack
* must return true if other matches the type, can report errors using the function passed in log.
*/
function Type(name, obj)
{
//var dbg = function(str){console.log(str);};
var dbg = function(str){};
this.getName = function(){
return name;
}
this.isLeaf = function(){
return typeof(obj) === "function";
}
this.getObj = function(){
return obj;
}
this.check = function(log, other, stack)
{
if(typeof(obj) === "object")
{
stack.push("<"+name+">");
var ok = _check(log, obj, other, stack);
stack.pop;
return ok;
}
if(typeof(obj) === "function")
return obj(log, other, stack);
log("FATAL Error: wrong usage of new Type(), second parameter should be an object or checker function");
return false;
}
function _check(log, ref, other, stack)
{
dbg("_check");
dbg("ref=" + ref);
dbg("other=" + other);
dbg("stack=[" + stack + "]");
if(ref instanceof Array)
{
dbg("is instanceof Array");
if(other instanceof Array)
{
if(other.length > 0)
{
return _check(log, ref[0], other[0], stack);
}
else
{
log("Warning: can't check array of length 0 in ["+stack+"]");
return true;
}
}
else
{
log("Error: not an Array ["+stack+"]");
return false;
}
}
if(ref instanceof Type)
{
dbg("is instanceof Type");
return ref.check(log, other, stack);
}
else // Object, have to check all children
{
dbg("is Object");
var ok = true;
for(m in ref)
{
if(m in other)
{
stack.push(m);
ok = ok && _check(log, ref[m], other[m], stack);
stack.pop();
}
else
{
log("Error: missing member \""+m+"\" in ["+stack+"]");
ok = false;
}
}
// check for additionally undocumented members
for(m in other)
{
if(!(m in ref))
{
log("Warning: found additional member \""+m+"\" in ["+stack+"]");
}
}
return ok;
}
}
};
// basic data types
// - string
// - bool
// - any (placeholder for unknown type)
var string = new Type("string",
function(log, other, stack)
{
if(typeof(other) !== "string")
{
log("Error: not a string ["+stack+"]");
return false;
}
else
return true;
}
);
var bool = new Type("bool",
function(log, other, stack)
{
if(typeof(other) !== "boolean")
{
log("Error: not a bool ["+stack+"]");
return false;
}
else
return true;
}
);
var any = new Type("any",
function(log, other, stack)
{
return true;
}
);
exports.Type = Type;
exports.string = string;
exports.bool = bool;
exports.any = any;

View File

@ -1,134 +0,0 @@
body {
background-color: black;
color: lime;
font-family: monospace;
margin: 0em;
/*padding: 1.5em;*/
padding: 2mm;
font-size: 1.1em;
}
#overlay{
z-index: 10;
position: fixed;
top:0;
left:0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.8);
}
.paddingbox{
padding:2mm;
}
.nav{
list-style-type: none;
padding: 0em;
margin: 0em;
}
.nav li{
display: inline;
padding: 0.1em;
margin-right: 1em;
border-width: 0.1em;
border-color: blue;
border-bottom-style: solid;
cursor: pointer;
}
td{
padding: 0.3em;
border-style: solid;
border-width: 0.1em;
border-color: lime;
}
.btn{
border-style: solid;
border-color: lime;
border-width: 0.1em;
cursor: pointer;
padding: 0.1em;
}
.btn2, .box{
border-style: solid;
/*border-color: lime;*/
border-color: limeGreen;
/*border-width: 1px;*/
border-radius: 3mm;
padding: 2mm;
font-size: 10mm;
cursor: pointer;
margin-bottom: 2mm;
}
.btn2:hover{
background-color: midnightblue;
}
.filelink{
color: inherit;
}
input, textarea{
color: lime;
font-family: monospace;
background-color: black;
border-color: lime;
font-size: 10mm;
border-radius: 3mm;
border-width: 1mm;
padding: 2mm;
margin-bottom: 2mm;
margin-right: 2mm;
/* make the button the whole screen width */
width: 100%;
/* make the text input fit small screens*/
box-sizing: border-box;
}
input:hover{
background-color: midnightblue;
}
.checkbox {
width: auto;
}
.flexbox{
display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */
display: -ms-flexbox; /* TWEENER - IE 10 */
display: -webkit-flex; /* NEW - Chrome */
display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
.flexwidemember{
-webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */
-moz-box-flex: 1; /* OLD - Firefox 19- */
width: 20%; /* For old syntax, otherwise collapses. */
-webkit-flex: 1; /* Chrome */
-ms-flex: 1; /* IE 10 */
flex: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
#logo_splash{
-webkit-animation-fill-mode: forwards; /* Chrome, Safari, Opera */
animation-fill-mode: forwards;
-webkit-animation-name: logo_splash; /* Chrome, Safari, Opera */
-webkit-animation-duration: 3s; /* Chrome, Safari, Opera */
animation-name: logo_splash;
animation-duration: 3s;
text-align: center;
}
/* Chrome, Safari, Opera */
@-webkit-keyframes logo_splash {
from {opacity: 0;}
to {opacity: 1;}
}
/* Standard syntax */
@keyframes logo_splash {
from {opacity: 0;}
to {opacity: 1;}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>New webinterface for Retroshare</title>
<script src="RsXHRConnection.js"></script>
<script src="RsApi.js"></script>
<!-- it seems to work more reliable, if the jsx file is loaded before react -->
<script type="text/jsx" src="gui.jsx"></script>
<script src="react.js"></script>
<script src="JSXTransformer.js"></script>
<link href="green-black.css" rel="stylesheet">
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1">
</head>
<body>
<script>
document.write("<p>loading lots of stuff...</p>");
</script>
<p><noscript>The Retroshare web interface requires JavaScript. Please enable JavaScript in your browser.</noscript></p>
<!--<div id="logo_splash">
<img src="img/logo_splash.png"></img>
</div>-->
</body>
</html>

View File

@ -1,10 +0,0 @@
{
"name": "rswebui",
"version": "0.0.0",
"dependencies": {
},
"devDependencies": {
},
"scripts": {
}
}

View File

@ -307,7 +307,6 @@ public:
bool isBroadcast() const;
RsPeerId toPeerId() const;
RsGxsId toGxsId() const;
ChatLobbyId toLobbyId() const;
DistantChatPeerId toDistantChatId() const;