diff --git a/libresapi/src/api/ApiServer.cpp b/libresapi/src/api/ApiServer.cpp index fa26501a2..994df8a75 100644 --- a/libresapi/src/api/ApiServer.cpp +++ b/libresapi/src/api/ApiServer.cpp @@ -227,7 +227,8 @@ public: mIdentityHandler(ifaces.mIdentity), mServiceControlHandler(rsServiceControl), // TODO: don't use global variable here mFileSearchHandler(sts, ifaces.mNotify, ifaces.mTurtle, ifaces.mFiles), - mTransfersHandler(sts, ifaces.mFiles) + mTransfersHandler(sts, ifaces.mFiles), + mChatHandler(sts, ifaces.mNotify, ifaces.mMsgs, ifaces.mPeers, ifaces.mIdentity, &mPeersHandler) { // the dynamic cast is to not confuse the addResourceHandler template like this: // addResourceHandler(derived class, parent class) @@ -243,6 +244,8 @@ public: &FileSearchHandler::handleRequest); router.addResourceHandler("transfers", dynamic_cast(&mTransfersHandler), &TransfersHandler::handleRequest); + router.addResourceHandler("chat", dynamic_cast(&mChatHandler), + &ChatHandler::handleRequest); } PeersHandler mPeersHandler; @@ -250,6 +253,7 @@ public: ServiceControlHandler mServiceControlHandler; FileSearchHandler mFileSearchHandler; TransfersHandler mTransfersHandler; + ChatHandler mChatHandler; }; ApiServer::ApiServer(): diff --git a/libresapi/src/api/ApiServer.h b/libresapi/src/api/ApiServer.h index 957a8ea6d..becb21641 100644 --- a/libresapi/src/api/ApiServer.h +++ b/libresapi/src/api/ApiServer.h @@ -11,6 +11,7 @@ #include "TransfersHandler.h" #include "LivereloadHandler.h" #include "TmpBlobStore.h" +#include "ChatHandler.h" namespace resource_api{ diff --git a/libresapi/src/api/ApiTypes.h b/libresapi/src/api/ApiTypes.h index ce6ee9898..ab34b4507 100644 --- a/libresapi/src/api/ApiTypes.h +++ b/libresapi/src/api/ApiTypes.h @@ -240,7 +240,10 @@ public: std::ostream& mDebug; inline void setOk(){mReturnCode = OK;} - inline void setWarning(){ mReturnCode = WARNING;} + inline void setWarning(std::string msg = ""){ + mReturnCode = WARNING; + if(msg != "") + mDebug << msg << std::endl;} inline void setFail(std::string msg = ""){ mReturnCode = FAIL; if(msg != "") diff --git a/libresapi/src/api/ChatHandler.cpp b/libresapi/src/api/ChatHandler.cpp index 5953ca2eb..412455fe3 100644 --- a/libresapi/src/api/ChatHandler.cpp +++ b/libresapi/src/api/ChatHandler.cpp @@ -1 +1,654 @@ #include "ChatHandler.h" +#include "Pagination.h" +#include "Operators.h" +#include "GxsResponseTask.h" + +#include +#include + +#include +#include + +namespace resource_api +{ + +std::string id(const ChatHandler::Msg& m) +{ + std::stringstream ss; + ss << m.id; + return ss.str(); +} + +StreamBase& operator << (StreamBase& left, ChatHandler::Msg& m) +{ + left << makeKeyValueReference("incoming", m.incoming) + << makeKeyValueReference("was_send", m.was_send) + << makeKeyValueReference("author_id", m.author_id) + << makeKeyValueReference("author_name", m.author_name) + << makeKeyValueReference("msg", m.msg) + << makeKeyValueReference("recv_time", m.recv_time) + << makeKeyValueReference("send_time", m.send_time) + << makeKeyValueReference("id", m.id); + StreamBase& ls = left.getStreamToMember("links"); + ls.getStreamToMember(); + for(std::vector::iterator vit = m.links.begin(); vit != m.links.end(); ++vit) + { + ls.getStreamToMember() << makeKeyValueReference("first", vit->first) + << makeKeyValueReference("second", vit->second) + << makeKeyValueReference("third", vit->third); + } + return left; +} + +bool compare_lobby_id(const ChatHandler::Lobby& l1, const ChatHandler::Lobby& l2) +{ + return l1.id < l2.id; +} + +StreamBase& operator <<(StreamBase& left, KeyValueReference kv) +{ + if(left.serialise()) + { + std::stringstream ss; + ss << kv.value; + left << makeKeyValue(kv.key, ss.str()); + } + else + { + std::string val; + left << makeKeyValueReference(kv.key, val); + std::stringstream ss(val); + ss >> kv.value; + } + return left; +} + +StreamBase& operator << (StreamBase& left, ChatHandler::Lobby& l) +{ + left << makeKeyValueReference("id", l.id) + << makeKeyValue("chat_id", ChatId(l.id).toStdString()) + << makeKeyValueReference("name",l.name) + << makeKeyValueReference("topic", l.topic) + << makeKeyValueReference("subscribed", l.subscribed) + << makeKeyValueReference("auto_subscribe", l.auto_subscribe) + << makeKeyValueReference("is_private", l.is_private) + << makeKeyValueReference("gxs_id", l.gxs_id); + return left; +} + +StreamBase& operator << (StreamBase& left, ChatHandler::ChatInfo& info) +{ + left << makeKeyValueReference("remote_author_id", info.remote_author_id) + << makeKeyValueReference("remote_author_name", info.remote_author_name) + << makeKeyValueReference("is_broadcast", info.is_broadcast) + << makeKeyValueReference("is_gxs_id", info.is_gxs_id) + << makeKeyValueReference("is_lobby", info.is_lobby) + << makeKeyValueReference("is_peer", info.is_peer); + return left; +} + +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") +{ + mNotify->registerNotifyClient(this); + mStateTokenServer->registerTickClient(this); + + mMsgStateToken = mStateTokenServer->getNewToken(); + mLobbiesStateToken = mStateTokenServer->getNewToken(); + mUnreadMsgsStateToken = mStateTokenServer->getNewToken(); + + addResourceHandler("*", this, &ChatHandler::handleWildcard); + addResourceHandler("lobbies", this, &ChatHandler::handleLobbies); + addResourceHandler("subscribe_lobby", this, &ChatHandler::handleSubscribeLobby); + addResourceHandler("unsubscribe_lobby", this, &ChatHandler::handleUnsubscribeLobby); + 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("send_status", this, &ChatHandler::handleSendStatus); + addResourceHandler("unread_msgs", this, &ChatHandler::handleUnreadMsgs); +} + +ChatHandler::~ChatHandler() +{ + mNotify->unregisterNotifyClient(this); + mStateTokenServer->unregisterTickClient(this); +} + +void ChatHandler::notifyChatMessage(const ChatMessage &msg) +{ + RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ + mRawMsgs.push_back(msg); +} + +// to be removed +/* +ChatHandler::Lobby ChatHandler::getLobbyInfo(ChatLobbyId id) +{ + tick(); + + RS_STACK_MUTEX(mMtx); // ********* LOCKED ********** + for(std::vector::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(); +} +*/ + +void ChatHandler::tick() +{ + RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ + + // first fetch lobbies + std::vector lobbies; + std::list subscribed_ids; + mRsMsgs->getChatLobbyList(subscribed_ids); + for(std::list::iterator lit = subscribed_ids.begin(); lit != subscribed_ids.end(); ++lit) + { + ChatLobbyInfo info; + if(mRsMsgs->getChatLobbyInfo(*lit,info)) + { + Lobby l; + l.id = *lit; + l.name = info.lobby_name; + l.topic = info.lobby_topic; + l.subscribed = true; + l.auto_subscribe = info.lobby_flags & RS_CHAT_LOBBY_FLAGS_AUTO_SUBSCRIBE; + l.is_private = !(info.lobby_flags & RS_CHAT_LOBBY_FLAGS_PUBLIC); + l.gxs_id = info.gxs_id; + lobbies.push_back(l); + } + } + + std::vector unsubscribed_lobbies; + mRsMsgs->getListOfNearbyChatLobbies(unsubscribed_lobbies); + for(std::vector::iterator vit = unsubscribed_lobbies.begin(); vit != unsubscribed_lobbies.end(); ++vit) + { + const VisibleChatLobbyRecord& info = *vit; + if(std::find(subscribed_ids.begin(), subscribed_ids.end(), info.lobby_id) == subscribed_ids.end()) + { + Lobby l; + l.id = info.lobby_id; + l.name = info.lobby_name; + l.topic = info.lobby_topic; + l.subscribed = false; + l.auto_subscribe = info.lobby_flags & RS_CHAT_LOBBY_FLAGS_AUTO_SUBSCRIBE; + l.is_private = !(info.lobby_flags & RS_CHAT_LOBBY_FLAGS_PUBLIC); + l.gxs_id = RsGxsId(); + lobbies.push_back(l); + } + } + + // process new messages + bool changed = false; + bool lobby_unread_count_changed = false; + std::vector::iterator> done; + std::vector peers_changed; + + bool gxs_id_failed = false; // to prevent asking for multiple failing gxs ids in one tick, to not flush the cache + + for(std::list::iterator lit = mRawMsgs.begin(); lit != mRawMsgs.end(); ++lit) + { + ChatMessage& msg = *lit; + std::string author_id; + std::string author_name; + if(msg.chat_id.isBroadcast()) + { + author_id = msg.broadcast_peer_id.toStdString(); + author_name = mRsPeers->getPeerName(msg.broadcast_peer_id); + } + else if(msg.chat_id.isGxsId()) + { + author_id = msg.chat_id.toGxsId().toStdString(); + RsIdentityDetails details; + if(!gxs_id_failed && mRsIdentity->getIdDetails(msg.chat_id.toGxsId(), details)) + { + author_name = details.mNickname; + } + else + { + gxs_id_failed = true; + continue; + } + } + else if(msg.chat_id.isLobbyId()) + { + author_id = msg.lobby_peer_gxs_id.toStdString(); + RsIdentityDetails details; + if(!gxs_id_failed && mRsIdentity->getIdDetails(msg.lobby_peer_gxs_id, details)) + { + author_name = details.mNickname; + lobby_unread_count_changed = true; + } + else + { + gxs_id_failed = true; + continue; + } + } + else if(msg.chat_id.isPeerId()) + { + RsPeerId id; + if(msg.incoming) + id = msg.chat_id.toPeerId(); + else + id = mRsPeers->getOwnId(); + author_id = id.toStdString(); + author_name = mRsPeers->getPeerName(id); + if(std::find(peers_changed.begin(), peers_changed.end(), msg.chat_id.toPeerId()) == peers_changed.end()) + peers_changed.push_back(msg.chat_id.toPeerId()); + } + else + { + std::cerr << "Error in ChatHandler::tick(): unhandled chat_id=" << msg.chat_id.toStdString() << std::endl; + // remove from queue, so msgs with wrong ids to not pile up + done.push_back(lit); + continue; + } + + if(mChatInfo.find(msg.chat_id) == mChatInfo.end()) + { + ChatInfo info; + info.is_broadcast = msg.chat_id.isBroadcast(); + info.is_gxs_id = msg.chat_id.isGxsId(); + info.is_lobby = msg.chat_id.isLobbyId(); + info.is_peer = msg.chat_id.isPeerId(); + if(msg.chat_id.isLobbyId()) + { + for(std::vector::iterator vit = mLobbies.begin(); vit != mLobbies.end(); ++vit) + { + if(vit->id == msg.chat_id.toLobbyId()) + info.remote_author_name = vit->name; + } + } + else if(msg.chat_id.isGxsId()) + { + RsIdentityDetails details; + if(!gxs_id_failed && mRsIdentity->getIdDetails(msg.chat_id.toGxsId(), details)) + { + info.remote_author_id = msg.chat_id.toGxsId().toStdString(); + info.remote_author_name = details.mNickname; + } + else + { + gxs_id_failed = true; + continue; + } + } + else if(msg.chat_id.isPeerId()) + { + info.remote_author_id = msg.chat_id.toPeerId().toStdString(); + info.remote_author_name = mRsPeers->getPeerName(msg.chat_id.toPeerId()); + } + mChatInfo[msg.chat_id] = info; + } + + Msg m; + m.read = !msg.incoming; + m.incoming = msg.incoming; + m.was_send = msg.online; + m.author_id = author_id; + m.author_name = author_name; + + // remove html tags from chat message + // extract links form href + const std::string& in = msg.msg; + std::string out; + bool ignore = false; + bool keep_link = false; + std::string last_six_chars; + Triple current_link; + std::vector links; + for(unsigned int i = 0; i < in.size(); ++i) + { + if(keep_link && in[i] == '"') + { + keep_link = false; + current_link.second = out.size(); + } + if(last_six_chars == "href=\"") + { + keep_link = true; + current_link.first = out.size(); + } + + // "rising edge" sets mode to ignore + if(in[i] == '<') + { + ignore = true; + } + std::string a = ""; + if( current_link.first != -1 + && last_six_chars.size() >= a.size() + && last_six_chars.substr(last_six_chars.size()-a.size()) == a) + { + current_link.third = out.size(); + links.push_back(current_link); + current_link = Triple(); + } + if(!ignore || keep_link) + out += in[i]; + // "falling edge" resets mode to keep + if(in[i] == '>') + ignore = false; + + last_six_chars += in[i]; + if(last_six_chars.size() > 6) + last_six_chars = last_six_chars.substr(1); + } + m.msg = out; + m.links = links; + m.recv_time = msg.recvTime; + m.send_time = msg.sendTime; + + m.id = RSRandom::random_u32(); + + mMsgs[msg.chat_id].push_back(m); + done.push_back(lit); + + changed = true; + } + for(std::vector::iterator>::iterator vit = done.begin(); vit != done.end(); ++vit) + mRawMsgs.erase(*vit); + + // send changes + + if(changed) + { + mStateTokenServer->replaceToken(mMsgStateToken); + mStateTokenServer->replaceToken(mUnreadMsgsStateToken); + } + + for(std::vector::iterator vit = peers_changed.begin(); vit != peers_changed.end(); ++vit) + { + const std::list& msgs = mMsgs[ChatId(*vit)]; + uint32_t count = 0; + for(std::list::const_iterator lit = msgs.begin(); lit != msgs.end(); ++lit) + if(!lit->read) + count++; + if(mUnreadMsgNotify) + mUnreadMsgNotify->notifyUnreadMsgCountChanged(*vit, count); + } + + std::sort(lobbies.begin(), lobbies.end(), &compare_lobby_id); + if(lobby_unread_count_changed || mLobbies != lobbies) + { + mStateTokenServer->replaceToken(mLobbiesStateToken); + mLobbies = lobbies; + } +} + +void ChatHandler::handleWildcard(Request &req, Response &resp) +{ + RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ + resp.mDataStream.getStreamToMember(); + for(std::map >::iterator mit = mMsgs.begin(); mit != mMsgs.end(); ++mit) + { + resp.mDataStream.getStreamToMember() << makeValue(mit->first.toStdString()); + } + resp.setOk(); +} + +void ChatHandler::handleLobbies(Request &/*req*/, Response &resp) +{ + tick(); + + RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ + resp.mDataStream.getStreamToMember(); + for(std::vector::iterator vit = mLobbies.begin(); vit != mLobbies.end(); ++vit) + { + uint32_t unread_msgs = 0; + ChatId chat_id(vit->id); + std::map >::iterator mit = mMsgs.find(chat_id); + if(mit != mMsgs.end()) + { + std::list& msgs = mit->second; + for(std::list::iterator lit = msgs.begin(); lit != msgs.end(); ++lit) + if(!lit->read) + unread_msgs++; + } + resp.mDataStream.getStreamToMember() << *vit << makeKeyValueReference("unread_msg_count", unread_msgs); + } + resp.mStateToken = mLobbiesStateToken; + resp.setOk(); +} + +void ChatHandler::handleSubscribeLobby(Request &req, Response &resp) +{ + ChatLobbyId id = 0; + RsGxsId gxs_id; + req.mStream << makeKeyValueReference("id", id) << makeKeyValueReference("gxs_id", gxs_id); + + if(id == 0) + { + resp.setFail("Error: id must not be null"); + return; + } + if(gxs_id.isNull()) + { + resp.setFail("Error: gxs_id must not be null"); + return; + } + if(mRsMsgs->joinVisibleChatLobby(id, gxs_id)) + resp.setOk(); + else + resp.setFail("lobby join failed. (See console for more info)"); +} + +void ChatHandler::handleUnsubscribeLobby(Request &req, Response &resp) +{ + ChatLobbyId id = 0; + req.mStream << makeKeyValueReference("id", id); + mRsMsgs->unsubscribeChatLobby(id); +} + +void ChatHandler::handleMessages(Request &req, Response &resp) +{ + RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ + ChatId id(req.mPath.top()); + // make response a list + resp.mDataStream.getStreamToMember(); + if(id.isNotSet()) + { + resp.setFail("\""+req.mPath.top()+"\" is not a valid chat id"); + return; + } + std::map >::iterator mit = mMsgs.find(id); + if(mit == mMsgs.end()) + { + resp.mStateToken = mMsgStateToken; // even set state token, if not found yet, maybe later messages arrive and then the chat id will be found + resp.setFail("chat with id=\""+req.mPath.top()+"\" not found"); + return; + } + resp.mStateToken = mMsgStateToken; + handlePaginationRequest(req, resp, mit->second); +} + +void ChatHandler::handleSendMessage(Request &req, Response &resp) +{ + std::string chat_id; + std::string msg; + req.mStream << makeKeyValueReference("chat_id", chat_id) + << makeKeyValueReference("msg", msg); + ChatId id(chat_id); + if(id.isNotSet()) + { + resp.setFail("chat_id is invalid"); + return; + } + if(mRsMsgs->sendChat(id, msg)) + resp.setOk(); + else + resp.setFail("failed to send message"); + +} + +void ChatHandler::handleMarkChatAsRead(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; + } + std::map >::iterator mit = mMsgs.find(id); + if(mit == mMsgs.end()) + { + resp.setFail("chat not found. Maybe this chat does not have messages yet?"); + return; + } + std::list& msgs = mit->second; + for(std::list::iterator lit = msgs.begin(); lit != msgs.end(); ++lit) + { + lit->read = true; + } + // lobby list contains unread msgs, so update it + if(id.isLobbyId()) + mStateTokenServer->replaceToken(mLobbiesStateToken); + if(id.isPeerId() && mUnreadMsgNotify) + mUnreadMsgNotify->notifyUnreadMsgCountChanged(id.toPeerId(), 0); + + 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 **********/ + ChatId id(req.mPath.top()); + if(id.isNotSet()) + { + resp.setFail("\""+req.mPath.top()+"\" is not a valid chat id"); + return; + } + std::map::iterator mit = mChatInfo.find(id); + if(mit == mChatInfo.end()) + { + resp.setFail("chat not found."); + return; + } + resp.mDataStream << mit->second; + resp.setOk(); +} + +void ChatHandler::handleTypingLabel(Request &req, Response &resp) +{ + +} + +void ChatHandler::handleSendStatus(Request &req, Response &resp) +{ + +} + +void ChatHandler::handleUnreadMsgs(Request &req, Response &resp) +{ + RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ + + resp.mDataStream.getStreamToMember(); + for(std::map >::const_iterator mit = mMsgs.begin(); mit != mMsgs.end(); ++mit) + { + uint32_t count = 0; + for(std::list::const_iterator lit = mit->second.begin(); lit != mit->second.end(); ++lit) + if(!lit->read) + count++; + std::map::iterator mit2 = mChatInfo.find(mit->first); + if(mit2 == mChatInfo.end()) + std::cerr << "Error in ChatHandler::handleUnreadMsgs(): ChatInfo not found. It is weird if this happens. Normally it should not happen." << std::endl; + if(count && (mit2 != mChatInfo.end())) + { + resp.mDataStream.getStreamToMember() + << makeKeyValue("id", mit->first.toStdString()) + << makeKeyValueReference("unread_count", count) + << mit2->second; + } + } + resp.mStateToken = mUnreadMsgsStateToken; +} + +} // namespace resource_api diff --git a/libresapi/src/api/ChatHandler.h b/libresapi/src/api/ChatHandler.h index 704aade3f..b849c2d98 100644 --- a/libresapi/src/api/ChatHandler.h +++ b/libresapi/src/api/ChatHandler.h @@ -3,48 +3,124 @@ #include "ResourceRouter.h" #include "StateTokenServer.h" #include +#include -class RsMsgs; +class RsPeers; +class RsIdentity; namespace resource_api { -class ChatHandler: public ResourceRouter, NotifyClient +class UnreadMsgNotify{ +public: + virtual void notifyUnreadMsgCountChanged(const RsPeerId& peer, uint32_t count) = 0; +}; + +class ChatHandler: public ResourceRouter, NotifyClient, Tickable { public: - ChatHandler(StateTokenServer* sts, RsNotify* notify, RsMsgs* msgs); + ChatHandler(StateTokenServer* sts, RsNotify* notify, RsMsgs* msgs, RsPeers* peers, RsIdentity *identity, UnreadMsgNotify* unread); virtual ~ChatHandler(); // from NotifyClient // note: this may get called from the own and from foreign threads - virtual void notifyChatMessage(); + virtual void notifyChatMessage(const ChatMessage& msg); -private: - void handleWildcard(Request& req, Response& resp); + // from tickable + virtual void tick(); - StateTokenServer* mStateTokenServer; - RsNotify* mNotify; - RsMsgs* mRsMsgs; - - RsMutex mMtx; - StateToken mStateToken; // mutex protected - - // msgs flow like this: - // chatservice -> rawMsgs -> processedMsgs -> deletion - - std::map > mRawMsgs; +public: + class Triple + { + public: + Triple(): first(-1), second(-1), third(-1){} + double first; + double second; + double third; + }; class Msg{ public: + bool read; bool incoming; bool was_send; //std::string chat_type; std::string author_id; // peer or gxs id or "system" for system messages std::string author_name; std::string msg; // plain text only! + std::vector links; + uint32_t recv_time; + uint32_t send_time; + + uint32_t id; }; - std::map > mProcessedMsgs; + class Lobby{ + public: + Lobby(): id(0), subscribed(false), auto_subscribe(false), is_private(false){} + ChatLobbyId id; + std::string name; + std::string topic; + bool subscribed; + bool auto_subscribe; + bool is_private; + + RsGxsId gxs_id;// for subscribed lobbies: the id we use to write messages + + bool operator==(const Lobby& l) const + { + return id == l.id + && name == l.name + && topic == l.topic + && subscribed == l.subscribed + && auto_subscribe == l.auto_subscribe + && is_private == l.is_private + && gxs_id == l.gxs_id; + } + }; + + class ChatInfo{ + public: + bool is_broadcast; + bool is_gxs_id; + bool is_lobby; + bool is_peer; + std::string remote_author_id; + std::string remote_author_name; + }; + +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); + 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); + void handleSendStatus(Request& req, Response& resp); + void handleUnreadMsgs(Request& req, Response& resp); + + StateTokenServer* mStateTokenServer; + RsNotify* mNotify; + RsMsgs* mRsMsgs; + RsPeers* mRsPeers; + RsIdentity* mRsIdentity; + UnreadMsgNotify* mUnreadMsgNotify; + + RsMutex mMtx; + + StateToken mMsgStateToken; + std::list mRawMsgs; + std::map > mMsgs; + + std::map mChatInfo; + + StateToken mLobbiesStateToken; + std::vector mLobbies; + + StateToken mUnreadMsgsStateToken; }; } // namespace resource_api diff --git a/libresapi/src/api/GxsResponseTask.cpp b/libresapi/src/api/GxsResponseTask.cpp index 2ccb917e0..10142aa92 100644 --- a/libresapi/src/api/GxsResponseTask.cpp +++ b/libresapi/src/api/GxsResponseTask.cpp @@ -112,4 +112,17 @@ void GxsResponseTask::streamGxsId(RsGxsId id, StreamBase &stream) } } +std::string GxsResponseTask::getName(RsGxsId id) +{ + for(std::vector::iterator vit = mIdentityDetails.begin(); + vit != mIdentityDetails.end(); ++vit) + { + if(vit->mId == id) + return vit->mNickname; + } + std::cerr << "Warning: identity not found in GxsResponseTask::getName(). This is probably a bug. You must call GxsResponseTask::requestGxsId() before you can get the name." << std::endl; + return ""; +} + + } // namespace resource_api diff --git a/libresapi/src/api/GxsResponseTask.h b/libresapi/src/api/GxsResponseTask.h index f268d205f..7e133cdb9 100644 --- a/libresapi/src/api/GxsResponseTask.h +++ b/libresapi/src/api/GxsResponseTask.h @@ -36,6 +36,7 @@ protected: void requestGxsId(RsGxsId id); // call stream function in the next cycle, then the names are available void streamGxsId(RsGxsId id, StreamBase& stream); + std::string getName(RsGxsId id); private: RsIdentity* mIdService; RsTokenService* mTokenService; diff --git a/libresapi/src/api/IdentityHandler.cpp b/libresapi/src/api/IdentityHandler.cpp index 9c92ebd82..26578380f 100644 --- a/libresapi/src/api/IdentityHandler.cpp +++ b/libresapi/src/api/IdentityHandler.cpp @@ -127,8 +127,11 @@ void IdentityHandler::handleWildcard(Request &req, Response &resp) ResponseTask* IdentityHandler::handleOwn(Request &req, Response &resp) { std::list ids; - mRsIdentity->getOwnIds(ids); - return new SendIdentitiesListTask(mRsIdentity, ids); + if(mRsIdentity->getOwnIds(ids)) + return new SendIdentitiesListTask(mRsIdentity, ids); + resp.mDataStream.getStreamToMember(); + resp.setWarning("identities not loaded yet"); + return 0; } } // namespace resource_api diff --git a/libresapi/src/api/Pagination.h b/libresapi/src/api/Pagination.h index 35b283efa..1d16b7ada 100644 --- a/libresapi/src/api/Pagination.h +++ b/libresapi/src/api/Pagination.h @@ -11,13 +11,15 @@ namespace resource_api // the type returned by dereferencing the iterator should have a stream operator for StreamBase // the stream operator must not add an element "id", this is done by the pagination handler template -void handlePaginationRequest(Request& req, Response& resp, const C& data) +void handlePaginationRequest(Request& req, Response& resp, C& data) { + /* if(!req.isGet()){ resp.mDebug << "unsupported method. only GET is allowed." << std::endl; resp.setFail(); return; } + */ if(data.begin() == data.end()){ // set result type to list resp.mDataStream.getStreamToMember(); @@ -25,15 +27,16 @@ void handlePaginationRequest(Request& req, Response& resp, const C& data) return; } - std::string first; + std::string begin_after; std::string last; - req.mStream << makeKeyValueReference("first", first) << makeKeyValueReference("last", last); + req.mStream << makeKeyValueReference("begin_after", begin_after) << makeKeyValueReference("last", last); - C::iterator it_first = data.begin(); - if(first != "begin") + typename C::iterator it_first = data.begin(); + if(begin_after != "begin" && begin_after != "") { - while(it_first != data.end() && id(*it_first) != first) + while(it_first != data.end() && id(*it_first) != begin_after) it_first++; + it_first++; // get after the specified element if(it_first == data.end()) { resp.setFail("Error: first id did not match any element"); @@ -41,8 +44,8 @@ void handlePaginationRequest(Request& req, Response& resp, const C& data) } } - C::iterator it_last = data.begin(); - if(last == "end") + typename C::iterator it_last = data.begin(); + if(last == "end" || last == "") { it_last = data.end(); } @@ -59,7 +62,7 @@ void handlePaginationRequest(Request& req, Response& resp, const C& data) } int count = 0; - for(C::iterator it = it_first; it != it_last; ++it) + for(typename C::iterator it = it_first; it != it_last; ++it) { StreamBase& stream = resp.mDataStream.getStreamToMember(); stream << *it; diff --git a/libresapi/src/api/PeersHandler.cpp b/libresapi/src/api/PeersHandler.cpp index 1a296c0fb..9cdf29897 100644 --- a/libresapi/src/api/PeersHandler.cpp +++ b/libresapi/src/api/PeersHandler.cpp @@ -28,7 +28,8 @@ bool peerInfoToStream(StreamBase& stream, RsPeerDetails& details, RsPeers* peers { bool ok = true; peerDetailsToStream(stream, details); - stream << makeKeyValue("is_online", peers->isOnline(details.id)); + stream << makeKeyValue("is_online", peers->isOnline(details.id)) + << makeKeyValue("chat_id", ChatId(details.id).toStdString()); std::string avatar_address = "/"+details.id.toStdString()+"/avatar_image"; @@ -91,7 +92,7 @@ void PeersHandler::tick() { std::list online; mRsPeers->getOnlineList(online); - if(!std::equal(online.begin(), online.end(), mOnlinePeers.begin())) + if(online != mOnlinePeers) { mOnlinePeers = online; @@ -101,6 +102,13 @@ void PeersHandler::tick() } } +void PeersHandler::notifyUnreadMsgCountChanged(const RsPeerId &peer, uint32_t count) +{ + RsStackMutex stack(mMtx); /********** STACK LOCKED MTX ******/ + mUnreadMsgsCounts[peer] = count; + mStateTokenServer->replaceToken(mStateToken); +} + static bool have_avatar(RsMsgs* msgs, const RsPeerId& id) { // check if avatar data is available @@ -163,6 +171,11 @@ void PeersHandler::handleWildcard(Request &req, Response &resp) // no more path element if(req.isGet()) { + std::map unread_msgs; + { + RsStackMutex stack(mMtx); /********** STACK LOCKED MTX ******/ + unread_msgs = mUnreadMsgsCounts; + } // list all peers ok = true; std::list identities; @@ -204,7 +217,14 @@ void PeersHandler::handleWildcard(Request &req, Response &resp) for(std::vector::iterator vit = detailsVec.begin(); vit != detailsVec.end(); ++vit) { if(vit->gpg_id == *lit) - peerInfoToStream(locationStream.getStreamToMember(),*vit, mRsPeers, grpInfo, have_avatar(mRsMsgs, vit->id)); + { + StreamBase& stream = locationStream.getStreamToMember(); + double unread = 0; + if(unread_msgs.find(vit->id) != unread_msgs.end()) + unread = unread_msgs.find(vit->id)->second; + stream << makeKeyValueReference("unread_msgs", unread); + peerInfoToStream(stream,*vit, mRsPeers, grpInfo, have_avatar(mRsMsgs, vit->id)); + } } } resp.mStateToken = getCurrentStateToken(); diff --git a/libresapi/src/api/PeersHandler.h b/libresapi/src/api/PeersHandler.h index e5b5fa6ea..dfcc9e10c 100644 --- a/libresapi/src/api/PeersHandler.h +++ b/libresapi/src/api/PeersHandler.h @@ -2,6 +2,7 @@ #include "ResourceRouter.h" #include "StateTokenServer.h" +#include "ChatHandler.h" #include #include @@ -11,7 +12,7 @@ class RsMsgs; namespace resource_api { -class PeersHandler: public ResourceRouter, NotifyClient, Tickable +class PeersHandler: public ResourceRouter, NotifyClient, Tickable, public UnreadMsgNotify { public: PeersHandler(StateTokenServer* sts, RsNotify* notify, RsPeers* peers, RsMsgs* msgs); @@ -24,6 +25,12 @@ public: // from Tickable virtual void tick(); + + // from UnreadMsgNotify + // ChatHandler calls this to tell us about unreadmsgs + // this allows to merge unread msgs info with the peers list + virtual void notifyUnreadMsgCountChanged(const RsPeerId& peer, uint32_t count); + private: void handleWildcard(Request& req, Response& resp); void handleExamineCert(Request& req, Response& resp); @@ -40,5 +47,6 @@ private: RsMutex mMtx; StateToken mStateToken; // mutex protected + std::map mUnreadMsgsCounts; }; } // namespace resource_api diff --git a/libresapi/src/webfiles/gui.jsx b/libresapi/src/webfiles/gui.jsx index 6a86a8dc2..e2c39e37b 100644 --- a/libresapi/src/webfiles/gui.jsx +++ b/libresapi/src/webfiles/gui.jsx @@ -98,7 +98,43 @@ var AutoUpdateMixin = }, }; -// the signlaSlotServer decouples event senders from event receivers +// similar to auto update mixin, but for immutable resources +// fetches data only once +var OneTimeUpdateMixin = +{ + // react component lifecycle callbacks + componentDidMount: function() + { + this._aum_debug("OneTimeUpdateMixin did mount path="+this.getPath()); + this._aum_on_data_changed(); + }, + // private OneTimeUpdateMixin methods + _aum_debug: function(msg) + { + console.log(msg); + }, + _aum_on_data_changed: function() + { + RS.request({path: this.getPath()}, this._aum_response_callback); + }, + _aum_response_callback: function(resp) + { + this._aum_debug("OneTimeUpdateMixin received data: "+JSON.stringify(resp)); + // it is impossible to update the state of an unmounted component + // but it may happen that the component is unmounted before a request finishes + // if response is too late, we drop it + if(!this.isMounted()) + { + this._aum_debug("OneTimeUpdateMixin: component not mounted. Discarding response. path="+this.getPath()); + return; + } + var state = this.state; + state.data = resp.data; + this.setState(state); + }, +}; + +// the signalSlotServer decouples event senders from event receivers // senders just send their events // the server will forwards them to all receivers // receivers have to register/unregister at the server @@ -311,7 +347,12 @@ var Peers3 = React.createClass({ }; if(loc.is_online) online_style.backgroundColor = "lime"; - return(
{/*
*/}{loc.location}
); + //console.log(loc); + return(
+ {/*
*/}{loc.location} {loc.unread_msgs !== 0? ("("+loc.unread_msgs + " unread msgs)"): ""} +
); }); var avatars = this.props.data.locations.map(function(loc){ if(loc.is_online && (loc.avatar_address !== "")) @@ -978,7 +1019,7 @@ var LoginWidget2 = React.createClass({ if(this.state.state === "waiting") { return(
-

please wait a second...

+

please wait a second... (LoginWidget2)

); //return(

Retroshare is initialising... please wait...

); } @@ -1070,6 +1111,12 @@ var Menu = React.createClass({ }, render: function(){ var outer = this; + var shutdownbutton; + if(this.props.fullcontrol === true) + shutdownbutton =
shutdown Retroshare
; + else + shutdownbutton =
; + return (
@@ -1091,6 +1138,10 @@ var Menu = React.createClass({
Search
+
+ Chatlobbies (unfinished) +
+ {shutdownbutton} {/*
TestWidget
*/} @@ -1099,6 +1150,331 @@ var Menu = React.createClass({ }, }); +var global_author_identity = null; + +var IdentitySelectorWidget = React.createClass({ + mixins: [AutoUpdateMixin], + getInitialState: function(){ + return {data: []}; + }, + getPath: function(){ + return "identity/own"; + }, + render: function(){ + if(this.state.data.length !== 0) + { + global_author_identity = this.state.data[0].gxs_id; + return
using identity {this.state.data[0].name}
; + } + else + { + return
error: no identity found. create_new_identity not implemented. try a page reload.
; + } + var c = this; + return( +
+ { + this.state.data.map(function(id){ + return
{id.name}
; + }) + } +
+ ); + }, +}); + +var LobbyListWidget = React.createClass({ + mixins: [AutoUpdateMixin, SignalSlotMixin], + getInitialState: function(){ + return {data: []}; + }, + getPath: function(){ + return "chat/lobbies"; + }, + enterLobby: function(id, chat_id) + { + var c = this; + if(global_author_identity === null) + { + alert("no identity selected, can't join a lobby"); + return; + } + RS.request( + {path:"chat/subscribe_lobby", data:{id:id, gxs_id:global_author_identity}}, + function(){ + c.emit("change_url", {url: "chat/"+chat_id}); + } + ); + }, + render: function(){ + var c = this; + return( +
+ { + this.state.data.map(function(lobby){ + return
{lobby.name}
{lobby.topic}
{lobby.unread_msg_count + " unread msgs"}
; + }) + } +
+ ); + }, +}); + +// implements automatic update using the state token system +// components using this mixin should have a member "getPath()" to specify the resource +// this widget handles paginated resources +var ListAutoUpdateMixin = +{ + // react component lifecycle callbacks + componentDidMount: function() + { + this._aum_debug("ListAutoUpdateMixin did mount path="+this.getPath()); + this._aum_on_data_changed(); + }, + componentWillUnmount: function() + { + this._aum_debug("ListAutoUpdateMixin will unmount path="+this.getPath()); + RS.unregister_token_listener(this._aum_on_data_changed); + }, + + // private auto update mixin methods + _aum_debug: function(msg) + { + console.log(msg); + }, + _aum_on_data_changed: function() + { + if(this.state.data.length === 0) + { + this._aum_debug("ListAutoUpdateMixin: first request"); + RS.request({path: this.getPath()}, this._aum_response_callback); + } + else + { + this._aum_debug("ListAutoUpdateMixin: requesting elements after id="+this._aum_last_id); + RS.request({path: this.getPath(), data: {begin_after: this.state.data[this.state.data.length-1].id}}, this._aum_response_callback); + } + }, + _aum_response_callback: function(resp) + { + this._aum_debug("Mixin received data: "+JSON.stringify(resp)); + // it is impossible to update the state of an unmounted component + // but it may happen that the component is unmounted before a request finishes + // if response is too late, we drop it + if(!this.isMounted()) + { + this._aum_debug("ListAutoUpdateMixin: component not mounted. Discarding response. path="+this.getPath()); + return; + } + var state = this.state; + if(state.data.length === 0) + { + this._aum_debug("ListAutoUpdateMixin: received first response"); + state.data = resp.data; + } + else + { + this._aum_debug("ListAutoUpdateMixin: appending response to the end"); + state.data = state.data.concat(resp.data); + if(resp.data.length !== 0) + this._aum_last_id = resp.data[resp.data.length-1].id; + } + if(this.onDataUpdated) + this.onDataUpdated(); + this.setState(state); + // load mroe data until there is no more data left + if(resp.data.length === 0) + { + RS.unregister_token_listener(this._aum_on_data_changed); + RS.register_token_listener(this._aum_on_data_changed, resp.statetoken); + } + else + { + this._aum_on_data_changed(); + } + }, +}; + +function getChatTypeString(info) +{ + if(info.is_broadcast) + return "[broadcast]"; + if(info.is_gxs_id) + return "[distant]"; + if(info.is_lobby) + return "[lobby]"; + if(info.is_peer) + return "[friend]"; + return "[unknown chat type]"; +} + +var ChatInfoWidget = React.createClass({ + mixins: [OneTimeUpdateMixin], + getInitialState: function(){ + return {data: null}; + }, + getPath: function(){ + return "chat/info/"+this.props.id; + }, + render: function(){ + if(this.state.data === null) + return
+ return( +
+ {getChatTypeString(this.state.data)} {this.state.data.remote_author_name} +
+
+ ); + }, +}); + +// ChatWidget sets this, and the unread msgs widget ignores unread smgs with this id +var global_current_chat_id = null; + +var UnreadChatMsgsCountWidget = React.createClass({ + mixins: [AutoUpdateMixin, SignalSlotMixin], + getInitialState: function(){ + return {data: []}; + }, + getPath: function(){ + return "chat/unread_msgs"; + }, + render: function(){ + var c = this; + var count = 0; + for(var i in this.state.data) + { + if(this.state.data[i].id !== global_current_chat_id) + count = count + parseInt(this.state.data[i].unread_count); + } + return( +
+ {count !== 0? (count + " unread chat messages. click here to read them."): ""} +
+ ); + }, +}); + +var UnreadChatMsgsListWidget = React.createClass({ + mixins: [AutoUpdateMixin, SignalSlotMixin], + getInitialState: function(){ + return {data: []}; + }, + getPath: function(){ + return "chat/unread_msgs"; + }, + render: function(){ + var c = this; + return( +
+
Unread Chats:
+ {this.state.data.map(function(i){ + return
+ {getChatTypeString(i)} {"["+i.unread_count+"]"} {i.remote_author_name} +
; + })} +
+ ); + }, +}); + +var LinkWidget = React.createClass({ + getInitialState: function(){ + return {expanded: false}; + }, + render: function(){ + var c = this; + if(this.state.expanded){ + return
Really follow this link? {this.props.url}
close
; + } + else{ + return {this.props.label}; + } + }, +}); + +// text: a string +// links: [{first:1, second:2, third:3}] +// text between first and second is the url of the link +// text between secon and third is the link label +function markup(text, links) +{ + function debug(stuff){console.log(stuff)} + var out = []; + var last_link = 0; + debug(text); + debug(links); + for(var i in links) + { + out.push(text.substr(last_link, links[i].first).replace(/ /g, " ")); + var url = text.substr(links[i].first, links[i].second); + url = url.replace(/&/g, "&"); + var label = text.substr(links[i].second, links[i].third); + label = label.replace(/ /g, " "); + last_link = links[i].third; + debug(links[i]); + debug(url); + debug(label); + out.push(); + } + out.push(text.substr(last_link).replace(/ /g, " ")); + debug(out); + return out; +} + +var ChatWidget = React.createClass({ + mixins: [ListAutoUpdateMixin], + getInitialState: function(){ + return {data: []}; + }, + getPath: function(){ + return "chat/messages/"+this.props.id; + }, + onDataUpdated: function() + { + RS.request({path:"chat/mark_chat_as_read/"+this.props.id}); + }, + // react component lifecycle callbacks + componentDidMount: function() + { + global_current_chat_id = this.props.id; + }, + componentWillUnmount: function() + { + global_current_chat_id = null; + }, + render: function(){ + var c = this; + return( +
+ + { + this.state.data.map(function(msg){ + return
{msg.send_time} {msg.author_name}: {markup(msg.msg, msg.links)}
; + }) + } + +
+ ); + }, +}); + var TestWidget = React.createClass({ mixins: [SignalSlotMixin], getInitialState: function(){ @@ -1242,6 +1618,9 @@ var MainWidget = React.createClass({ }); }, render: function(){ + console.log("MainWidget render()"); + console.log(this.state); + var outer = this; var mainpage =

page not implemented: {this.state.page}

; @@ -1286,6 +1665,18 @@ var MainWidget = React.createClass({ { mainpage = ; } + if(this.state.page === "lobbies") + { + mainpage = ; + } + if(this.state.page.split("/")[0] === "chat") + { + mainpage = ; + } + if(this.state.page === "unread_chats") + { + mainpage = ; + } if(this.state.page === "add_friend") { mainpage = ; @@ -1294,6 +1685,12 @@ var MainWidget = React.createClass({ { mainpage = ; } + mainpage =
+ + + + {mainpage} +
; } var menubutton =
<- menu
; @@ -1304,7 +1701,6 @@ var MainWidget = React.createClass({ {/*
test
*/} - {menubutton} {mainpage} {/**/} diff --git a/libresapi/src/webui/gui.jsx b/libresapi/src/webui/gui.jsx index 6a86a8dc2..e2c39e37b 100644 --- a/libresapi/src/webui/gui.jsx +++ b/libresapi/src/webui/gui.jsx @@ -98,7 +98,43 @@ var AutoUpdateMixin = }, }; -// the signlaSlotServer decouples event senders from event receivers +// similar to auto update mixin, but for immutable resources +// fetches data only once +var OneTimeUpdateMixin = +{ + // react component lifecycle callbacks + componentDidMount: function() + { + this._aum_debug("OneTimeUpdateMixin did mount path="+this.getPath()); + this._aum_on_data_changed(); + }, + // private OneTimeUpdateMixin methods + _aum_debug: function(msg) + { + console.log(msg); + }, + _aum_on_data_changed: function() + { + RS.request({path: this.getPath()}, this._aum_response_callback); + }, + _aum_response_callback: function(resp) + { + this._aum_debug("OneTimeUpdateMixin received data: "+JSON.stringify(resp)); + // it is impossible to update the state of an unmounted component + // but it may happen that the component is unmounted before a request finishes + // if response is too late, we drop it + if(!this.isMounted()) + { + this._aum_debug("OneTimeUpdateMixin: component not mounted. Discarding response. path="+this.getPath()); + return; + } + var state = this.state; + state.data = resp.data; + this.setState(state); + }, +}; + +// the signalSlotServer decouples event senders from event receivers // senders just send their events // the server will forwards them to all receivers // receivers have to register/unregister at the server @@ -311,7 +347,12 @@ var Peers3 = React.createClass({ }; if(loc.is_online) online_style.backgroundColor = "lime"; - return(
{/*
*/}{loc.location}
); + //console.log(loc); + return(
+ {/*
*/}{loc.location} {loc.unread_msgs !== 0? ("("+loc.unread_msgs + " unread msgs)"): ""} +
); }); var avatars = this.props.data.locations.map(function(loc){ if(loc.is_online && (loc.avatar_address !== "")) @@ -978,7 +1019,7 @@ var LoginWidget2 = React.createClass({ if(this.state.state === "waiting") { return(
-

please wait a second...

+

please wait a second... (LoginWidget2)

); //return(

Retroshare is initialising... please wait...

); } @@ -1070,6 +1111,12 @@ var Menu = React.createClass({ }, render: function(){ var outer = this; + var shutdownbutton; + if(this.props.fullcontrol === true) + shutdownbutton =
shutdown Retroshare
; + else + shutdownbutton =
; + return (
@@ -1091,6 +1138,10 @@ var Menu = React.createClass({
Search
+
+ Chatlobbies (unfinished) +
+ {shutdownbutton} {/*
TestWidget
*/} @@ -1099,6 +1150,331 @@ var Menu = React.createClass({ }, }); +var global_author_identity = null; + +var IdentitySelectorWidget = React.createClass({ + mixins: [AutoUpdateMixin], + getInitialState: function(){ + return {data: []}; + }, + getPath: function(){ + return "identity/own"; + }, + render: function(){ + if(this.state.data.length !== 0) + { + global_author_identity = this.state.data[0].gxs_id; + return
using identity {this.state.data[0].name}
; + } + else + { + return
error: no identity found. create_new_identity not implemented. try a page reload.
; + } + var c = this; + return( +
+ { + this.state.data.map(function(id){ + return
{id.name}
; + }) + } +
+ ); + }, +}); + +var LobbyListWidget = React.createClass({ + mixins: [AutoUpdateMixin, SignalSlotMixin], + getInitialState: function(){ + return {data: []}; + }, + getPath: function(){ + return "chat/lobbies"; + }, + enterLobby: function(id, chat_id) + { + var c = this; + if(global_author_identity === null) + { + alert("no identity selected, can't join a lobby"); + return; + } + RS.request( + {path:"chat/subscribe_lobby", data:{id:id, gxs_id:global_author_identity}}, + function(){ + c.emit("change_url", {url: "chat/"+chat_id}); + } + ); + }, + render: function(){ + var c = this; + return( +
+ { + this.state.data.map(function(lobby){ + return
{lobby.name}
{lobby.topic}
{lobby.unread_msg_count + " unread msgs"}
; + }) + } +
+ ); + }, +}); + +// implements automatic update using the state token system +// components using this mixin should have a member "getPath()" to specify the resource +// this widget handles paginated resources +var ListAutoUpdateMixin = +{ + // react component lifecycle callbacks + componentDidMount: function() + { + this._aum_debug("ListAutoUpdateMixin did mount path="+this.getPath()); + this._aum_on_data_changed(); + }, + componentWillUnmount: function() + { + this._aum_debug("ListAutoUpdateMixin will unmount path="+this.getPath()); + RS.unregister_token_listener(this._aum_on_data_changed); + }, + + // private auto update mixin methods + _aum_debug: function(msg) + { + console.log(msg); + }, + _aum_on_data_changed: function() + { + if(this.state.data.length === 0) + { + this._aum_debug("ListAutoUpdateMixin: first request"); + RS.request({path: this.getPath()}, this._aum_response_callback); + } + else + { + this._aum_debug("ListAutoUpdateMixin: requesting elements after id="+this._aum_last_id); + RS.request({path: this.getPath(), data: {begin_after: this.state.data[this.state.data.length-1].id}}, this._aum_response_callback); + } + }, + _aum_response_callback: function(resp) + { + this._aum_debug("Mixin received data: "+JSON.stringify(resp)); + // it is impossible to update the state of an unmounted component + // but it may happen that the component is unmounted before a request finishes + // if response is too late, we drop it + if(!this.isMounted()) + { + this._aum_debug("ListAutoUpdateMixin: component not mounted. Discarding response. path="+this.getPath()); + return; + } + var state = this.state; + if(state.data.length === 0) + { + this._aum_debug("ListAutoUpdateMixin: received first response"); + state.data = resp.data; + } + else + { + this._aum_debug("ListAutoUpdateMixin: appending response to the end"); + state.data = state.data.concat(resp.data); + if(resp.data.length !== 0) + this._aum_last_id = resp.data[resp.data.length-1].id; + } + if(this.onDataUpdated) + this.onDataUpdated(); + this.setState(state); + // load mroe data until there is no more data left + if(resp.data.length === 0) + { + RS.unregister_token_listener(this._aum_on_data_changed); + RS.register_token_listener(this._aum_on_data_changed, resp.statetoken); + } + else + { + this._aum_on_data_changed(); + } + }, +}; + +function getChatTypeString(info) +{ + if(info.is_broadcast) + return "[broadcast]"; + if(info.is_gxs_id) + return "[distant]"; + if(info.is_lobby) + return "[lobby]"; + if(info.is_peer) + return "[friend]"; + return "[unknown chat type]"; +} + +var ChatInfoWidget = React.createClass({ + mixins: [OneTimeUpdateMixin], + getInitialState: function(){ + return {data: null}; + }, + getPath: function(){ + return "chat/info/"+this.props.id; + }, + render: function(){ + if(this.state.data === null) + return
+ return( +
+ {getChatTypeString(this.state.data)} {this.state.data.remote_author_name} +
+
+ ); + }, +}); + +// ChatWidget sets this, and the unread msgs widget ignores unread smgs with this id +var global_current_chat_id = null; + +var UnreadChatMsgsCountWidget = React.createClass({ + mixins: [AutoUpdateMixin, SignalSlotMixin], + getInitialState: function(){ + return {data: []}; + }, + getPath: function(){ + return "chat/unread_msgs"; + }, + render: function(){ + var c = this; + var count = 0; + for(var i in this.state.data) + { + if(this.state.data[i].id !== global_current_chat_id) + count = count + parseInt(this.state.data[i].unread_count); + } + return( +
+ {count !== 0? (count + " unread chat messages. click here to read them."): ""} +
+ ); + }, +}); + +var UnreadChatMsgsListWidget = React.createClass({ + mixins: [AutoUpdateMixin, SignalSlotMixin], + getInitialState: function(){ + return {data: []}; + }, + getPath: function(){ + return "chat/unread_msgs"; + }, + render: function(){ + var c = this; + return( +
+
Unread Chats:
+ {this.state.data.map(function(i){ + return
+ {getChatTypeString(i)} {"["+i.unread_count+"]"} {i.remote_author_name} +
; + })} +
+ ); + }, +}); + +var LinkWidget = React.createClass({ + getInitialState: function(){ + return {expanded: false}; + }, + render: function(){ + var c = this; + if(this.state.expanded){ + return
Really follow this link? {this.props.url}
close
; + } + else{ + return {this.props.label}; + } + }, +}); + +// text: a string +// links: [{first:1, second:2, third:3}] +// text between first and second is the url of the link +// text between secon and third is the link label +function markup(text, links) +{ + function debug(stuff){console.log(stuff)} + var out = []; + var last_link = 0; + debug(text); + debug(links); + for(var i in links) + { + out.push(text.substr(last_link, links[i].first).replace(/ /g, " ")); + var url = text.substr(links[i].first, links[i].second); + url = url.replace(/&/g, "&"); + var label = text.substr(links[i].second, links[i].third); + label = label.replace(/ /g, " "); + last_link = links[i].third; + debug(links[i]); + debug(url); + debug(label); + out.push(); + } + out.push(text.substr(last_link).replace(/ /g, " ")); + debug(out); + return out; +} + +var ChatWidget = React.createClass({ + mixins: [ListAutoUpdateMixin], + getInitialState: function(){ + return {data: []}; + }, + getPath: function(){ + return "chat/messages/"+this.props.id; + }, + onDataUpdated: function() + { + RS.request({path:"chat/mark_chat_as_read/"+this.props.id}); + }, + // react component lifecycle callbacks + componentDidMount: function() + { + global_current_chat_id = this.props.id; + }, + componentWillUnmount: function() + { + global_current_chat_id = null; + }, + render: function(){ + var c = this; + return( +
+ + { + this.state.data.map(function(msg){ + return
{msg.send_time} {msg.author_name}: {markup(msg.msg, msg.links)}
; + }) + } + +
+ ); + }, +}); + var TestWidget = React.createClass({ mixins: [SignalSlotMixin], getInitialState: function(){ @@ -1242,6 +1618,9 @@ var MainWidget = React.createClass({ }); }, render: function(){ + console.log("MainWidget render()"); + console.log(this.state); + var outer = this; var mainpage =

page not implemented: {this.state.page}

; @@ -1286,6 +1665,18 @@ var MainWidget = React.createClass({ { mainpage = ; } + if(this.state.page === "lobbies") + { + mainpage = ; + } + if(this.state.page.split("/")[0] === "chat") + { + mainpage = ; + } + if(this.state.page === "unread_chats") + { + mainpage = ; + } if(this.state.page === "add_friend") { mainpage = ; @@ -1294,6 +1685,12 @@ var MainWidget = React.createClass({ { mainpage = ; } + mainpage =
+ + + + {mainpage} +
; } var menubutton =
<- menu
; @@ -1304,7 +1701,6 @@ var MainWidget = React.createClass({ {/*
test
*/} - {menubutton} {mainpage} {/**/} diff --git a/retroshare-gui/src/gui/settings/WebuiPage.cpp b/retroshare-gui/src/gui/settings/WebuiPage.cpp index 1f5b1166d..3b6770c6c 100644 --- a/retroshare-gui/src/gui/settings/WebuiPage.cpp +++ b/retroshare-gui/src/gui/settings/WebuiPage.cpp @@ -117,7 +117,7 @@ QString WebuiPage::helpText() const } else { - QMessageBox::warning(0, tr("Webinterface not enabled"), "The webinterface is not enabled. Enable it in Settings -> Webinterface."); + QMessageBox::warning(0, tr("Webinterface not enabled"), tr("The webinterface is not enabled. Enable it in Settings -> Webinterface.")); } }