#include "../include/ChatWindow.hpp" #include "../include/ChatMessage.hpp" #include "../include/InputDialog.hpp" #include "../include/Window.hpp" #include #include #include #include #include namespace dchat { // Merge all messages that are written by the same user without interrupt within a timeframe const int MERGE_MESSAGE_TIMESTAMP_DIFF_SEC = 60; ChatWindow::ChatWindow(Window *_window) : addRoomButton("images/add_button_small.png", " Add room"), roomCount(0), currentRoomData(nullptr), window(_window) { assert(window); leftPanelUsersStack.set_homogeneous(false); messageAreaStack.set_homogeneous(false); setupTopBar(); Gtk::Paned *sidePanels = Gtk::manage(new Gtk::Paned()); sidePanels->set_name("side-panels"); sidePanels->set_border_width(0); attach(*sidePanels, 0, 1, 1, 2); setupLeftPanel(sidePanels); Gtk::Grid *rightPanel = Gtk::manage(new Gtk::Grid()); rightPanel->set_vexpand(true); rightPanel->set_hexpand(true); sidePanels->add2(*rightPanel); setupMessageArea(rightPanel); setupChatInput(rightPanel); set_vexpand(true); set_hexpand(true); } ChatWindow::~ChatWindow() { for(auto &it : roomDataById) { delete it.second->leftPanelUsersLayout; delete it.second->messageAreaLayout; delete it.second->button; delete it.second; } } void ChatWindow::setupTopBar() { topbar.set_name("top-bar"); topbar.set_hexpand(true); attach(topbar, 0, 0, 2, 1); Gtk::Grid *topbarLeft = Gtk::manage(new Gtk::Grid()); topbarLeft->set_name("top-bar-left"); topbarLeft->set_size_request(180); topbarLeft->set_valign(Gtk::ALIGN_CENTER); topbarLeft->set_halign(Gtk::ALIGN_CENTER); topbar.attach(*topbarLeft, 0, 0, 1, 1); topbarSearchBar.set_name("top-bar-search"); topbarSearchBar.set_placeholder_text("Search..."); topbarSearchBar.set_size_request(180); topbarLeft->attach(topbarSearchBar, 0, 0, 1, 1); Gtk::Grid *topbarRight = Gtk::manage(new Gtk::Grid()); topbarRight->set_name("top-bar-right"); topbarRight->set_hexpand(true); topbarRight->set_valign(Gtk::ALIGN_CENTER); topbar.attach_next_to(*topbarRight, *topbarLeft, Gtk::POS_RIGHT, 1, 1); currentChannelTitle.set_name("current-room-title"); currentChannelTitle.set_hexpand(true); currentChannelTitle.set_alignment(Gtk::ALIGN_START, Gtk::ALIGN_CENTER); topbarRight->attach(currentChannelTitle, 0, 0, 1, 1); ImageButton *channelSettings = Gtk::manage(new ImageButton("images/settings-icon.png", nullptr)); topbarRight->attach_next_to(*channelSettings, currentChannelTitle, Gtk::POS_RIGHT, 1, 1); } void ChatWindow::setupLeftPanel(Gtk::Paned *sidePanels) { Gtk::Grid *leftPanelLayout = Gtk::manage(new Gtk::Grid()); leftPanelLayout->set_vexpand(true); leftPanelLayout->set_size_request(200); leftPanelLayout->set_name("left-panel"); sidePanels->add1(*leftPanelLayout); Gtk::Paned *leftPanel = Gtk::manage(new Gtk::Paned(Gtk::ORIENTATION_VERTICAL)); leftPanel->set_vexpand(true); leftPanelLayout->attach(*leftPanel, 0, 0, 1, 2); leftPanelChannels.set_vexpand(true); leftPanel->add1(leftPanelChannels); Gtk::Label *channelsTitle = Gtk::manage(new Gtk::Label()); channelsTitle->set_name("channels-title"); channelsTitle->set_text("Channels"); channelsTitle->set_halign(Gtk::ALIGN_START); leftPanelChannels.attach(*channelsTitle, 0, 0, 1, 1); //// leftPanelUsersStack.set_vexpand(true); leftPanel->add2(leftPanelUsersStack); addRoomButton.set_halign(Gtk::ALIGN_START); leftPanelLayout->attach_next_to(addRoomButton, *leftPanel, Gtk::POS_BOTTOM, 1, 1); addRoomButton.signal_clicked().connect([this]() { InputDialog createRoomDialog("Create a new room", "Room name"); switch(createRoomDialog.run()) { case Gtk::RESPONSE_ACCEPT: { // TODO: Show error inline in the create room dialog Glib::ustring roomName = createRoomDialog.getInput(); if(roomName.size() == 0 || roomName.size() > 32) window->windowNotification->show("Room name has to be between 1 and 32 characters"); else window->rooms->createRoom(roomName); break; } default: break; } }); } void ChatWindow::setupMessageArea(Gtk::Grid *rightPanel) { //messageArea.set_valign(Gtk::ALIGN_START); messageArea.set_vexpand(true); messageArea.set_policy(Gtk::PolicyType::POLICY_NEVER, Gtk::PolicyType::POLICY_AUTOMATIC); rightPanel->attach(messageArea, 0, 0, 1, 2); messageAreaStack.set_name("chat-area-layout"); messageArea.add(messageAreaStack); } void ChatWindow::setupChatInput(Gtk::Grid *rightPanel) { Gtk::Grid *chatArea = Gtk::manage(new Gtk::Grid()); rightPanel->attach_next_to(*chatArea, messageArea, Gtk::POS_BOTTOM, 1, 1); Gtk::ScrolledWindow *chatScrollWindow = Gtk::manage(new Gtk::ScrolledWindow()); chatScrollWindow->set_hexpand(true); chatScrollWindow->set_policy(Gtk::PolicyType::POLICY_NEVER, Gtk::PolicyType::POLICY_NEVER); chatScrollWindow->set_name("chat-scroll-view"); chatArea->attach(*chatScrollWindow, 0, 0, 1, 1); chatInput.set_hexpand(true); chatInput.set_editable(false); chatInput.set_name("chat-input"); chatInput.set_wrap_mode(Gtk::WrapMode::WRAP_WORD_CHAR); chatScrollWindow->add(chatInput); double fontSize = 18.5;//PANGO_PIXELS(chatInput.get_style_context()->get_font().get_size()); chatPrevNumLines = 1; chatInput.signal_key_press_event().connect([this](GdkEventKey *event) { if((event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter) && !(event->state & Gdk::SHIFT_MASK)) { if(chatInput.get_editable()) { currentRoom->publishMessage(chatInput.get_buffer()->get_text()); chatInput.get_buffer()->set_text(""); } return true; } return false; }, false); chatInput.get_buffer()->signal_changed().connect([this, chatScrollWindow, fontSize] { int numLines = chatInput.get_buffer()->get_line_count(); numLines = std::min(numLines, 10); if(numLines != chatPrevNumLines) { chatPrevNumLines = numLines; chatScrollWindow->set_min_content_height(fontSize * numLines); auto adj = chatScrollWindow->get_vadjustment(); if(chatInput.get_buffer()->get_line_count() <= 10) { chatScrollWindow->set_policy(Gtk::PolicyType::POLICY_NEVER, Gtk::PolicyType::POLICY_NEVER); adj->set_value(0); } else { chatScrollWindow->set_policy(Gtk::PolicyType::POLICY_NEVER, Gtk::PolicyType::POLICY_ALWAYS); } } }); chatScrollWindow->get_vadjustment()->signal_value_changed().connect([this, chatScrollWindow]() { auto adj = chatScrollWindow->get_vadjustment(); if(chatInput.get_buffer()->get_line_count() <= 11) { chatScrollWindow->set_policy(Gtk::PolicyType::POLICY_NEVER, Gtk::PolicyType::POLICY_NEVER); adj->set_value(0); } else { chatScrollWindow->set_policy(Gtk::PolicyType::POLICY_NEVER, Gtk::PolicyType::POLICY_ALWAYS); } }); } void ChatWindow::addRoom(std::shared_ptr room) { std::string roomIdStr = room->id->toString(); Gtk::Grid *leftPanelUsersLayout = new Gtk::Grid(); leftPanelUsersLayout->set_vexpand(true); leftPanelUsersLayout->show(); leftPanelUsersStack.add(*leftPanelUsersLayout, roomIdStr); Gtk::Label *usersTitle = Gtk::manage(new Gtk::Label()); usersTitle->get_style_context()->add_class("users-title"); usersTitle->set_text("Users"); usersTitle->set_halign(Gtk::ALIGN_START); usersTitle->show(); leftPanelUsersLayout->attach(*usersTitle, 0, 0, 1, 1); Gtk::Grid *messageAreaLayout = new Gtk::Grid(); messageAreaLayout->show(); messageAreaStack.add(*messageAreaLayout, roomIdStr); fprintf(stderr, "Added channel %s\n", room->id->toString().c_str()); Gtk::ToggleButton *roomButton = new Gtk::ToggleButton("Room name"); roomButton->set_active(true); roomButton->get_style_context()->add_class("room-button"); roomButton->set_hexpand(true); roomButton->set_halign(Gtk::ALIGN_START); roomButton->get_child()->set_halign(Gtk::ALIGN_START); roomButton->show(); roomButton->signal_clicked().connect([this, room] { setCurrentRoom(room); }); leftPanelChannels.attach(*roomButton, 0, 1 + roomCount, 1, 1); ++roomCount; currentRoomData = new RoomData { leftPanelUsersLayout, messageAreaLayout, roomButton }; roomDataById[*room->id] = currentRoomData; currentRoom = room; chatInput.set_editable(true); leftPanelUsersStack.set_visible_child(roomIdStr); messageAreaStack.set_visible_child(roomIdStr); } void ChatWindow::setCurrentRoom(std::shared_ptr room) { std::string roomIdStr = room->id->toString(); leftPanelUsersStack.set_visible_child(roomIdStr); messageAreaStack.set_visible_child(roomIdStr); currentChannelTitle.set_text(room->name); currentRoom = room; currentRoomData = roomDataById[*room->id]; chatInput.set_editable(true); // TODO: Instead of scrolling to bottom, remember scroll position (even after restarting application). // We want to show oldest unread message first scrollToBottom(); } void ChatWindow::addMessage(const RoomAddMessageRequest &request) { auto roomMessages = request.room->messages; RoomMessage *lastMessage = nullptr; if(!roomMessages.empty()) lastMessage = &roomMessages.back(); if(lastMessage && lastMessage->creator->publicKey == request.message.creator->publicKey) { int64_t msgTimeDiff = (int64_t)request.message.timestampSeconds - (int64_t)lastMessage->timestampSeconds; if(msgTimeDiff <= MERGE_MESSAGE_TIMESTAMP_DIFF_SEC) { auto message = messageById[lastMessage->id]; message->text.set_text(message->text.get_text() + "\n" + request.message.text); // Since messages that are sent withing a timeframe are combined, several message ids can refer to the same message messageById[request.message.id] = message; if(!request.loadedFromCache && *request.room->id == *currentRoom->id) { currentRoomData->messageAreaLayout->queue_draw(); scrollToBottom(); } return; } } std::string userNickname = request.message.creator->nickname; if(userNickname.empty()) userNickname = "Anonymous"; ChatMessage *message = Gtk::manage(new ChatMessage(userNickname, request.message.text, request.message.timestampSeconds)); message->set_valign(Gtk::Align::ALIGN_START); message->set_hexpand(true); message->show_all(); messageById[request.message.id] = message; RoomData *roomData = roomDataById[*request.room->id]; roomData->messageAreaLayout->attach(*message, 0, roomMessages.size(), 1, 1); // TODO: When we get a message in the current room we scroll to the bottom, but this should only be done if we are not manually scrolling to view old messages if(!request.loadedFromCache && *request.room->id == *currentRoom->id) { roomData->messageAreaLayout->queue_draw(); scrollToBottom(); } } void ChatWindow::addUser(std::shared_ptr room, std::shared_ptr user) { Gtk::Label *username = Gtk::manage(new Gtk::Label("Anonymous")); username->set_halign(Gtk::ALIGN_START); username->show(); username->get_style_context()->add_class("username-list-username"); user->userdata = username; RoomData *roomData = roomDataById[*room->id]; roomData->leftPanelUsersLayout->attach(*username, 0, room->userByPublicKey.size(), 1, 1); fprintf(stderr, "Added user %s\n", user->publicKey.toString().c_str()); } void ChatWindow::setUserNickname(const UserChangeNicknameRequest &request) { Gtk::Label *userNicknameLabel = (Gtk::Label*)request.user->userdata; userNicknameLabel->set_text(request.newNickname); fprintf(stderr, "Set nickname for user %s to %s\n", request.user->publicKey.toString().c_str(), request.newNickname.c_str()); } void ChatWindow::changeRoomName(const RoomChangeNameRequest &request) { Gtk::Button *button = roomDataById[*request.room->id]->button; static_cast(button->get_child())->set_text(request.newName); if(*request.room->id == *currentRoom->id) currentChannelTitle.set_text(request.newName); fprintf(stderr, "Changed room %s name to %s\n", request.room->id->toString().c_str(), request.newName.c_str()); } void ChatWindow::scrollToBottom() { while(gtk_events_pending()) gtk_main_iteration_do(FALSE); auto adj = messageArea.get_vadjustment(); adj->set_value(adj->get_upper()); } }