From 06f30543730c372226c398c11b3de0213d711d13 Mon Sep 17 00:00:00 2001
From: dec05eba <dec05eba@protonmail.com>
Date: Wed, 8 Aug 2018 23:17:10 +0200
Subject: Add support for discord

---
 src/Cache.cpp           |  51 ++++++-
 src/Channel.cpp         |  86 +++++++++++-
 src/ChannelTopPanel.cpp |   2 +-
 src/Chatbar.cpp         |  14 ++
 src/MessageBoard.cpp    |  51 +++++--
 src/ResourceCache.cpp   |  24 ++++
 src/Rpc.cpp             |  39 ++++++
 src/Suggestions.cpp     |  54 ++++++++
 src/Text.cpp            |  55 ++++----
 src/User.cpp            |  25 +++-
 src/UsersSidePanel.cpp  | 126 ++++++++---------
 src/main.cpp            | 361 ++++++++++++++++++++++++++++++++++++++++++++++--
 12 files changed, 753 insertions(+), 135 deletions(-)
 create mode 100644 src/Rpc.cpp
 create mode 100644 src/Suggestions.cpp

(limited to 'src')

diff --git a/src/Cache.cpp b/src/Cache.cpp
index 2a09591..77ba515 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -14,6 +14,7 @@
 #include <sibs/SafeSerializer.hpp>
 #include <sibs/SafeDeserializer.hpp>
 #include <libpreview.h>
+#include <gd.h>
 
 #if OS_FAMILY == OS_FAMILY_POSIX
 #include <pwd.h>
@@ -119,8 +120,42 @@ namespace dchat
         }
         fileReplace(getDchatDir() / "binds", StringView((const char*)serializer.getBuffer().data(), serializer.getBuffer().size()));
     }
+
+    static bool downscaleImage(const boost::filesystem::path &filepath, void *data, const int size, const int newWidth, const int newHeight)
+    {
+        gdImagePtr imgPtr = gdImageCreateFromPngPtr(size, data);
+        if(!imgPtr)
+            return false;
+
+        int width = gdImageSX(imgPtr);
+        if(width < newWidth)
+        {
+            gdImageDestroy(imgPtr);
+            return false;
+        }
+
+        int height = gdImageSX(imgPtr);
+        if(height < newHeight)
+        {
+            gdImageDestroy(imgPtr);
+            return false;
+        }
+
+        gdImageSetInterpolationMethod(imgPtr, GD_BILINEAR_FIXED);
+        gdImagePtr newImgPtr = gdImageScale(imgPtr, newWidth, newHeight);
+        if(!newImgPtr)
+        {
+            gdImageDestroy(imgPtr);
+            return false;
+        }
+
+        bool success = (gdImageFile(newImgPtr, filepath.c_str()) == 0);
+        gdImageDestroy(imgPtr);
+        gdImageDestroy(newImgPtr);
+        return success;
+    }
     
-    static ContentByUrlResult loadImageFromFile(const boost::filesystem::path &filepath)
+    static ContentByUrlResult loadImageFromFile(const boost::filesystem::path &filepath, bool loadFromCache)
     {
         StringView fileContent;
         try
@@ -147,8 +182,16 @@ namespace dchat
                     }
                     else
                     {
+                        if(!loadFromCache)
+                        {
+                            if(!downscaleImage(filepath, (void*)fileContent.data, fileContent.size, 100, 100))
+                            {
+                                fprintf(stderr, "Failed to resize image: %s, using original file\n", filepath.c_str());
+                            }
+                        }
+
                         sf::Texture *texture = new sf::Texture();
-                        if(texture->loadFromMemory(fileContent.data, fileContent.size))
+                        if(texture->loadFromFile(filepath.c_str()))
                         {
                             delete[] fileContent.data;
                             fileContent.data = nullptr;
@@ -213,7 +256,7 @@ namespace dchat
                         }
                         else
                         {
-                            contentByUrlResult = loadImageFromFile(filepath);
+                            contentByUrlResult = loadImageFromFile(filepath, false);
                             contentByUrlResult.lastAccessed = chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now().time_since_epoch()).count();
                             if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED)
                             {
@@ -350,7 +393,7 @@ namespace dchat
         }
         
         // TODO: Do not load content in this thread. Return LOADING status and load it in another thread, because with a lot of images, chat can freeze
-        ContentByUrlResult contentByUrlResult = loadImageFromFile(filepath);
+        ContentByUrlResult contentByUrlResult = loadImageFromFile(filepath, true);
         if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED)
         {
             contentByUrlResult.lastAccessed = chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now().time_since_epoch()).count();
diff --git a/src/Channel.cpp b/src/Channel.cpp
index 75d805c..d13c476 100644
--- a/src/Channel.cpp
+++ b/src/Channel.cpp
@@ -17,12 +17,25 @@ namespace dchat
         messageBoard(this),
         localUser(_localUser ? _localUser : new OfflineUser("You"))
     {
+        bridgeServices.push_back(new DiscordService());
         addUserLocally(localUser);
             
         //addLocalMessage(u8"[emoji](https://discordemoji.com/assets/emoji/PepeDab.gif) deaf [emoji](https://discordemoji.com/assets/emoji/COGGERS.gif)", &systemUser, 0, odhtdb::Hash());
         //addLocalMessage(u8"[emoji](https://discordemoji.com/assets/emoji/PepeDab.gif)[emoji](https://discordemoji.com/assets/emoji/COGGERS.gif)", &systemUser, 0, odhtdb::Hash());
         //addLocalMessage(u8"pepedab https://discordemoji.com/assets/emoji/PepeDab.gif coggers https://discordemoji.com/assets/emoji/COGGERS.gif check out this url http://www.grandtournation.com/6808/start-date-of-the-grand-tour-season-3-confirmed-mark-your-calendars/ owo", &systemUser, 0, odhtdb::Hash());
-       // addLocalMessage(u8"ht clic", &systemUser, 0, odhtdb::Hash());
+        //addLocalMessage(u8"ht clic", &systemUser, 0, odhtdb::Hash());
+
+        auto binds = Chatbar::getBinds();
+        vector<string> suggestionsStr;
+        suggestionsStr.reserve(binds.size());
+        for(auto &bind : binds)
+        {
+            string suggestion = bind.first;
+            suggestion += " ";
+            suggestion += bind.second;
+            suggestionsStr.emplace_back(move(suggestion));
+        }
+        suggestions.show(suggestionsStr);
         
         if(database)
         {
@@ -67,6 +80,11 @@ namespace dchat
     
     Channel::~Channel()
     {
+        for(BridgeService *bridgeService : bridgeServices)
+        {
+            delete bridgeService;
+        }
+
         if(database)
         {
             database->cancelNodeListener(pingKey, pingListener);
@@ -78,6 +96,11 @@ namespace dchat
         {
             delete user;
         }
+
+        for(auto &discordUserIt : discordUserById)
+        {
+            delete discordUserIt.second;
+        }
     }
     
     User* Channel::getLocalUser()
@@ -100,7 +123,7 @@ namespace dchat
         return name;
     }
     
-    const vector<User*> Channel::getUsers() const
+    const vector<User*>& Channel::getUsers() const
     {
         return users;
     }
@@ -137,6 +160,31 @@ namespace dchat
         }
         messageBoard.addMessage(new Message(owner, msg, timestampSeconds), id);
     }
+
+    void Channel::addLocalDiscordMessage(const string &discordUserName, u64 discordUserId, const string &msg, User *owner, u64 timestampSeconds, const odhtdb::Hash &id)
+    {
+        assert(owner);
+        if(timestampSeconds == 0)
+        {
+            timestampSeconds = time(NULL);
+        }
+
+        OnlineDiscordUser *discordUser = nullptr;
+        auto discordUserIt = discordUserById.find(discordUserId);
+        if(discordUserIt == discordUserById.end())
+        {
+            discordUser = new OnlineDiscordUser(discordUserName, discordUserId, owner);
+            discordUserById[discordUserId] = discordUser;
+        }
+        else
+        {
+            // TODO: What if several users bridge same chat? the same discord user id could belong to different owners.
+            // Dchat channels should only allow one user to bridge data from one discord channel. Bridging data between multiple discord channels to
+            // one dchat channel should be allowed.
+            discordUser = discordUserIt->second;
+        }
+        messageBoard.addMessage(new Message(discordUser, msg, timestampSeconds), id);
+    }
     
     void Channel::addSystemMessage(const string &msg, bool plainText)
     {
@@ -159,6 +207,21 @@ namespace dchat
         else
             addLocalMessage(msg, localUser, 0, odhtdb::Hash());
     }
+
+    void Channel::addDiscordMessage(const string &discordUserName, u64 discordUserId, const string &msg)
+    {
+        assert(database && localUser->type == User::Type::ONLINE_LOCAL_USER);
+        auto onlineLocalUser = static_cast<OnlineLocalUser*>(localUser);
+        
+        sibs::SafeSerializer serializer;
+        serializer.add(ChannelDataType::ADD_DISCORD_MESSAGE);
+        serializer.add(discordUserId);
+        serializer.add((u8)discordUserName.size());
+        serializer.add((const u8*)discordUserName.data(), discordUserName.size());
+        serializer.add((const u8*)msg.data(), msg.size());
+        
+        database->addData(databaseNodeInfo, onlineLocalUser->keyPair, odhtdb::DataView(serializer.getBuffer().data(), serializer.getBuffer().size()));
+    }
     
     void Channel::deleteLocalMessage(const odhtdb::Hash &id, const odhtdb::Signature::PublicKey &requestedByUser)
     {
@@ -282,6 +345,12 @@ namespace dchat
             database->addData(databaseNodeInfo, onlineLocalUser->keyPair, odhtdb::DataView(serializer.getBuffer().data(), serializer.getBuffer().size()));
         }
     }
+
+    int Channel::getUserLowestPermissionLevel(OnlineUser *user) const
+    {
+        if(!database) return -1;
+        return database->getUserLowestPermissionLevel(*databaseNodeInfo.getRequestHash(), user->getPublicKey());
+    }
     
     void Channel::processEvent(const sf::Event &event, Cache &cache)
     {
@@ -293,6 +362,7 @@ namespace dchat
     {
         messageBoard.draw(window, cache);
         chatbar.draw(window, cache);
+        //suggestions.draw(window, cache);
     }
     
     void Channel::update()
@@ -300,7 +370,7 @@ namespace dchat
         if(database && localUser->type == User::Type::ONLINE_LOCAL_USER && pingTimer.getElapsedTime().asMilliseconds() > 5000)
         {
             pingTimer.restart();
-            sendPing(database->getSyncedTimestampUtc().seconds);
+            //sendPing(database->getSyncedTimestampUtc().seconds);
         }
     }
     
@@ -327,6 +397,16 @@ namespace dchat
             return 0;
         return database->getSyncedTimestampUtc().seconds;
     }
+
+    const vector<BridgeService*>& Channel::getBridgeServices() const
+    {
+        return bridgeServices;
+    }
+
+    DiscordService* Channel::getDiscordService()
+    {
+        return (DiscordService*)bridgeServices[0];
+    }
     
     void Channel::setCurrent(Channel *channel)
     {
diff --git a/src/ChannelTopPanel.cpp b/src/ChannelTopPanel.cpp
index bf122d2..9d4869e 100644
--- a/src/ChannelTopPanel.cpp
+++ b/src/ChannelTopPanel.cpp
@@ -45,7 +45,7 @@ namespace dchat
         const sf::Color lineCenterColor = lineSideColor + sf::Color(40, 0, 0);
         
         auto windowSize = window.getSize();
-        sf::RectangleShape rect(sf::Vector2f(floor(windowSize.x - ChannelSidePanel::getWidth() - UsersSidePanel::getWidth()), getHeight() - BOTTOM_LINE_HEIGHT));
+        sf::RectangleShape rect(sf::Vector2f(floor(windowSize.x - ChannelSidePanel::getWidth()), getHeight() - BOTTOM_LINE_HEIGHT));
         rect.setPosition(ChannelSidePanel::getWidth(), 0.0f);
         rect.setFillColor(ColorScheme::getBackgroundColor());
         window.draw(rect);
diff --git a/src/Chatbar.cpp b/src/Chatbar.cpp
index bb06308..a3e1d0f 100644
--- a/src/Chatbar.cpp
+++ b/src/Chatbar.cpp
@@ -217,6 +217,20 @@ namespace dchat
         window.draw(inputBackground);
         text.draw(window, cache);
     }
+
+    sf::Vector2f Chatbar::getInputPosition(sf::RenderWindow &window)
+    {
+        auto windowSize = window.getSize();
+        return { floor(ChannelSidePanel::getWidth() + PADDING_SIDE * Settings::getScaling()), floor(windowSize.y - getInputSize(window).y - PADDING_BOTTOM  * Settings::getScaling()) };
+    }
+
+    sf::Vector2f Chatbar::getInputSize(sf::RenderWindow &window)
+    {
+        auto windowSize = window.getSize();
+        const float fontSize = FONT_SIZE * Settings::getScaling();
+        const float fontHeight = ResourceCache::getFont("fonts/Nunito-Regular.ttf")->getLineSpacing(fontSize);
+        return { floor(windowSize.x - ChannelSidePanel::getWidth() - UsersSidePanel::getWidth() - PADDING_SIDE * Settings::getScaling() * 2.0f), floor(fontHeight * 1.7f + BOX_PADDING_Y * Settings::getScaling() * 2.0f) };
+    }
     
     float Chatbar::getHeight()
     {
diff --git a/src/MessageBoard.cpp b/src/MessageBoard.cpp
index aa92d46..2c89be3 100644
--- a/src/MessageBoard.cpp
+++ b/src/MessageBoard.cpp
@@ -36,7 +36,7 @@ namespace dchat
     const float TEXT_LINE_SPACING = 5.0f;
     const float USERNAME_TIMESTAMP_SIDE_PADDING = 15.0f;
     const float AVATAR_DIAMETER = 70.0f;
-    const float AVATAR_PADDING_SIDE = 30.0f;
+    const float AVATAR_PADDING_SIDE = 20.0f;
     const double SCROLL_MAX_SPEED = 20.0;
     
     // Merge messages from same user that are sent within one minute
@@ -52,7 +52,7 @@ namespace dchat
         visibleMessageEndIndex(-1)
     {
         scrollbar.backgroundColor = sf::Color(49, 52, 57);
-        scrollbar.scrollColor = sf::Color(42, 44, 49);
+        scrollbar.scrollColor = sf::Color(37, 39, 44);
     }
     
     MessageBoard::~MessageBoard()
@@ -81,16 +81,24 @@ namespace dchat
         dirty = true;
     }
     
-    void MessageBoard::addMessage(Message *message, const odhtdb::Hash &id)
+    bool MessageBoard::addMessage(Message *message, const odhtdb::Hash &id)
     {
         lock_guard<mutex> lock(messageProcessMutex);
+        bool emptyHash = id.isEmpty();
+        if(!emptyHash && messageIdMap.find(id) != messageIdMap.end())
+        {
+            delete message;
+            return false;
+        }
         auto positionToAddMessage = findPositionToInsertMessageByTimestamp(message);
         if(positionToAddMessage == messages.size())
             scrollToBottom = true;
         messages.insert(messages.begin() + positionToAddMessage, message);
         message->id = id;
-        messageIdMap[id] = message;
+        if(!emptyHash)
+            messageIdMap[id] = message;
         dirty = true;
+        return true;
     }
     
     void MessageBoard::deleteMessage(const odhtdb::Hash &id, const odhtdb::Signature::PublicKey &requestedByUser)
@@ -167,6 +175,8 @@ namespace dchat
             }
             
             bool visible = false;
+
+            float startX = floor(position.x + LINE_SIDE_PADDING * Settings::getScaling() - PADDING_SIDE * Settings::getScaling());
             
             if(!mergeTextWithPrev)
             {
@@ -174,9 +184,19 @@ namespace dchat
                 if(position.y + usernameTextHeight > 0.0f && position.y < backgroundPos.y + backgroundSize.y)
                 {
                     visible = true;
-                    sf::Text usernameText(sf::String::fromUtf8(message->user->getName().begin(), message->user->getName().end()), *usernameFont, usernameTextCharacterSize);
+                    string usernameStr;
+                    if(message->user->type == User::Type::ONLINE_DISCORD_USER)
+                    {
+                        usernameStr = "(Discord) ";
+                        usernameStr += message->user->getName();
+                    }
+                    else
+                    {
+                        usernameStr = message->user->getName();
+                    }
+                    sf::Text usernameText(sf::String::fromUtf8(usernameStr.begin(), usernameStr.end()), *usernameFont, usernameTextCharacterSize);
                     usernameText.setFillColor(sf::Color(15, 192, 252));
-                    usernameText.setPosition(sf::Vector2f(floor(position.x + (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling()), floor(position.y)));
+                    usernameText.setPosition(sf::Vector2f(floor(startX + (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling()), floor(position.y)));
                     window.draw(usernameText);
                     
                     if(message->timestampSeconds)
@@ -188,7 +208,7 @@ namespace dchat
                         
                         sf::Text timestamp(date, *timestampFont, timestampTextCharacterSize);
                         timestamp.setFillColor(ColorScheme::getTextRegularColor() * sf::Color(255, 255, 255, 50));
-                        timestamp.setPosition(sf::Vector2f(floor(position.x + (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling() + usernameText.getLocalBounds().width + USERNAME_TIMESTAMP_SIDE_PADDING * Settings::getScaling()), floor(position.y + 2.0f * Settings::getScaling() + usernameTextHeight * 0.5f - timestampTextHeight * 0.5f)));
+                        timestamp.setPosition(sf::Vector2f(floor(startX + (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling() + usernameText.getLocalBounds().width + USERNAME_TIMESTAMP_SIDE_PADDING * Settings::getScaling()), floor(position.y + 2.0f * Settings::getScaling() + usernameTextHeight * 0.5f - timestampTextHeight * 0.5f)));
                         window.draw(timestamp);
                     }
                     
@@ -203,14 +223,14 @@ namespace dchat
                             // TODO: Store this sprite somewhere, might not be efficient to create a new sprite object every frame
                             sf::Sprite sprite(*avatarResult.texture);
                             auto textureSize = avatarResult.texture->getSize();
-                            sprite.setPosition(sf::Vector2f(floor(position.x), floor(position.y)));
+                            sprite.setPosition(sf::Vector2f(startX, floor(position.y)));
                             sprite.setScale(sf::Vector2f(AVATAR_DIAMETER * Settings::getScaling() / (float)textureSize.x, AVATAR_DIAMETER * Settings::getScaling() / (float)textureSize.y));
                             window.draw(sprite, circleShader);
                         }
                         else if(avatarResult.cachedType == ContentByUrlResult::CachedType::GIF)
                         {
                             auto gifSize = avatarResult.gif->getSize();
-                            avatarResult.gif->setPosition(sf::Vector2f(floor(position.x), floor(position.y)));
+                            avatarResult.gif->setPosition(sf::Vector2f(startX, floor(position.y)));
                             avatarResult.gif->setScale(sf::Vector2f(AVATAR_DIAMETER * Settings::getScaling() / (float)gifSize.x, AVATAR_DIAMETER * Settings::getScaling() / (float)gifSize.y));
                             avatarResult.gif->setColor(sf::Color::White);
                             avatarResult.gif->draw(window, circleShader);
@@ -219,7 +239,7 @@ namespace dchat
                     else
                     {
                         sf::CircleShape avatarCircle(AVATAR_DIAMETER * 0.5f * Settings::getScaling(), 60 * Settings::getScaling());
-                        avatarCircle.setPosition(sf::Vector2f(floor(position.x), floor(position.y)));
+                        avatarCircle.setPosition(sf::Vector2f(startX, floor(position.y)));
                         avatarCircle.setFillColor(ColorScheme::getBackgroundColor() + sf::Color(30, 30, 30));
                         window.draw(avatarCircle);
                     }
@@ -230,8 +250,8 @@ namespace dchat
             
             // No need to perform culling here, that is done in @Text draw function
             message->text.setCharacterSize(18.0f * Settings::getScaling());
-            message->text.setMaxWidth(backgroundSize.x - (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling());
-            message->text.setPosition(sf::Vector2f(floor(position.x + (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling()), floor(position.y)));
+            message->text.setMaxWidth(lineRect.getSize().x - (AVATAR_DIAMETER + AVATAR_PADDING_SIDE * 2.0f) * Settings::getScaling());
+            message->text.setPosition(sf::Vector2f(floor(startX + (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling()), floor(position.y)));
             bool textDrawn = message->text.draw(window, cache);
             if(!visible)
                 visible = textDrawn;
@@ -243,7 +263,7 @@ namespace dchat
                 if(position.y + LINE_HEIGHT > 0.0f && position.y < backgroundPos.y + backgroundSize.y && i + 1 != numMessages)
                 {
                     visible = true;
-                    lineRect.setPosition(sf::Vector2f(position.x + LINE_SIDE_PADDING * Settings::getScaling() - PADDING_SIDE * Settings::getScaling(), floor(position.y)));
+                    lineRect.setPosition(sf::Vector2f(startX, floor(position.y)));
                     window.draw(lineRect);
                     //drawGradientLine(sf::Vector2f(position.x + LINE_SIDE_PADDING, floor(position.y)), sf::Vector2f(backgroundSizeWithoutPadding.x - LINE_SIDE_PADDING * 2.0f, LINE_HEIGHT), LINE_COLOR, window);
                 }
@@ -459,4 +479,9 @@ namespace dchat
             return messages.back();
         return nullptr;
     }
+
+    const std::vector<Message*>& MessageBoard::getMessages() const
+    {
+        return messages;
+    }
 }
diff --git a/src/ResourceCache.cpp b/src/ResourceCache.cpp
index 4664bf0..f6f8fad 100644
--- a/src/ResourceCache.cpp
+++ b/src/ResourceCache.cpp
@@ -1,5 +1,6 @@
 #include "../include/ResourceCache.hpp"
 #include <unordered_map>
+#include <gd.h>
 
 using namespace std;
 
@@ -33,6 +34,29 @@ namespace dchat
         auto it = textures.find(filepath);
         if(it != textures.end())
             return it->second;
+
+        gdImagePtr imgPtr = gdImageCreateFromFile(filepath.c_str());
+        if(!imgPtr)
+        {
+            string errMsg = "Failed to load texture with gd: ";
+            errMsg += filepath;
+            throw FailedToLoadResourceException(errMsg);
+        }
+
+        gdImageSetInterpolationMethod(imgPtr, GD_BILINEAR_FIXED);
+        gdImagePtr newImgPtr = gdImageScale(imgPtr, 100, 100);
+        if(!newImgPtr)
+        {
+            gdImageDestroy(imgPtr);
+            string errMsg = "Failed to scale image with gd: ";
+            errMsg += filepath;
+            throw FailedToLoadResourceException(errMsg);
+        }
+
+        gdImageFile(newImgPtr, filepath.c_str());
+
+        gdImageDestroy(imgPtr);
+        gdImageDestroy(newImgPtr);
         
         sf::Texture *texture = new sf::Texture();
         if(!texture->loadFromFile(filepath))
diff --git a/src/Rpc.cpp b/src/Rpc.cpp
new file mode 100644
index 0000000..2bf54fb
--- /dev/null
+++ b/src/Rpc.cpp
@@ -0,0 +1,39 @@
+#include "../include/Rpc.hpp"
+#include <string>
+#include <cassert>
+
+namespace dchat
+{
+    Rpc::Rpc(u16 port) : 
+        context(1), 
+        socket(context, ZMQ_PAIR)
+    {
+        std::string addr = "tcp://*:";
+        addr += std::to_string(port);
+        socket.bind(addr);
+    }
+
+    void Rpc::recv(RpcRecvCallbackFunc recvCallbackFunc)
+    {
+        assert(recvCallbackFunc);
+        zmq::message_t request;
+        if(socket.recv(&request, ZMQ_NOBLOCK))
+        {
+            recvCallbackFunc(&request);
+        }
+    }
+
+    bool Rpc::send(const void *data, const usize size)
+    {
+        if(size == 0) return false;
+        try
+        {
+            return socket.send(data, size, ZMQ_NOBLOCK) > 0;
+        }
+        catch(zmq::error_t &e)
+        {
+            fprintf(stderr, "Rpc::send failed, reason: %s\n", e.what());
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Suggestions.cpp b/src/Suggestions.cpp
new file mode 100644
index 0000000..113cd7e
--- /dev/null
+++ b/src/Suggestions.cpp
@@ -0,0 +1,54 @@
+#include "../include/Suggestions.hpp"
+#include "../include/Text.hpp"
+#include "../include/ResourceCache.hpp"
+#include "../include/Settings.hpp"
+#include "../include/ColorScheme.hpp"
+#include "../include/Chatbar.hpp"
+#include <cmath>
+
+namespace dchat
+{
+    static const char *FONT_PATH = "fonts/Nunito-Regular.ttf";
+    static float FONT_SCALING = 18.0f;
+
+    static sf::Vector2f floor(const sf::Vector2f &vec)
+    {
+        return { std::floor(vec.x), std::floor(vec.y) };
+    }
+
+    void Suggestions::show(const std::vector<std::string> &_texts)
+    {
+        for(const auto &text : _texts)
+        {
+            sf::String str = sf::String::fromUtf8(text.begin(), text.end());
+            texts.emplace_back(std::make_unique<Text>(str, ResourceCache::getFont(FONT_PATH), FONT_SCALING * Settings::getScaling(), 0.0f, false));
+        }
+    }
+
+    void Suggestions::draw(sf::RenderWindow &window, Cache &cache)
+    {
+        if(texts.empty()) return;
+
+        sf::Vector2f position = Chatbar::getInputPosition(window);
+        sf::Vector2f size = Chatbar::getInputSize(window);
+        size.y = FONT_SCALING * Settings::getScaling() * (1 + texts.size()) * 1.7f;
+        position.y -= size.y;
+
+        position = floor(position);
+        size = floor(size);
+
+        sf::RectangleShape rect(size);
+        rect.setPosition(position);
+        rect.setFillColor(ColorScheme::getPanelColor());
+        window.draw(rect);
+
+        for(const auto &text : texts)
+        {
+            text->setCharacterSize(FONT_SCALING * Settings::getScaling());
+            text->setMaxWidth(size.x);
+            text->setPosition(position.x, std::floor(position.y));
+            text->draw(window, cache);
+            position.y += text->getHeight();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Text.cpp b/src/Text.cpp
index c8ee731..39e339f 100644
--- a/src/Text.cpp
+++ b/src/Text.cpp
@@ -6,8 +6,8 @@
 #include "../include/ImagePreview.hpp"
 #include "../include/StringUtils.hpp"
 #include <SFML/Graphics/RectangleShape.hpp>
+#include <SFML/Window/Clipboard.hpp>
 #include <cmath>
-#include <process.hpp>
 
 namespace dchat
 {
@@ -341,6 +341,7 @@ namespace dchat
         sf::Uint32 prevCodePoint = 0;
         size_t lastSpacingWordWrapIndex = -1;
         float lastSpacingAccumulatedOffset = 0.0f;
+        bool lineHasEmoji = false;
         for(usize textElementIndex = 0; textElementIndex < textElements.size(); ++textElementIndex)
         {
             TextElement &textElement = textElements[textElementIndex];
@@ -377,11 +378,12 @@ namespace dchat
                 {
                     textElement.position.y = glyphPos.y;
                     // TODO: Find a better way to do this, @totalHeight is wrong because we add emojiSize and then vspace
-                    glyphPos.y += floor(emojiSize - vspace);
+                    glyphPos.y += floor(emojiSize - vspace) + lineSpacing;
                 }
                 else
                 {
                     textElement.position.y = glyphPos.y + vspace * 0.5f - emojiSize * 0.5f;
+                    lineHasEmoji = true;
                 }
                 glyphPos.x += emojiSize + EMOJI_PADDING + characterSpacing;
                 if(glyphPos.x > maxWidth)
@@ -443,16 +445,13 @@ namespace dchat
                         vertices[vertexStart + 2] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() };
                         vertices[vertexStart + 3] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() };
                         glyphPos.x = 0.0f;
-                        glyphPos.y += floor(vspace + lineSpacing);
-                        continue;
-                    }
-                    case '\v':
-                    {
-                        vertices[vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() };
-                        vertices[vertexStart + 1] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() };
-                        vertices[vertexStart + 2] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() };
-                        vertices[vertexStart + 3] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() };
-                        glyphPos.y += floor(vspace * TAB_WIDTH + lineSpacing);
+                        if(lineHasEmoji)
+                        {
+                            lineHasEmoji = false;
+                            glyphPos.y += floor(vspace * EMOJI_SCALE_WITH_TEXT + lineSpacing);
+                        }
+                        else
+                            glyphPos.y += floor(vspace + lineSpacing);
                         continue;
                     }
                 }
@@ -481,7 +480,13 @@ namespace dchat
                     else
                         glyphPos.x = 0.0f;
                     
-                    glyphPos.y += floor(vspace + lineSpacing);
+                    if(lineHasEmoji)
+                    {
+                        lineHasEmoji = false;
+                        glyphPos.y += floor(vspace * EMOJI_SCALE_WITH_TEXT + lineSpacing);
+                    }
+                    else
+                        glyphPos.y += floor(vspace + lineSpacing);
                 }
                 
                 sf::Vector2f vertexTopLeft(glyphPos.x + glyph.bounds.left, glyphPos.y + glyph.bounds.top);
@@ -513,7 +518,7 @@ namespace dchat
             
             if(textElement.type == TextElement::Type::URL)
             {
-                glyphPos.y += vspace + lineSpacing;
+                glyphPos.y += floor(vspace + lineSpacing);
                 textElement.position.y = glyphPos.y;
                 
                 glyphPos.x = 0.0f;
@@ -521,7 +526,12 @@ namespace dchat
             }
         }
         
-        boundingBox.height = glyphPos.y + vspace + lineSpacing;
+        boundingBox.height = glyphPos.y + lineSpacing;
+        if(lineHasEmoji)
+            boundingBox.height += vspace * EMOJI_SCALE_WITH_TEXT;
+        else
+            boundingBox.height += vspace;
+
         usize numVertices = vertices.getVertexCount();
         for(usize i = 0; i < numVertices; i += 4)
         {
@@ -708,18 +718,6 @@ namespace dchat
         return static_cast<int>(1.0f + position.y / (vspace + lineSpacing));
     }
     
-    static std::string getClipboard()
-    {
-        std::string result;
-        TinyProcessLib::Process process("xsel -o -b", "", [&result](const char *bytes, size_t n)
-        {
-            result.append(bytes, n);
-        });
-        if(process.get_exit_status() != 0)
-            fprintf(stderr, "Failed to get clipboard content\n");
-        return result;
-    }
-    
     void Text::onMouseClick(const sf::Event::MouseButtonEvent &event, Cache &cache)
     {
         if(event.button != sf::Mouse::Button::Left) return;
@@ -869,8 +867,7 @@ namespace dchat
             sf::String stringToAdd;
             if(event.text.unicode == 22) // ctrl+v
             {
-                auto clipboardString = getClipboard();
-                stringToAdd = sf::String::fromUtf8(clipboardString.begin(), clipboardString.end());
+                stringToAdd = sf::Clipboard::getString();
             }
             else if(event.text.unicode >= 32 || event.text.unicode == 9) // 9 == tab
                 stringToAdd = event.text.unicode;
diff --git a/src/User.cpp b/src/User.cpp
index aabfd0b..510f884 100644
--- a/src/User.cpp
+++ b/src/User.cpp
@@ -1,8 +1,10 @@
 #include "../include/User.hpp"
+#include <cassert>
 
 namespace dchat
 {
     const static std::string SYSTEM_USER_NAME = "System";
+    const i64 USER_TIMEOUT_SEC = 25;
     
     User::User(Type _type) : 
         type(_type)
@@ -17,12 +19,18 @@ namespace dchat
     {
         
     }
-    
+
     const std::string& OnlineUser::getName() const
     {
         return name;
     }
 
+    bool OnlineUser::isConnected(i64 timestampUtcSec) const
+    {
+        i64 pingTimeDiffSec = timestampUtcSec - (i64)pingTimestampSec;
+        return pingTimeDiffSec <= USER_TIMEOUT_SEC;
+    }
+
     OnlineRemoteUser::OnlineRemoteUser(const std::string &name, const odhtdb::Signature::PublicKey &_publicKey) : 
         OnlineUser(name, Type::ONLINE_REMOTE_USER),
         publicKey(_publicKey)
@@ -41,11 +49,24 @@ namespace dchat
     {
         
     }
-    
+
     const odhtdb::Signature::PublicKey& OnlineLocalUser::getPublicKey() const
     {
         return keyPair.getPublicKey();
     }
+
+    OnlineDiscordUser::OnlineDiscordUser(const std::string &discordUserName, u64 _discordUserId, User *_bridgeOwner) : 
+        OnlineUser(discordUserName, Type::ONLINE_DISCORD_USER),
+        discordUserId(_discordUserId),
+        bridgeOwner(_bridgeOwner)
+    {
+        assert(bridgeOwner);
+    }
+    
+    const odhtdb::Signature::PublicKey& OnlineDiscordUser::getPublicKey() const
+    {
+        return odhtdb::Signature::PublicKey::ZERO;
+    }
     
     OfflineUser::OfflineUser(const std::string &_name) : 
         User(Type::OFFLINE),
diff --git a/src/UsersSidePanel.cpp b/src/UsersSidePanel.cpp
index 687b976..b1f7843 100644
--- a/src/UsersSidePanel.cpp
+++ b/src/UsersSidePanel.cpp
@@ -11,6 +11,7 @@
 #include <SFML/Graphics/Text.hpp>
 #include <vector>
 #include <cmath>
+#include <sibs/Functional.hpp>
 
 using namespace std;
 
@@ -24,47 +25,50 @@ namespace dchat
     const float PADDING_BOTTOM = 20.0f;
     const i64 USER_TIMEOUT_SEC = 25;
     
-    static void renderUser(Cache &cache, User *user, sf::Shader *circleShader, sf::RenderWindow &window, sf::Vector2f &position, const sf::Font *font, const float textHeight, bool isUserOnline)
+    static void renderUser(Cache &cache, const User *user, sf::Shader *circleShader, sf::RenderWindow &window, sf::Vector2f &position, const sf::Font *font, const float textHeight, bool isUserOnline)
     {
-        // Max avatar size = 1mb
-        const ContentByUrlResult avatarResult = cache.getContentByUrl(user->avatarUrl, 1024 * 1024);
-        if(avatarResult.type == ContentByUrlResult::Type::CACHED)
+        if(position.y + AVATAR_DIAMETER > 0.0f && position.y < window.getSize().y)
         {
-            circleShader->setUniform("texture", sf::Shader::CurrentTexture);
-            
-            if(avatarResult.cachedType == ContentByUrlResult::CachedType::TEXTURE)
+            // Max avatar size = 1mb
+            const ContentByUrlResult avatarResult = cache.getContentByUrl(user->avatarUrl, 1024 * 1024);
+            if(avatarResult.type == ContentByUrlResult::Type::CACHED)
             {
-                // TODO: Store this sprite somewhere, might not be efficient to create a new sprite object every frame
-                sf::Sprite sprite(*avatarResult.texture);
-                auto textureSize = avatarResult.texture->getSize();
-                sprite.setPosition(sf::Vector2f(floor(position.x), floor(position.y)));
-                sprite.setScale(sf::Vector2f(AVATAR_DIAMETER * Settings::getScaling() / (float)textureSize.x, AVATAR_DIAMETER * Settings::getScaling() / (float)textureSize.y));
-                sprite.setColor(isUserOnline ? sf::Color::White : sf::Color(255, 255, 255, 100));
-                window.draw(sprite, circleShader);
+                circleShader->setUniform("texture", sf::Shader::CurrentTexture);
+                
+                if(avatarResult.cachedType == ContentByUrlResult::CachedType::TEXTURE)
+                {
+                    // TODO: Store this sprite somewhere, might not be efficient to create a new sprite object every frame
+                    sf::Sprite sprite(*avatarResult.texture);
+                    auto textureSize = avatarResult.texture->getSize();
+                    sprite.setPosition(sf::Vector2f(floor(position.x), floor(position.y)));
+                    sprite.setScale(sf::Vector2f(AVATAR_DIAMETER * Settings::getScaling() / (float)textureSize.x, AVATAR_DIAMETER * Settings::getScaling() / (float)textureSize.y));
+                    sprite.setColor(isUserOnline ? sf::Color::White : sf::Color(255, 255, 255, 100));
+                    window.draw(sprite, circleShader);
+                }
+                else if(avatarResult.cachedType == ContentByUrlResult::CachedType::GIF)
+                {
+                    auto gifSize = avatarResult.gif->getSize();
+                    avatarResult.gif->setPosition(sf::Vector2f(floor(position.x), floor(position.y)));
+                    avatarResult.gif->setScale(sf::Vector2f(AVATAR_DIAMETER * Settings::getScaling() / (float)gifSize.x, AVATAR_DIAMETER * Settings::getScaling() / (float)gifSize.y));
+                    avatarResult.gif->setColor(isUserOnline ? sf::Color::White : sf::Color(255, 255, 255, 100));
+                    avatarResult.gif->draw(window, circleShader);
+                }
             }
-            else if(avatarResult.cachedType == ContentByUrlResult::CachedType::GIF)
+            else
             {
-                auto gifSize = avatarResult.gif->getSize();
-                avatarResult.gif->setPosition(sf::Vector2f(floor(position.x), floor(position.y)));
-                avatarResult.gif->setScale(sf::Vector2f(AVATAR_DIAMETER * Settings::getScaling() / (float)gifSize.x, AVATAR_DIAMETER * Settings::getScaling() / (float)gifSize.y));
-                avatarResult.gif->setColor(isUserOnline ? sf::Color::White : sf::Color(255, 255, 255, 100));
-                avatarResult.gif->draw(window, circleShader);
+                sf::CircleShape avatarCircle(AVATAR_DIAMETER * 0.5f * Settings::getScaling(), 60 * Settings::getScaling());
+                avatarCircle.setPosition(sf::Vector2f(floor(position.x), floor(position.y)));
+                avatarCircle.setFillColor(isUserOnline ? ColorScheme::getBackgroundColor() + sf::Color(30, 30, 30) : ColorScheme::getBackgroundColor() * sf::Color(255, 255, 255, 100));
+                window.draw(avatarCircle);
             }
+            
+            // TODO: Remove this shit
+            sf::String str = sf::String::fromUtf8(user->getName().begin(), user->getName().end());
+            sf::Text text(str, *font, FONT_SIZE * Settings::getScaling());
+            text.setPosition(floor(position.x + (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling()), floor(position.y + AVATAR_DIAMETER * 0.5f - textHeight * 0.5f));
+            text.setFillColor(isUserOnline ? sf::Color(15, 192, 252) : sf::Color(15, 192, 252, 100));
+            window.draw(text);
         }
-        else
-        {
-            sf::CircleShape avatarCircle(AVATAR_DIAMETER * 0.5f * Settings::getScaling(), 60 * Settings::getScaling());
-            avatarCircle.setPosition(sf::Vector2f(floor(position.x), floor(position.y)));
-            avatarCircle.setFillColor(isUserOnline ? ColorScheme::getBackgroundColor() + sf::Color(30, 30, 30) : ColorScheme::getBackgroundColor() * sf::Color(255, 255, 255, 100));
-            window.draw(avatarCircle);
-        }
-        
-        // TODO: Remove this shit
-        sf::String str = sf::String::fromUtf8(user->getName().begin(), user->getName().end());
-        sf::Text text(str, *font, FONT_SIZE * Settings::getScaling());
-        text.setPosition(floor(position.x + (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling()), floor(position.y + AVATAR_DIAMETER * 0.5f - textHeight * 0.5f));
-        text.setFillColor(isUserOnline ? sf::Color(15, 192, 252) : sf::Color(15, 192, 252, 100));
-        window.draw(text);
         position.y += ((AVATAR_DIAMETER + PADDING_BOTTOM) * Settings::getScaling());
     }
     
@@ -86,24 +90,22 @@ namespace dchat
         const float textHeight = font->getLineSpacing(FONT_SIZE * Settings::getScaling());
         
         i64 timestampSec = currentChannel->getSyncedTimestampUtcInSec();
+        auto &channelUsers = currentChannel->getUsers();
+        auto currentChannelUsers = sibs::makeFunction(channelUsers.data(), channelUsers.data() + channelUsers.size());
+        for(BridgeService *bridgeService : currentChannel->getBridgeServices())
+        {
+            auto &bridgeServiceUsers = bridgeService->getUsers();
+            currentChannelUsers.merge(sibs::makeFunction(bridgeServiceUsers.data(), bridgeServiceUsers.data() + bridgeServiceUsers.size()));
+        }
         
         u32 numOnlineUsers = 0;
         u32 numOfflineUsers = 0;
-        for(User *user : currentChannel->getUsers())
+        for(const User *user : currentChannelUsers)
         {
-            bool hasUserTimedOut = false;
-            if(user->isOnlineUser())
-            {
-                auto onlineUser = static_cast<OnlineUser*>(user);
-                i64 pingTimeDiffSec = timestampSec - (i64)onlineUser->pingTimestampSec;
-                if(pingTimeDiffSec > USER_TIMEOUT_SEC)
-                    hasUserTimedOut = true;
-            }
-            
-            if(hasUserTimedOut)
-                ++numOfflineUsers;
-            else
+            if(user->isConnected(timestampSec))
                 ++numOnlineUsers;
+            else
+                ++numOfflineUsers;
         }
         
         // TODO: Remove this shit
@@ -118,19 +120,10 @@ namespace dchat
         
         sf::Shader *circleShader = ResourceCache::getShader("shaders/circleMask.glsl", sf::Shader::Fragment);
         
-        for(User *user : currentChannel->getUsers())
+        for(const User *user : currentChannelUsers)
         {
-            bool isUserOnline = true;
-            if(user->isOnlineUser())
-            {
-                auto onlineUser = static_cast<OnlineUser*>(user);
-                i64 pingTimeDiffSec = timestampSec - (i64)onlineUser->pingTimestampSec;
-                if(pingTimeDiffSec > USER_TIMEOUT_SEC)
-                    isUserOnline = false;
-            }
-            
-            if(isUserOnline)
-                renderUser(cache, user, circleShader, window, position, font, textHeight, isUserOnline);
+            if(user->isConnected(timestampSec))
+                renderUser(cache, user, circleShader, window, position, font, textHeight, true);
         }
         
         if(numOfflineUsers == 0) return;
@@ -146,19 +139,10 @@ namespace dchat
         position.y += floor(font->getLineSpacing(text.getCharacterSize()));
         position.y += PADDING_BOTTOM * Settings::getScaling() * 0.5f;
         
-        for(User *user : currentChannel->getUsers())
+        for(const User *user : currentChannelUsers)
         {
-            bool isUserOnline = true;
-            if(user->isOnlineUser()) 
-            {
-                auto onlineUser = static_cast<OnlineUser*>(user);
-                i64 pingTimeDiffSec = timestampSec - (i64)onlineUser->pingTimestampSec;
-                if(pingTimeDiffSec > USER_TIMEOUT_SEC)
-                    isUserOnline = false;
-            }
-            
-            if(!isUserOnline)
-                renderUser(cache, user, circleShader, window, position, font, textHeight, isUserOnline);
+            if(!user->isConnected(timestampSec))
+                renderUser(cache, user, circleShader, window, position, font, textHeight, false);
         }
     }
     
diff --git a/src/main.cpp b/src/main.cpp
index 0836278..efb354f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -11,6 +11,9 @@
 #include "../include/GlobalContextMenu.hpp"
 #include "../include/StringUtils.hpp"
 #include "../include/ImagePreview.hpp"
+#include "../include/Rpc.hpp"
+#include <msgpack.hpp>
+#include <sstream>
 #include <string>
 #include <SFML/Graphics.hpp>
 #include <cstring>
@@ -94,6 +97,13 @@ static void channelChangeChannelName(Channel *channel, const StringView data, co
         fprintf(stderr, "Channel change name: user with public key %s not found in channel %s\n", userPublicKey.toString().c_str(), channel->getName().c_str());
         return;
     }
+
+    int userPermissionLevel = channel->getUserLowestPermissionLevel(user);
+    if(userPermissionLevel != odhtdb::PERMISSION_LEVEL_ADMIN)
+    {
+        fprintf(stderr, "Channel change name: attempted by user %s who is not an admin\n", user->getName().c_str());
+        return;
+    }
     
     sibs::SafeDeserializer deserializer((const u8*)data.data, data.size);
     u16 channelNameLength = deserializer.extract<u16>();
@@ -107,6 +117,40 @@ static void channelChangeChannelName(Channel *channel, const StringView data, co
     // We dont care if there is more data to read (malicious packet), we already got all the data we need
 }
 
+static void channelAddDiscordMessage(Channel *channel, const StringView data, const odhtdb::Signature::PublicKey &userPublicKey, u64 timestamp, const odhtdb::Hash &requestHash)
+{
+    auto bridgeOwner = channel->getUserByPublicKey(userPublicKey);
+    if(!bridgeOwner)
+    {
+        fprintf(stderr, "Channel add discord message: user with public key %s not found in channel %s\n", userPublicKey.toString().c_str(), channel->getName().c_str());
+        return;
+    }
+
+    int userPermissionLevel = channel->getUserLowestPermissionLevel(bridgeOwner);
+    if(userPermissionLevel != odhtdb::PERMISSION_LEVEL_ADMIN)
+    {
+        fprintf(stderr, "Channel add discord message: attempted by user %s who is not an admin\n", bridgeOwner->getName().c_str());
+        return;
+    }
+
+    sibs::SafeDeserializer deserializer((const u8*)data.data, data.size);
+    u64 discordUserId = deserializer.extract<u64>();
+    u8 discordNameLength = deserializer.extract<u8>();
+    if(discordNameLength == 0) return;
+
+    string discordUserName;
+    discordUserName.resize(discordNameLength);
+    deserializer.extract((u8*)&discordUserName[0], discordNameLength);
+
+    usize msgSize = deserializer.getSize();
+    if(msgSize == 0) return;
+    string msg(deserializer.getBuffer(), deserializer.getBuffer() + deserializer.getSize());
+
+    auto timestampSeconds = ntp::NtpTimestamp::fromCombined(timestamp).seconds;
+    channel->addLocalDiscordMessage(discordUserName, discordUserId, msg, bridgeOwner, timestampSeconds, requestHash);
+    // We dont care if there is more data to read (malicious packet), we already got all the data we need
+}
+
 static void channelAddStoredMessage(Channel *channel, const odhtdb::Hash &requestHash, const odhtdb::Signature::PublicKey &creatorPublicKey, const StringView decryptedObject, u64 timestamp, bool loadedFromCache)
 {
     User *user = channel->getUserByPublicKey(creatorPublicKey);
@@ -187,6 +231,18 @@ static void channelAddStoredMessage(Channel *channel, const odhtdb::Hash &reques
                 }
                 break;
             }
+            case ChannelDataType::ADD_DISCORD_MESSAGE:
+            {
+                try
+                {
+                    channelAddDiscordMessage(channel, decryptedData, creatorPublicKey, timestamp, requestHash);
+                }
+                catch(sibs::DeserializeException &e)
+                {
+                    fprintf(stderr, "Failed to deserialize channel add discord message\n");
+                }
+                break;
+            }
             default:
                 fprintf(stderr, "Got unexpected channel data type: %u\n", channelDataType);
                 break;
@@ -207,7 +263,7 @@ int main(int argc, char **argv)
         printf("Resource path set to: %s\n", resourcesPath.string().c_str());
     }
     else
-        printf("Resource directory not defined, using currently directory");
+        printf("Resource directory not defined, using currently directory\n");
     
     const sf::Int64 FRAMERATE_FOCUSED = 144;
     const sf::Int64 FRAMERATE_NOT_FOCUSED = 10;
@@ -239,6 +295,9 @@ int main(int argc, char **argv)
     
     odhtdb::Database *database = nullptr;
     odhtdb::DatabaseCallbackFuncs callbackFuncs;
+
+    using LocalUserMessageCallback = function<void(const odhtdb::DatabaseAddNodeRequest &request)>;
+    LocalUserMessageCallback onMessageByLocalUser = nullptr;
     
     callbackFuncs.createNodeCallbackFunc = [&waitingToJoinChannels, &database, &channels, &channelMessageMutex, &waitingToJoin, &localNodeUsers, &lastFocusedTimer](const odhtdb::DatabaseCreateNodeRequest &request)
     {
@@ -280,7 +339,7 @@ int main(int argc, char **argv)
         }
     };
     
-    callbackFuncs.addNodeCallbackFunc = [&channels, &channelMessageMutex, &lastFocusedTimer](const odhtdb::DatabaseAddNodeRequest &request)
+    callbackFuncs.addNodeCallbackFunc = [&channels, &channelMessageMutex, &lastFocusedTimer, &onMessageByLocalUser](const odhtdb::DatabaseAddNodeRequest &request)
     {
         lock_guard<recursive_mutex> lock(channelMessageMutex);
         //printf("Add node callback func %s\n", request.requestHash->toString().c_str());
@@ -291,6 +350,12 @@ int main(int argc, char **argv)
                 channelAddStoredMessage(channel, *request.requestHash, *request.creatorPublicKey, StringView((const char*)request.decryptedData.data, request.decryptedData.size), request.timestamp, request.loadedFromCache);
                 if(channel == Channel::getCurrent())
                     lastFocusedTimer.restart();
+
+                if(!request.loadedFromCache && *request.creatorPublicKey == static_cast<OnlineLocalUser*>(channel->getLocalUser())->getPublicKey())
+                {
+                    if(onMessageByLocalUser)
+                        onMessageByLocalUser(request);
+                }
                 return;
             }
         }
@@ -808,7 +873,7 @@ int main(int argc, char **argv)
     });
     
     // Change name of the current channel
-    Command::add("channelname", [&loggedIn, &offlineChannel, addSystemMessage](const vector<string> &args)
+    Command::add("channelname", [&offlineChannel, addSystemMessage](const vector<string> &args)
     {
         if(args.size() != 1)
         {
@@ -818,16 +883,17 @@ int main(int argc, char **argv)
             addSystemMessage(errMsg);
             return;
         }
-        
-        if(!loggedIn)
+
+        Channel *currentChannel = Channel::getCurrent();
+        if(currentChannel == &offlineChannel)
         {
-            addSystemMessage("You need to be logged in to change channel name");
+            addSystemMessage("You need to be in a channel to change channel name");
             return;
         }
         
-        if(Channel::getCurrent() == &offlineChannel)
+        if(!currentChannel->getLocalUser()->isOnlineUser())
         {
-            addSystemMessage("You need to be in a channel to change channel name");
+            addSystemMessage("You need to be logged in to change channel name");
             return;
         }
         
@@ -836,8 +902,15 @@ int main(int argc, char **argv)
             addSystemMessage("Channel name has to be between 1 and 32 bytes long");
             return;
         }
+
+        int localUserPermissionLevel = currentChannel->getUserLowestPermissionLevel(static_cast<OnlineLocalUser*>(currentChannel->getLocalUser()));
+        if(localUserPermissionLevel != odhtdb::PERMISSION_LEVEL_ADMIN)
+        {
+            addSystemMessage("You need to be admin to change channel name");
+            return;
+        }
         
-        Channel::getCurrent()->setName(args[0]);
+        currentChannel->setName(args[0]);
         string msg = "Channel name has been changed to ";
         msg += args[0];
         addSystemMessage(msg);
@@ -855,10 +928,268 @@ int main(int argc, char **argv)
         commandsMsg += commandIt.first;
     }
     addSystemMessage(commandsMsg);
+
+    odhtdb::MapHash<u64> myDiscordIdsByChannel;
+
+    bool running = true;
+    thread rpcThread([&running, &onMessageByLocalUser, &channels, &lastFocusedTimer, &myDiscordIdsByChannel]()
+    {
+        Rpc rpc(5555);
+        mutex messageMutex;
+        vector<msgpack::sbuffer> messagesToSend;
+        onMessageByLocalUser = [&messagesToSend, &messageMutex](const odhtdb::DatabaseAddNodeRequest &request)
+        {
+            lock_guard<mutex> lock(messageMutex);
+            auto channelDataType = (ChannelDataType)static_cast<const char*>(request.decryptedData.data)[0];
+            usize size = request.decryptedData.size - 1;
+            const char *data = (const char*)request.decryptedData.data + 1;
+            const char *action = nullptr;
+            if(channelDataType == ChannelDataType::ADD_MESSAGE)
+                action = "addMessage";
+
+            if(!action) return;
+            vector<string> msg = { action, string(data, data + size), request.nodeHash->toString() };
+            msgpack::sbuffer buffer;
+            msgpack::pack(buffer, msg);
+            messagesToSend.emplace_back(move(buffer));
+        };
+
+        while(running)
+        {
+            rpc.recv([&channels, &lastFocusedTimer, &myDiscordIdsByChannel](zmq::message_t *message)
+            {
+                try
+                {
+                    msgpack::object_handle oh = msgpack::unpack((const char*)message->data(), message->size());
+                    auto deserialized = oh.get();
+                    vector<string> msg;
+                    deserialized.convert(msg);
+                    if(msg.size() < 2)
+                    {
+                        fprintf(stderr, "Rpc receive, data length expected to be at least 2, was %u\n", msg.size());
+                        return;
+                    }
+                    auto &action = msg[0];
+
+                    string dchatChannelIdRaw = odhtdb::hex2bin(msg[1].c_str(), msg[1].size());
+                    odhtdb::Hash dchatChannelId;
+                    memcpy(dchatChannelId.getData(), dchatChannelIdRaw.data(), dchatChannelIdRaw.size());
+                    Channel *bridgedChannel = nullptr;
+                    for(Channel *channel : channels)
+                    {
+                        if(*channel->getNodeInfo().getRequestHash() == dchatChannelId)
+                        {
+                            bridgedChannel = channel;
+                            break;
+                        }
+                    }
+
+                    if(!bridgedChannel)
+                    {
+                        fprintf(stderr, "Rcp addMessage, invalid dchat channel %s\n", msg[1].c_str());
+                        return;
+                    }
+
+                    if(bridgedChannel == Channel::getCurrent())
+                        lastFocusedTimer.restart();
+
+                    fprintf(stderr, "Received rpc, action: %s\n", action.c_str());
+                    if(action == "addMessage")
+                    {
+                        if((msg.size() - 2) % 4 != 0)
+                        {
+                            fprintf(stderr, "Rpc addMessage, request was malformed\n");
+                            return;
+                        }
+
+                        for(size_t i = 2; i < msg.size(); i += 4)
+                        {
+                            auto &content = msg[i];
+                            auto &discordUserId = msg[i + 1];
+                            u64 discordUserIdNumber = 0;
+                            auto &discordUserName = msg[i + 2];
+                            auto &messageTimestampMillisec = msg[i + 3];
+                            u64 messageTimestampSecondsNumber = 0;
+
+                            try
+                            {
+                                discordUserIdNumber = stoull(discordUserId);
+                            }
+                            catch(...)
+                            {
+                                fprintf(stderr, "Rpc receive, failed to convert discord id to uint64_t: %s\n", discordUserId.c_str());
+                                return;
+                            }
+
+                            try
+                            {
+                                messageTimestampSecondsNumber = stoull(messageTimestampMillisec) / 1000;
+                            }
+                            catch(...)
+                            {
+                                fprintf(stderr, "Rpc receive, failed to convert discord message timestamp to uint64_t: %s\n", messageTimestampMillisec.c_str());
+                                return;
+                            }
+
+                            auto myDiscordIdIt = myDiscordIdsByChannel.find(dchatChannelId);
+                            if(myDiscordIdIt != myDiscordIdsByChannel.end() && myDiscordIdIt->second == discordUserIdNumber)
+                            {
+                                auto &channelMessages = bridgedChannel->getMessageBoard().getMessages();
+                                Message *myLatestMessage = nullptr;
+                                for(auto it = channelMessages.rbegin(), end = channelMessages.rend(); it != end; ++it)
+                                {
+                                    if((*it)->user == bridgedChannel->getLocalUser())
+                                    {
+                                        myLatestMessage = *it;
+                                        break;
+                                    }
+                                }
+
+                                if(myLatestMessage && (i64)messageTimestampSecondsNumber - (i64)myLatestMessage->timestampSeconds <= 3)
+                                {
+                                    return;
+                                    /*
+                                    auto myMessageUtf8 = myLatestMessage->text.getString().toUtf8();
+                                    odhtdb::Hash myMessageHash(myMessageUtf8.data(), myMessageUtf8.size());
+                                    odhtdb::Hash myDiscordMessageHash(content.data(), content.size());
+                                    if(myMessageHash == myDiscordMessageHash)
+                                        return;
+                                    */
+                                }
+                            }
+
+                            bridgedChannel->addLocalDiscordMessage(discordUserName, discordUserIdNumber, content, bridgedChannel->getLocalUser(), messageTimestampSecondsNumber, odhtdb::Hash(messageTimestampMillisec.c_str(), messageTimestampMillisec.size()));
+                        }
+                    }
+                    else if(action == "addUser")
+                    {
+                        if((msg.size() - 2) % 4 != 0)
+                        {
+                            fprintf(stderr, "Rpc addUser, request was malformed\n");
+                            return;
+                        }
+
+                        for(size_t i = 2; i < msg.size(); i += 4)
+                        {
+                            auto &discordUsername = msg[i];
+                            auto &discordUserId = msg[i + 1];
+                            auto &userStatus = msg[i + 2];
+                            auto &avatarURL = msg[i + 3];
+
+                            try
+                            {
+                                u64 discordUserIdNumber = stoull(discordUserId);
+                                bool online = (userStatus != "offline");
+                                printf("Rpc, adding user %s with status %s\n", discordUsername.c_str(), userStatus.c_str());
+                                DiscordServiceUser *discordUser = bridgedChannel->getDiscordService()->getUserById(discordUserIdNumber);
+                                if(discordUser)
+                                {
+                                    discordUser->connected = online;
+                                }
+                                else
+                                {
+                                    discordUser = new DiscordServiceUser(discordUsername, discordUserIdNumber, online);
+                                    bridgedChannel->getDiscordService()->addUser(discordUser);
+                                }
+                                discordUser->avatarUrl = avatarURL;
+                            }
+                            catch(...)
+                            {
+                                fprintf(stderr, "Warning: Rpc receive, failed to convert discord id to uint64_t: %s\n", discordUserId.c_str());
+                                // Ignore for now.. should we really handle this error other than showing warning?
+                            }
+                        }
+                    }
+                    else if(action == "removeUser")
+                    {
+                        for(size_t i = 2; i < msg.size(); ++i)
+                        {
+                            auto &discordUserId = msg[i];
+                            try
+                            {
+                                u64 discordUserIdNumber = stoull(discordUserId);
+                                bridgedChannel->getDiscordService()->removeUser(discordUserIdNumber);
+                            }
+                            catch(...)
+                            {
+                                fprintf(stderr, "Warning: Rpc receive, failed to convert discord id to uint64_t: %s\n", discordUserId.c_str());
+                                // Ignore for now.. should we really handle this error other than showing warning?
+                            }
+                        }
+                    }
+                    else if(action == "statusChange")
+                    {
+                        if((msg.size() - 2) != 2)
+                        {
+                            fprintf(stderr, "Rpc statusChange, request was malformed\n");
+                            return;
+                        }
+
+                        auto &discordUserId = msg[2];
+                        auto &userStatus = msg[3];
+
+                        try
+                        {
+                            u64 discordUserIdNumber = stoull(discordUserId);
+                            DiscordServiceUser *discordUser = bridgedChannel->getDiscordService()->getUserById(discordUserIdNumber);
+                            if(discordUser)
+                            {
+                                discordUser->connected = (userStatus != "offline");
+                                printf("Rcp statusChange, changed user %s (%s) status to %s\n", discordUserId.c_str(), discordUser->getName().c_str(), userStatus.c_str());
+                            }
+                        }
+                        catch(...)
+                        {
+                            fprintf(stderr, "Warning: Rpc receive, failed to convert discord id to uint64_t: %s\n", discordUserId.c_str());
+                            // Ignore for now.. should we really handle this error other than showing warning?
+                        }
+                    }
+                    else if(action == "addMe")
+                    {
+                        if((msg.size() - 2) != 1)
+                        {
+                            fprintf(stderr, "Rpc addMe, request was malformed\n");
+                            return;
+                        }
+
+                        auto &myDiscordId = msg[2];
+                        try
+                        {
+                            u64 myDiscordIdNumber = stoull(myDiscordId);
+                            myDiscordIdsByChannel[dchatChannelId] = myDiscordIdNumber;
+                        }
+                        catch(...)
+                        {
+                            fprintf(stderr, "Warning: Rpc receive, failed to convert discord id to uint64_t: %s\n", myDiscordId.c_str());
+                            // Ignore for now.. should we really handle this error other than showing warning?
+                        }
+                    }
+                    else
+                    {
+                        fprintf(stderr, "Rcp received unknown action %s\n", action.c_str());
+                    }
+                }
+                catch(msgpack::type_error &e)
+                {
+                    fprintf(stderr, "Failed to deserialize received rpc, error: %s\n", e.what());
+                }
+            });
+            {
+                lock_guard<mutex> lock(messageMutex);
+                for(auto &messageToSend : messagesToSend)
+                {
+                    fprintf(stderr, "Rpc, sending message\n");
+                    rpc.send(messageToSend.data(), messageToSend.size());
+                }
+                messagesToSend.clear();
+            }
+            this_thread::sleep_for(chrono::milliseconds(50));
+        }
+    });
     
     sf::Clock frameTimer;
     
-    while (window.isOpen())
+    while (running)
     {
         frameTimer.restart();
         Channel *currentChannel =  Channel::getCurrent();
@@ -867,11 +1198,14 @@ int main(int argc, char **argv)
         while (window.pollEvent(event))
         {
             if (event.type == sf::Event::Closed)
+            {
                 window.close();
+                running = false;
+            }
             else if(event.type == sf::Event::Resized)
             {
                 sf::FloatRect viewRect(0.0f, 0.0f, event.size.width, event.size.height);
-                /* // TODO: Use xlib to set window minimum size instead
+                /* // TODO: Use xlib/xcb to set window minimum size instead
                 const int minWidth = 800;
                 if(event.size.width < minWidth)
                 {
@@ -916,7 +1250,7 @@ int main(int argc, char **argv)
             channel->update();
         }
 
-        if(lastFocusedTimer.getElapsedTime().asMilliseconds() > 5000)
+        if((!windowFocused || !focused) && lastFocusedTimer.getElapsedTime().asMilliseconds() > 5000)
         {
             this_thread::sleep_for(chrono::milliseconds(250));
             continue;
@@ -948,6 +1282,9 @@ int main(int argc, char **argv)
         //video.draw(window);
         window.display();
     }
+
+    onMessageByLocalUser = nullptr;
+    rpcThread.join();
     
     for(Channel *channel : channels)
     {
-- 
cgit v1.2.3-70-g09d2