2023-07-28 18:03:45 +02:00
|
|
|
#include "./chat_gui4.hpp"
|
|
|
|
|
|
|
|
#include "./file_selector.hpp"
|
|
|
|
|
|
|
|
#include <solanaceae/message3/components.hpp>
|
|
|
|
#include <solanaceae/tox_messages/components.hpp>
|
|
|
|
#include <solanaceae/contact/components.hpp>
|
2024-01-14 18:50:23 +01:00
|
|
|
#include <solanaceae/util/utils.hpp>
|
2023-07-28 18:03:45 +02:00
|
|
|
|
2023-12-13 14:02:31 +01:00
|
|
|
// HACK: remove them
|
|
|
|
#include <solanaceae/tox_contacts/components.hpp>
|
2023-07-28 18:03:45 +02:00
|
|
|
|
|
|
|
#include <imgui/imgui.h>
|
|
|
|
#include <imgui/misc/cpp/imgui_stdlib.h>
|
|
|
|
|
2023-07-30 15:10:26 +02:00
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
|
2023-08-02 19:24:51 +02:00
|
|
|
#include "./media_meta_info_loader.hpp"
|
2023-07-30 15:10:26 +02:00
|
|
|
#include "./sdl_clipboard_utils.hpp"
|
|
|
|
|
2024-01-09 02:26:50 +01:00
|
|
|
#include <cctype>
|
2023-10-04 02:19:03 +02:00
|
|
|
#include <cstdint>
|
2023-07-28 18:03:45 +02:00
|
|
|
#include <ctime>
|
2023-10-20 21:40:45 +02:00
|
|
|
#include <cstdio>
|
2024-01-09 02:26:50 +01:00
|
|
|
#include <chrono>
|
|
|
|
#include <filesystem>
|
2023-10-04 02:19:03 +02:00
|
|
|
#include <fstream>
|
2024-01-09 02:26:50 +01:00
|
|
|
#include <iomanip>
|
2023-10-04 02:19:03 +02:00
|
|
|
#include <sstream>
|
2024-01-09 02:26:50 +01:00
|
|
|
#include <string>
|
|
|
|
#include <variant>
|
|
|
|
#include <vector>
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
|
2023-09-29 18:15:18 +02:00
|
|
|
namespace Components {
|
|
|
|
|
|
|
|
struct UnreadFade {
|
|
|
|
// fades form 1 to 0
|
|
|
|
float fade {1.f};
|
|
|
|
};
|
|
|
|
|
|
|
|
} // Components
|
|
|
|
|
|
|
|
static float lerp(float a, float b, float t) {
|
|
|
|
return a + t * (b - a);
|
|
|
|
}
|
|
|
|
|
2024-01-09 02:26:50 +01:00
|
|
|
static std::string file_url_escape(const std::string&& value) {
|
|
|
|
std::ostringstream escaped;
|
|
|
|
|
|
|
|
escaped << std::hex;
|
|
|
|
escaped.fill('0');
|
|
|
|
|
|
|
|
for (const char c : value) {
|
|
|
|
if (
|
|
|
|
c == '-' || c == '_' || c == '.' || c == '~' || // normal allowed url chars
|
|
|
|
std::isalnum(static_cast<unsigned char>(c)) || // more normal
|
|
|
|
c == '/' // special bc its a file://
|
|
|
|
) {
|
|
|
|
escaped << c;
|
|
|
|
} else {
|
|
|
|
escaped
|
|
|
|
<< std::uppercase
|
|
|
|
<< '%' <<
|
|
|
|
std::setw(2) << static_cast<int>((static_cast<unsigned char>(c)))
|
|
|
|
<< std::nouppercase
|
|
|
|
;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return escaped.str();
|
|
|
|
}
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
ChatGui4::ChatGui4(
|
2023-07-29 20:07:59 +02:00
|
|
|
ConfigModelI& conf,
|
2023-07-28 18:03:45 +02:00
|
|
|
RegistryMessageModel& rmm,
|
|
|
|
Contact3Registry& cr,
|
|
|
|
TextureUploaderI& tu
|
2023-10-04 02:11:06 +02:00
|
|
|
) : _conf(conf), _rmm(rmm), _cr(cr), _tal(_cr), _contact_tc(_tal, tu), _msg_tc(_mil, tu), _sip(tu) {
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
|
2024-01-07 16:33:08 +01:00
|
|
|
void ChatGui4::render(float time_delta) {
|
2023-08-01 18:25:56 +02:00
|
|
|
if (!_cr.storage<Contact::Components::TagAvatarInvalidate>().empty()) { // handle force-reloads for avatars
|
|
|
|
std::vector<Contact3> to_purge;
|
|
|
|
_cr.view<Contact::Components::TagAvatarInvalidate>().each([&to_purge](const Contact3 c) {
|
|
|
|
to_purge.push_back(c);
|
|
|
|
});
|
|
|
|
_cr.remove<Contact::Components::TagAvatarInvalidate>(to_purge.cbegin(), to_purge.cend());
|
|
|
|
_contact_tc.invalidate(to_purge);
|
|
|
|
}
|
|
|
|
// ACTUALLY NOT IF RENDERED, MOVED LOGIC TO ABOVE
|
|
|
|
// it might unload textures, so it needs to be done before rendering
|
|
|
|
_contact_tc.update();
|
2023-08-02 19:24:51 +02:00
|
|
|
_msg_tc.update();
|
2023-08-01 18:25:56 +02:00
|
|
|
|
2023-10-11 21:57:36 +02:00
|
|
|
_fss.render();
|
2024-01-07 16:33:08 +01:00
|
|
|
_sip.render(time_delta);
|
2023-10-11 21:57:36 +02:00
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
|
|
|
ImGui::SetNextWindowPos(viewport->WorkPos);
|
|
|
|
ImGui::SetNextWindowSize(viewport->WorkSize);
|
|
|
|
|
|
|
|
TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x;
|
|
|
|
TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing();
|
|
|
|
|
|
|
|
constexpr auto bg_window_flags =
|
|
|
|
ImGuiWindowFlags_NoDecoration |
|
|
|
|
ImGuiWindowFlags_NoMove |
|
|
|
|
ImGuiWindowFlags_NoResize |
|
|
|
|
ImGuiWindowFlags_NoSavedSettings |
|
|
|
|
ImGuiWindowFlags_MenuBar |
|
|
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
|
|
|
|
|
|
if (ImGui::Begin("tomato", nullptr, bg_window_flags)) {
|
|
|
|
if (ImGui::BeginMenuBar()) {
|
|
|
|
//ImGui::Separator();
|
|
|
|
ImGui::Text("%.1fFPS", ImGui::GetIO().Framerate);
|
|
|
|
ImGui::EndMenuBar();
|
|
|
|
}
|
|
|
|
|
|
|
|
renderContactList();
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
if (_selected_contact) {
|
|
|
|
const std::string chat_label = "chat " + std::to_string(entt::to_integral(*_selected_contact));
|
2024-01-12 16:45:52 +01:00
|
|
|
|
|
|
|
const std::vector<Contact3>* sub_contacts = nullptr;
|
|
|
|
if (_cr.all_of<Contact::Components::ParentOf>(*_selected_contact)) {
|
|
|
|
sub_contacts = &_cr.get<Contact::Components::ParentOf>(*_selected_contact).subs;
|
|
|
|
}
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
if (ImGui::BeginChild(chat_label.c_str(), {0, 0}, true)) {
|
2024-01-12 16:45:52 +01:00
|
|
|
//if (_cr.all_of<Contact::Components::ParentOf>(*_selected_contact)) {
|
|
|
|
if (sub_contacts != nullptr) {
|
|
|
|
if (!sub_contacts->empty()) {
|
2023-07-28 18:03:45 +02:00
|
|
|
if (ImGui::BeginChild("subcontacts", {150, -100}, true)) {
|
2024-01-12 16:45:52 +01:00
|
|
|
ImGui::Text("subs: %zu", sub_contacts->size());
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::Separator();
|
2024-01-12 16:45:52 +01:00
|
|
|
for (const auto& c : *sub_contacts) {
|
2023-08-06 16:07:50 +02:00
|
|
|
// TODO: can a sub be selected? no
|
|
|
|
if (renderSubContactListContact(c, _selected_contact.has_value() && *_selected_contact == c)) {
|
2023-09-01 21:13:36 +02:00
|
|
|
_text_input_buffer.insert(0, (_cr.all_of<Contact::Components::Name>(c) ? _cr.get<Contact::Components::Name>(c).name : "<unk>") + ": ");
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
|
|
ImGui::SameLine();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-29 20:07:59 +02:00
|
|
|
const bool request_incoming = _cr.all_of<Contact::Components::RequestIncoming>(*_selected_contact);
|
|
|
|
const bool request_outgoing = _cr.all_of<Contact::Components::TagRequestOutgoing>(*_selected_contact);
|
|
|
|
if (request_incoming || request_outgoing) {
|
|
|
|
// TODO: theming
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, {0.90f, 0.70f, 0.00f, 0.32f});
|
|
|
|
if (ImGui::BeginChild("request", {0, TEXT_BASE_HEIGHT*6.1f}, true, ImGuiWindowFlags_NoScrollbar)) {
|
|
|
|
if (request_incoming) {
|
|
|
|
const auto& ri = _cr.get<Contact::Components::RequestIncoming>(*_selected_contact);
|
|
|
|
ImGui::TextUnformatted("You got a request to add this contact.");
|
|
|
|
|
|
|
|
static std::string self_name = _conf.get_string("tox", "name").value_or("default_tomato");
|
|
|
|
if (ri.name) {
|
|
|
|
ImGui::InputText("name to join with", &self_name);
|
|
|
|
} else {
|
2023-07-29 20:39:31 +02:00
|
|
|
//ImGui::TextUnformatted("");
|
|
|
|
ImGui::Dummy({0, TEXT_BASE_HEIGHT});
|
2023-07-29 20:07:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static std::string password;
|
|
|
|
if (ri.password) {
|
|
|
|
ImGui::InputText("password to join with", &password);
|
|
|
|
} else {
|
2023-07-29 20:39:31 +02:00
|
|
|
////ImGui::TextUnformatted("");
|
|
|
|
ImGui::Dummy({0, TEXT_BASE_HEIGHT});
|
2023-07-29 20:07:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (ImGui::Button("Accept")) {
|
|
|
|
_cr.get<Contact::Components::ContactModel>(*_selected_contact)->acceptRequest(*_selected_contact, self_name, password);
|
|
|
|
password.clear();
|
|
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
|
|
if (ImGui::Button("Decline")) {
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ImGui::TextUnformatted("You sent a reqeust to add this contact.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
ImGui::EndChild();
|
|
|
|
}
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
if (ImGui::BeginChild("message_log", {0, -100}, false, ImGuiWindowFlags_MenuBar)) {
|
|
|
|
if (ImGui::BeginMenuBar()) {
|
2023-10-21 18:07:06 +02:00
|
|
|
if (ImGui::BeginMenu("debug")) {
|
|
|
|
ImGui::Checkbox("show extra info", &_show_chat_extra_info);
|
|
|
|
ImGui::Checkbox("show avatar transfers", &_show_chat_avatar_tf);
|
|
|
|
|
2023-12-13 14:02:31 +01:00
|
|
|
ImGui::SeparatorText("tox");
|
|
|
|
|
2024-01-14 18:50:23 +01:00
|
|
|
// TODO: cheese it and rename to copy id?
|
2023-12-13 14:02:31 +01:00
|
|
|
if (_cr.all_of<Contact::Components::ToxGroupPersistent>(*_selected_contact)) {
|
|
|
|
if (ImGui::MenuItem("copy ngc chatid")) {
|
|
|
|
const auto& chat_id = _cr.get<Contact::Components::ToxGroupPersistent>(*_selected_contact).chat_id.data;
|
|
|
|
const auto chat_id_str = bin2hex(std::vector<uint8_t>{chat_id.begin(), chat_id.end()});
|
|
|
|
ImGui::SetClipboardText(chat_id_str.c_str());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-21 18:07:06 +02:00
|
|
|
ImGui::EndMenu();
|
2023-08-01 18:25:56 +02:00
|
|
|
}
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::EndMenuBar();
|
|
|
|
}
|
|
|
|
|
|
|
|
auto* msg_reg_ptr = _rmm.get(*_selected_contact);
|
|
|
|
|
2023-10-02 17:03:00 +02:00
|
|
|
if (msg_reg_ptr != nullptr) {
|
|
|
|
const auto& mm = *msg_reg_ptr;
|
|
|
|
//const auto& unread_storage = mm.storage<Message::Components::TagUnread>();
|
|
|
|
if (const auto* unread_storage = mm.storage<Message::Components::TagUnread>(); unread_storage != nullptr && !unread_storage->empty()) {
|
|
|
|
//assert(unread_storage->size() == 0);
|
|
|
|
//assert(unread_storage.cbegin() == unread_storage.cend());
|
2023-11-28 13:19:00 +01:00
|
|
|
|
|
|
|
#if 0
|
2023-10-02 17:03:00 +02:00
|
|
|
std::cout << "UNREAD ";
|
|
|
|
Message3 prev_ent = entt::null;
|
|
|
|
for (const Message3 e : mm.view<Message::Components::TagUnread>()) {
|
|
|
|
std::cout << entt::to_integral(e) << " ";
|
|
|
|
if (prev_ent == e) {
|
|
|
|
assert(false && "dup");
|
|
|
|
}
|
|
|
|
prev_ent = e;
|
|
|
|
}
|
|
|
|
std::cout << "\n";
|
2023-11-28 13:19:00 +01:00
|
|
|
#endif
|
2023-10-02 17:03:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
constexpr ImGuiTableFlags table_flags =
|
|
|
|
ImGuiTableFlags_BordersInnerV |
|
|
|
|
ImGuiTableFlags_RowBg |
|
|
|
|
ImGuiTableFlags_SizingFixedFit
|
|
|
|
;
|
2024-01-12 16:45:52 +01:00
|
|
|
if (msg_reg_ptr != nullptr && ImGui::BeginTable("chat_table", 5, table_flags)) {
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::TableSetupColumn("name", 0, TEXT_BASE_WIDTH * 15.f);
|
|
|
|
ImGui::TableSetupColumn("message", ImGuiTableColumnFlags_WidthStretch);
|
2024-01-12 16:45:52 +01:00
|
|
|
ImGui::TableSetupColumn("delivered/read");
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::TableSetupColumn("timestamp");
|
|
|
|
ImGui::TableSetupColumn("extra_info", _show_chat_extra_info ? ImGuiTableColumnFlags_None : ImGuiTableColumnFlags_Disabled);
|
|
|
|
|
|
|
|
// very hacky, and we have variable hight entries
|
|
|
|
//ImGuiListClipper clipper;
|
|
|
|
|
|
|
|
// fake empty placeholders
|
|
|
|
// TODO: save/calc height for each row
|
|
|
|
// - use number of lines for text
|
|
|
|
// - save img dims (capped)
|
|
|
|
// - other static sizes
|
|
|
|
//ImGui::TableNextRow(0, TEXT_BASE_HEIGHT);
|
|
|
|
//ImGui::TableNextRow(0, TEXT_BASE_HEIGHT);
|
|
|
|
|
|
|
|
Message3Registry& msg_reg = *msg_reg_ptr;
|
2023-09-29 18:15:18 +02:00
|
|
|
|
|
|
|
// do systems TODO: extract
|
|
|
|
{ // fade system
|
|
|
|
std::vector<Message3> to_remove;
|
2024-01-09 16:30:27 +01:00
|
|
|
msg_reg.view<Components::UnreadFade>().each([&to_remove, time_delta](const Message3 e, Components::UnreadFade& fade) {
|
2023-09-29 18:15:18 +02:00
|
|
|
// TODO: configurable
|
|
|
|
const float fade_duration = 7.5f;
|
2024-01-10 14:17:29 +01:00
|
|
|
fade.fade -= 1.f/fade_duration * std::min<float>(time_delta, 1.f/10.f); // fps but not below 10 for smooth fade
|
2023-09-29 18:15:18 +02:00
|
|
|
if (fade.fade <= 0.f) {
|
|
|
|
to_remove.push_back(e);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
msg_reg.remove<Components::UnreadFade>(to_remove.cbegin(), to_remove.cend());
|
|
|
|
}
|
|
|
|
|
2024-01-09 22:38:13 +01:00
|
|
|
//auto tmp_view = msg_reg.view<Message::Components::ContactFrom, Message::Components::ContactTo, Message::Components::Timestamp>();
|
|
|
|
//tmp_view.use<Message::Components::Timestamp>();
|
|
|
|
//tmp_view.each([&](const Message3 e, Message::Components::ContactFrom& c_from, Message::Components::ContactTo& c_to, Message::Components::Timestamp ts
|
|
|
|
//) {
|
|
|
|
auto tmp_view = msg_reg.view<Message::Components::Timestamp>();
|
|
|
|
for (auto view_it = tmp_view.rbegin(), view_last = tmp_view.rend(); view_it != view_last; view_it++) {
|
|
|
|
const Message3 e = *view_it;
|
|
|
|
|
|
|
|
// manually filter ("reverse" iteration <.<)
|
|
|
|
if (!msg_reg.all_of<Message::Components::ContactFrom, Message::Components::ContactTo>(e)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
Message::Components::ContactFrom& c_from = msg_reg.get<Message::Components::ContactFrom>(e);
|
|
|
|
Message::Components::ContactTo& c_to = msg_reg.get<Message::Components::ContactTo>(e);
|
|
|
|
Message::Components::Timestamp ts = tmp_view.get<Message::Components::Timestamp>(e);
|
|
|
|
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
// TODO: why?
|
|
|
|
ImGui::TableNextRow(0, TEXT_BASE_HEIGHT);
|
|
|
|
|
|
|
|
ImGui::PushID(entt::to_integral(e));
|
|
|
|
|
|
|
|
// name
|
|
|
|
if (ImGui::TableNextColumn()) {
|
|
|
|
if (_cr.all_of<Contact::Components::Name>(c_from.c)) {
|
|
|
|
ImGui::TextUnformatted(_cr.get<Contact::Components::Name>(c_from.c).name.c_str());
|
|
|
|
} else {
|
|
|
|
ImGui::TextUnformatted("<unk>");
|
|
|
|
}
|
|
|
|
|
2023-09-29 18:15:18 +02:00
|
|
|
// use username as visibility test
|
|
|
|
if (ImGui::IsItemVisible() && msg_reg.all_of<Message::Components::TagUnread>(e)) {
|
|
|
|
// get time now
|
|
|
|
const uint64_t ts_now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
|
|
|
msg_reg.emplace_or_replace<Message::Components::Read>(e, ts_now);
|
|
|
|
msg_reg.remove<Message::Components::TagUnread>(e);
|
|
|
|
msg_reg.emplace_or_replace<Components::UnreadFade>(e, 1.f);
|
|
|
|
}
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
// highlight self
|
|
|
|
if (_cr.any_of<Contact::Components::TagSelfWeak, Contact::Components::TagSelfStrong>(c_from.c)) {
|
|
|
|
ImU32 cell_bg_color = ImGui::GetColorU32(ImVec4(0.3f, 0.7f, 0.3f, 0.20f));
|
|
|
|
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, cell_bg_color);
|
|
|
|
} else {
|
|
|
|
//based on power level?
|
|
|
|
//ImU32 cell_bg_color = ImGui::GetColorU32(ImVec4(0.3f, 0.7f, 0.3f, 0.65f));
|
|
|
|
//ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, cell_bg_color);
|
|
|
|
}
|
|
|
|
|
2023-09-29 18:15:18 +02:00
|
|
|
std::optional<ImVec4> row_bg;
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
// private group message
|
|
|
|
if (_cr.any_of<Contact::Components::TagSelfWeak, Contact::Components::TagSelfStrong>(c_to.c)) {
|
2023-09-29 18:15:18 +02:00
|
|
|
const ImVec4 priv_msg_hi_col = ImVec4(0.5f, 0.2f, 0.5f, 0.35f);
|
|
|
|
ImU32 row_bg_color = ImGui::GetColorU32(priv_msg_hi_col);
|
|
|
|
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, row_bg_color);
|
|
|
|
row_bg = priv_msg_hi_col;
|
|
|
|
}
|
|
|
|
|
|
|
|
// fade
|
|
|
|
if (msg_reg.all_of<Components::UnreadFade>(e)) {
|
|
|
|
ImVec4 hi_color = ImGui::GetStyleColorVec4(ImGuiCol_PlotHistogramHovered);
|
|
|
|
hi_color.w = 0.8f;
|
|
|
|
const ImVec4 orig_color = row_bg.value_or(ImGui::GetStyleColorVec4(ImGuiCol_TableRowBg)); // imgui defaults to 0,0,0,0
|
|
|
|
const float fade_frac = msg_reg.get<Components::UnreadFade>(e).fade;
|
|
|
|
|
|
|
|
ImVec4 res_color{
|
|
|
|
lerp(orig_color.x, hi_color.x, fade_frac),
|
|
|
|
lerp(orig_color.y, hi_color.y, fade_frac),
|
|
|
|
lerp(orig_color.z, hi_color.z, fade_frac),
|
|
|
|
lerp(orig_color.w, hi_color.w, fade_frac),
|
|
|
|
};
|
|
|
|
|
|
|
|
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(res_color));
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// content (msgtext/file)
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
if (msg_reg.all_of<Message::Components::MessageText>(e)) {
|
|
|
|
renderMessageBodyText(msg_reg, e);
|
|
|
|
} else if (msg_reg.any_of<Message::Components::Transfer::FileInfo>(e)) { // add more comps?
|
|
|
|
renderMessageBodyFile(msg_reg, e);
|
|
|
|
} else {
|
2023-08-11 12:18:50 +02:00
|
|
|
ImGui::TextDisabled("---");
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
|
2024-01-12 16:45:52 +01:00
|
|
|
// remote received and read state
|
|
|
|
if (ImGui::TableNextColumn()) {
|
|
|
|
// TODO: theming for hardcoded values
|
|
|
|
|
|
|
|
if (msg_reg.all_of<Message::Components::Remote::TimestampReceived>(e)) {
|
|
|
|
const auto list = msg_reg.get<Message::Components::Remote::TimestampReceived>(e).ts;
|
|
|
|
// wrongly assumes contacts never get removed from a group
|
|
|
|
if (sub_contacts != nullptr && list.size() < sub_contacts->size()) {
|
|
|
|
// if partically delivered
|
2024-01-12 19:04:50 +01:00
|
|
|
ImGui::TextColored(ImVec4{0.8f, 0.8f, 0.1f, 0.7f}, "d");
|
2024-01-12 16:45:52 +01:00
|
|
|
} else {
|
|
|
|
// if fully delivered
|
2024-01-12 18:51:29 +01:00
|
|
|
ImGui::TextColored(ImVec4{0.1f, 0.8f, 0.1f, 0.7f}, "D");
|
2024-01-12 16:45:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (ImGui::BeginItemTooltip()) {
|
|
|
|
std::string synced_by_text {"delivery confirmed by:"};
|
|
|
|
const int64_t now_ts_s = int64_t(Message::getTimeMS() / 1000u);
|
|
|
|
|
|
|
|
for (const auto& [c, syned_ts] : list) {
|
|
|
|
if (_cr.all_of<Contact::Components::TagSelfStrong>(c)) {
|
2024-01-12 19:04:50 +01:00
|
|
|
//synced_by_text += "\n sself(!)"; // makes no sense
|
|
|
|
continue;
|
2024-01-12 16:45:52 +01:00
|
|
|
} else if (_cr.all_of<Contact::Components::TagSelfWeak>(c)) {
|
2024-01-12 19:04:50 +01:00
|
|
|
synced_by_text += "\n wself"; // TODO: add name?
|
2024-01-12 16:45:52 +01:00
|
|
|
} else {
|
|
|
|
synced_by_text += "\n >" + (_cr.all_of<Contact::Components::Name>(c) ? _cr.get<Contact::Components::Name>(c).name : "<unk>");
|
|
|
|
}
|
|
|
|
const int64_t seconds_ago = (int64_t(syned_ts / 1000u) - now_ts_s) * -1;
|
|
|
|
synced_by_text += " (" + std::to_string(seconds_ago) + "sec ago)";
|
|
|
|
}
|
|
|
|
|
|
|
|
ImGui::Text("%s", synced_by_text.c_str());
|
|
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ImGui::TextDisabled("_");
|
|
|
|
}
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
// TODO: dedup
|
|
|
|
if (msg_reg.all_of<Message::Components::Remote::TimestampRead>(e)) {
|
|
|
|
const auto list = msg_reg.get<Message::Components::Remote::TimestampRead>(e).ts;
|
|
|
|
// wrongly assumes contacts never get removed from a group
|
|
|
|
if (sub_contacts != nullptr && list.size() < sub_contacts->size()) {
|
|
|
|
// if partially read
|
2024-01-12 19:04:50 +01:00
|
|
|
ImGui::TextColored(ImVec4{0.8f, 0.8f, 0.1f, 0.7f}, "r");
|
2024-01-12 16:45:52 +01:00
|
|
|
} else {
|
|
|
|
// if fully read
|
2024-01-12 18:51:29 +01:00
|
|
|
ImGui::TextColored(ImVec4{0.1f, 0.8f, 0.1f, 0.7f}, "R");
|
2024-01-12 16:45:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (ImGui::BeginItemTooltip()) {
|
|
|
|
std::string synced_by_text {"read confirmed by:"};
|
|
|
|
const int64_t now_ts_s = int64_t(Message::getTimeMS() / 1000u);
|
|
|
|
|
|
|
|
for (const auto& [c, syned_ts] : list) {
|
|
|
|
if (_cr.all_of<Contact::Components::TagSelfStrong>(c)) {
|
2024-01-12 19:04:50 +01:00
|
|
|
//synced_by_text += "\n sself(!)"; // makes no sense
|
|
|
|
continue;
|
2024-01-12 16:45:52 +01:00
|
|
|
} else if (_cr.all_of<Contact::Components::TagSelfWeak>(c)) {
|
|
|
|
synced_by_text += "\n wself";
|
|
|
|
} else {
|
|
|
|
synced_by_text += "\n >" + (_cr.all_of<Contact::Components::Name>(c) ? _cr.get<Contact::Components::Name>(c).name : "<unk>");
|
|
|
|
}
|
|
|
|
const int64_t seconds_ago = (int64_t(syned_ts / 1000u) - now_ts_s) * -1;
|
|
|
|
synced_by_text += " (" + std::to_string(seconds_ago) + "sec ago)";
|
|
|
|
}
|
|
|
|
|
|
|
|
ImGui::Text("%s", synced_by_text.c_str());
|
|
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ImGui::TextDisabled("_");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
// ts
|
|
|
|
if (ImGui::TableNextColumn()) {
|
|
|
|
auto time = std::chrono::system_clock::to_time_t(
|
|
|
|
std::chrono::time_point<std::chrono::system_clock, std::chrono::milliseconds>{std::chrono::milliseconds{ts.ts}}
|
|
|
|
);
|
|
|
|
auto localtime = std::localtime(&time);
|
|
|
|
|
|
|
|
ImGui::Text("%.2d:%.2d", localtime->tm_hour, localtime->tm_min);
|
|
|
|
}
|
|
|
|
|
|
|
|
// extra
|
|
|
|
if (ImGui::TableNextColumn()) {
|
|
|
|
renderMessageExtra(msg_reg, e);
|
|
|
|
}
|
|
|
|
|
|
|
|
ImGui::PopID(); // ent
|
2024-01-09 22:38:13 +01:00
|
|
|
}
|
2023-07-28 18:03:45 +02:00
|
|
|
|
|
|
|
// fake empty placeholders
|
|
|
|
//ImGui::TableNextRow(0, TEXT_BASE_HEIGHT);
|
|
|
|
//ImGui::TableNextRow(0, TEXT_BASE_HEIGHT);
|
|
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
|
|
|
|
ImGui::SetScrollHereY(1.f);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
if (ImGui::BeginChild("text_input", {-150, 0})) {
|
|
|
|
static bool evil_enter_looses_focus_hack = false;
|
|
|
|
if (evil_enter_looses_focus_hack) {
|
|
|
|
ImGui::SetKeyboardFocusHere();
|
|
|
|
evil_enter_looses_focus_hack = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
constexpr ImGuiInputTextFlags input_flags =
|
|
|
|
ImGuiInputTextFlags_EnterReturnsTrue |
|
|
|
|
//ImGuiInputTextFlags_AllowTabInput |
|
|
|
|
ImGuiInputTextFlags_NoHorizontalScroll |
|
|
|
|
ImGuiInputTextFlags_CtrlEnterForNewLine;
|
|
|
|
|
2023-09-01 21:13:36 +02:00
|
|
|
if (ImGui::InputTextMultiline("##text_input", &_text_input_buffer, {-0.001f, -0.001f}, input_flags)) {
|
2023-07-28 18:03:45 +02:00
|
|
|
//_mm.sendMessage(*_selected_contact, MessageType::TEXT, text_buffer);
|
2023-09-01 21:13:36 +02:00
|
|
|
_rmm.sendText(*_selected_contact, _text_input_buffer);
|
|
|
|
_text_input_buffer.clear();
|
2023-07-28 18:03:45 +02:00
|
|
|
evil_enter_looses_focus_hack = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
if (ImGui::BeginChild("buttons")) {
|
|
|
|
if (ImGui::Button("send\nfile", {-FLT_MIN, 0})) {
|
|
|
|
_fss.requestFile(
|
|
|
|
[](const auto& path) -> bool { return std::filesystem::is_regular_file(path); },
|
|
|
|
[this](const auto& path){
|
2023-07-29 21:13:08 +02:00
|
|
|
_rmm.sendFilePath(*_selected_contact, path.filename().u8string(), path.u8string());
|
2023-07-28 18:03:45 +02:00
|
|
|
},
|
|
|
|
[](){}
|
|
|
|
);
|
|
|
|
}
|
2023-10-04 02:11:06 +02:00
|
|
|
|
|
|
|
{
|
|
|
|
// TODO: add support for more than images
|
|
|
|
// !!! polling each frame can be VERY expensive !!!
|
|
|
|
//const auto* mime_type = clipboardHasImage();
|
|
|
|
//ImGui::BeginDisabled(mime_type == nullptr);
|
|
|
|
if (ImGui::Button("paste\nfile", {-FLT_MIN, 0})) {
|
|
|
|
const auto* mime_type = clipboardHasImage();
|
|
|
|
if (mime_type != nullptr) { // making sure
|
2023-10-11 21:57:36 +02:00
|
|
|
pasteFile(mime_type);
|
|
|
|
}
|
|
|
|
//} else if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
|
|
} else if (ImGui::BeginPopupContextItem(nullptr, ImGuiMouseButton_Right)) {
|
|
|
|
const static std::vector<const char*> image_mime_types {
|
|
|
|
"image/png",
|
2023-10-14 15:59:32 +02:00
|
|
|
"image/webp",
|
2023-10-11 21:57:36 +02:00
|
|
|
"image/gif",
|
|
|
|
"image/jpeg",
|
|
|
|
"image/bmp",
|
|
|
|
};
|
|
|
|
|
|
|
|
for (const char* mime_type : image_mime_types) {
|
|
|
|
if (ImGui::MenuItem(mime_type)) {
|
|
|
|
pasteFile(mime_type);
|
|
|
|
}
|
2023-10-04 02:11:06 +02:00
|
|
|
}
|
2023-10-12 01:10:38 +02:00
|
|
|
ImGui::EndPopup();
|
2023-10-04 02:11:06 +02:00
|
|
|
}
|
|
|
|
//ImGui::EndDisabled();
|
|
|
|
}
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
ImGui::EndChild();
|
2023-07-30 15:10:26 +02:00
|
|
|
|
2023-10-04 02:11:06 +02:00
|
|
|
#if 0
|
2023-08-06 16:07:50 +02:00
|
|
|
// if preview window not open?
|
|
|
|
if (ImGui::IsKeyPressed(ImGuiKey_V) && ImGui::IsKeyPressed(ImGuiMod_Shortcut, false)) {
|
|
|
|
std::cout << "CG: paste?\n";
|
2023-07-30 15:10:26 +02:00
|
|
|
if (const auto* mime_type = clipboardHasImage(); mime_type != nullptr) {
|
|
|
|
size_t data_size = 0;
|
|
|
|
const auto* data = SDL_GetClipboardData(mime_type, &data_size);
|
|
|
|
// open file send preview.rawpixels
|
2023-08-06 16:07:50 +02:00
|
|
|
std::cout << "CG: pasted image of size " << data_size << " mime " << mime_type << "\n";
|
2023-07-30 15:10:26 +02:00
|
|
|
}
|
|
|
|
}
|
2023-10-04 02:11:06 +02:00
|
|
|
#endif
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ImGui::End();
|
|
|
|
|
2023-08-01 13:21:16 +02:00
|
|
|
_contact_tc.workLoadQueue();
|
2023-08-02 19:24:51 +02:00
|
|
|
_msg_tc.workLoadQueue();
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
|
2023-10-18 14:23:27 +02:00
|
|
|
void ChatGui4::sendFilePath(const char* file_path) {
|
|
|
|
if (_selected_contact && std::filesystem::is_regular_file(file_path)) {
|
|
|
|
_rmm.sendFilePath(*_selected_contact, std::filesystem::path(file_path).filename().u8string(), file_path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
// has MessageText
|
|
|
|
void ChatGui4::renderMessageBodyText(Message3Registry& reg, const Message3 e) {
|
|
|
|
const auto& msgtext = reg.get<Message::Components::MessageText>(e).text;
|
|
|
|
|
2023-07-29 21:13:08 +02:00
|
|
|
// TODO: set word wrap
|
2023-07-28 18:03:45 +02:00
|
|
|
ImVec2 text_size = ImGui::CalcTextSize(msgtext.c_str(), msgtext.c_str()+msgtext.size());
|
|
|
|
text_size.x = -FLT_MIN; // fill width (suppresses label)
|
|
|
|
text_size.y += ImGui::GetStyle().FramePadding.y; // single pad
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {0, 0}); // make align with text height
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, {0.f, 0.f, 0.f, 0.f}); // remove text input box
|
|
|
|
|
|
|
|
ImGui::InputTextMultiline(
|
2023-09-01 21:13:36 +02:00
|
|
|
"##text",
|
2023-07-28 18:03:45 +02:00
|
|
|
const_cast<char*>(msgtext.c_str()), // ugly const cast
|
|
|
|
msgtext.size() + 1, // needs to include '\0'
|
|
|
|
text_size,
|
|
|
|
ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_NoHorizontalScroll
|
|
|
|
);
|
2023-09-01 21:13:36 +02:00
|
|
|
if (ImGui::BeginPopupContextItem("##text")) {
|
|
|
|
if (ImGui::MenuItem("quote")) {
|
|
|
|
//text_buffer.insert(0, (_cr.all_of<Contact::Components::Name>(c) ? _cr.get<Contact::Components::Name>(c).name : "<unk>") + ": ");
|
|
|
|
if (!_text_input_buffer.empty()) {
|
|
|
|
_text_input_buffer += "\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
_text_input_buffer += "> ";
|
|
|
|
|
|
|
|
for (const char c : msgtext) {
|
|
|
|
_text_input_buffer += c;
|
|
|
|
|
|
|
|
if (c == '\n') {
|
|
|
|
_text_input_buffer += "> ";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
|
|
}
|
2023-07-28 18:03:45 +02:00
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChatGui4::renderMessageBodyFile(Message3Registry& reg, const Message3 e) {
|
2023-10-21 18:07:06 +02:00
|
|
|
if (
|
|
|
|
!_show_chat_avatar_tf
|
|
|
|
&& (
|
|
|
|
reg.all_of<Message::Components::Transfer::FileKind>(e)
|
|
|
|
&& reg.get<Message::Components::Transfer::FileKind>(e).kind == 1
|
|
|
|
)
|
|
|
|
) {
|
2023-10-14 15:59:32 +02:00
|
|
|
// TODO: this looks ugly
|
|
|
|
ImGui::TextDisabled("set avatar");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
#if 0
|
|
|
|
if (msg_reg.all_of<Components::TransferState>(e)) {
|
|
|
|
switch (msg_reg.get<Components::TransferState>(e).state) {
|
|
|
|
case Components::TransferState::State::running: ImGui::TextUnformatted("running"); break;
|
|
|
|
case Components::TransferState::State::paused: ImGui::TextUnformatted("paused"); break;
|
|
|
|
case Components::TransferState::State::failed: ImGui::TextUnformatted("failed"); break;
|
|
|
|
case Components::TransferState::State::finished: ImGui::TextUnformatted("finished"); break;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
assert(false);
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
// TODO: better way to display state
|
|
|
|
if (reg.all_of<Message::Components::Transfer::TagPaused>(e)) {
|
|
|
|
ImGui::TextUnformatted("paused");
|
2023-08-06 16:07:50 +02:00
|
|
|
} else if (reg.all_of<Message::Components::Transfer::TagReceiving, Message::Components::Transfer::TagHaveAll>(e)) {
|
|
|
|
ImGui::TextUnformatted("done");
|
2023-07-28 18:03:45 +02:00
|
|
|
} else {
|
2023-08-06 16:07:50 +02:00
|
|
|
// TODO: missing other states
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::TextUnformatted("running");
|
|
|
|
}
|
|
|
|
|
2023-08-06 16:07:50 +02:00
|
|
|
if (reg.all_of<Message::Components::Transfer::TagHaveAll, Message::Components::Transfer::FileInfoLocal>(e)) {
|
|
|
|
// hack lul
|
|
|
|
ImGui::SameLine();
|
|
|
|
if (ImGui::SmallButton("forward")) {
|
|
|
|
ImGui::OpenPopup("forward to contact");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ImGui::BeginPopup("forward to contact")) {
|
|
|
|
// TODO: make exclusion work
|
|
|
|
//for (const auto& c : _cr.view<entt::get_t<Contact::Components::TagBig>, entt::exclude_t<Contact::Components::RequestIncoming, Contact::Components::TagRequestOutgoing>>()) {
|
|
|
|
for (const auto& c : _cr.view<Contact::Components::TagBig>()) {
|
|
|
|
if (renderContactListContactSmall(c, false)) {
|
|
|
|
//_rmm.sendFilePath(*_selected_contact, path.filename().u8string(), path.u8string());
|
|
|
|
const auto& fil = reg.get<Message::Components::Transfer::FileInfoLocal>(e);
|
|
|
|
for (const auto& path : fil.file_list) {
|
|
|
|
_rmm.sendFilePath(c, std::filesystem::path{path}.filename().u8string(), path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
// if in offered state
|
|
|
|
// paused, never started
|
|
|
|
if (
|
|
|
|
reg.all_of<Message::Components::Transfer::TagReceiving>(e) &&
|
|
|
|
reg.all_of<Message::Components::Transfer::TagPaused>(e) &&
|
|
|
|
// TODO: how does restarting a broken/incomplete transfer look like?
|
|
|
|
!reg.all_of<Message::Components::Transfer::FileInfoLocal>(e) &&
|
|
|
|
!reg.all_of<Message::Components::Transfer::ActionAccept>(e)
|
|
|
|
) {
|
|
|
|
if (ImGui::Button("save to")) {
|
|
|
|
_fss.requestFile(
|
|
|
|
[](const auto& path) -> bool { return std::filesystem::is_directory(path); },
|
|
|
|
[this, ®, e](const auto& path) {
|
|
|
|
if (reg.valid(e)) { // still valid
|
2023-08-28 14:06:27 +02:00
|
|
|
// TODO: trim file?
|
2023-07-28 18:03:45 +02:00
|
|
|
reg.emplace<Message::Components::Transfer::ActionAccept>(e, path.string());
|
|
|
|
_rmm.throwEventUpdate(reg, e);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[](){}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// down progress
|
|
|
|
if (reg.all_of<Message::Components::Transfer::TagReceiving>(e)) {
|
|
|
|
ImGui::TextUnformatted("down");
|
|
|
|
if (reg.all_of<Message::Components::Transfer::BytesReceived>(e)) {
|
|
|
|
ImGui::SameLine();
|
2023-10-20 02:39:13 +02:00
|
|
|
|
|
|
|
float fraction = float(reg.get<Message::Components::Transfer::BytesReceived>(e).total) / reg.get<Message::Components::Transfer::FileInfo>(e).total_size;
|
|
|
|
|
|
|
|
char overlay_buf[32];
|
|
|
|
std::snprintf(overlay_buf, sizeof(overlay_buf), "%.1f%%", fraction * 100 + 0.01f);
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::ProgressBar(
|
2023-10-20 02:39:13 +02:00
|
|
|
fraction,
|
|
|
|
{-FLT_MIN, TEXT_BASE_HEIGHT},
|
|
|
|
overlay_buf
|
2023-07-28 18:03:45 +02:00
|
|
|
);
|
|
|
|
// TODO: numbers
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// (can be both)
|
|
|
|
// up progess
|
|
|
|
if (reg.all_of<Message::Components::Transfer::TagSending>(e)) {
|
|
|
|
ImGui::TextUnformatted(" up");
|
|
|
|
if (reg.all_of<Message::Components::Transfer::BytesSent>(e)) {
|
|
|
|
ImGui::SameLine();
|
2023-10-20 02:39:13 +02:00
|
|
|
|
|
|
|
float fraction = float(reg.get<Message::Components::Transfer::BytesSent>(e).total) / reg.get<Message::Components::Transfer::FileInfo>(e).total_size;
|
|
|
|
|
|
|
|
char overlay_buf[32];
|
|
|
|
std::snprintf(overlay_buf, sizeof(overlay_buf), "%.1f%%", fraction * 100 + 0.01f);
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::ProgressBar(
|
2023-10-20 02:39:13 +02:00
|
|
|
fraction,
|
|
|
|
{-FLT_MIN, TEXT_BASE_HEIGHT},
|
|
|
|
overlay_buf
|
2023-07-28 18:03:45 +02:00
|
|
|
);
|
|
|
|
// TODO: numbers
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto file_list = reg.get<Message::Components::Transfer::FileInfo>(e).file_list;
|
|
|
|
|
|
|
|
// if has local, display save base path?
|
|
|
|
|
|
|
|
for (size_t i = 0; i < file_list.size(); i++) {
|
2023-10-04 02:11:06 +02:00
|
|
|
ImGui::PushID(i);
|
|
|
|
|
|
|
|
// TODO: selectable text widget ?
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::Bullet(); ImGui::Text("%s (%lu)", file_list[i].file_name.c_str(), file_list[i].file_size);
|
2023-10-04 02:11:06 +02:00
|
|
|
|
|
|
|
if (reg.all_of<Message::Components::Transfer::FileInfoLocal>(e)) {
|
|
|
|
const auto& local_info = reg.get<Message::Components::Transfer::FileInfoLocal>(e);
|
|
|
|
if (local_info.file_list.size() > i && ImGui::BeginPopupContextItem("##file_c")) {
|
|
|
|
if (ImGui::MenuItem("open")) {
|
2024-01-09 02:26:50 +01:00
|
|
|
std::string url{"file://" + file_url_escape(std::filesystem::canonical(local_info.file_list.at(i)).u8string())};
|
2023-10-04 02:11:06 +02:00
|
|
|
std::cout << "opening file '" << url << "'\n";
|
|
|
|
SDL_OpenURL(url.c_str());
|
|
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ImGui::PopID();
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (file_list.size() == 1 && reg.all_of<Message::Components::Transfer::FileInfoLocal, Message::Components::FrameDims>(e)) {
|
2023-08-02 19:24:51 +02:00
|
|
|
auto [id, width, height] = _msg_tc.get(Message3Handle{reg, e});
|
2023-07-28 18:03:45 +02:00
|
|
|
|
|
|
|
// if cache gives 0s, fall back to frame dims (eg if pic not loaded yet)
|
2023-08-02 19:24:51 +02:00
|
|
|
if (width == 0 || height == 0) {
|
2023-07-28 18:03:45 +02:00
|
|
|
const auto& frame_dims = reg.get<Message::Components::FrameDims>(e);
|
|
|
|
width = frame_dims.width;
|
|
|
|
height = frame_dims.height;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: config
|
|
|
|
const auto max_inline_height = 10*TEXT_BASE_HEIGHT;
|
|
|
|
if (height > max_inline_height) {
|
|
|
|
const float scale = max_inline_height / height;
|
|
|
|
height = max_inline_height;
|
|
|
|
width *= scale;
|
|
|
|
}
|
2023-08-02 19:24:51 +02:00
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::Image(id, ImVec2{static_cast<float>(width), static_cast<float>(height)});
|
2023-10-04 02:11:06 +02:00
|
|
|
// TODO: clickable to open in internal image viewer
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChatGui4::renderMessageExtra(Message3Registry& reg, const Message3 e) {
|
|
|
|
if (reg.all_of<Message::Components::Transfer::FileKind>(e)) {
|
|
|
|
ImGui::TextDisabled("fk:%lu", reg.get<Message::Components::Transfer::FileKind>(e).kind);
|
|
|
|
}
|
|
|
|
if (reg.all_of<Message::Components::Transfer::ToxTransferFriend>(e)) {
|
|
|
|
ImGui::TextDisabled("ttf:%u", reg.get<Message::Components::Transfer::ToxTransferFriend>(e).transfer_number);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reg.all_of<Message::Components::ToxGroupMessageID>(e)) {
|
|
|
|
ImGui::TextDisabled("msgid:%u", reg.get<Message::Components::ToxGroupMessageID>(e).id);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reg.all_of<Message::Components::SyncedBy>(e)) {
|
|
|
|
std::string synced_by_text {"syncedBy:"};
|
2024-01-12 16:45:52 +01:00
|
|
|
const int64_t now_ts_s = int64_t(Message::getTimeMS() / 1000u);
|
|
|
|
|
|
|
|
for (const auto& [c, syned_ts] : reg.get<Message::Components::SyncedBy>(e).ts) {
|
2023-07-28 18:03:45 +02:00
|
|
|
if (_cr.all_of<Contact::Components::TagSelfStrong>(c)) {
|
|
|
|
synced_by_text += "\n sself";
|
|
|
|
} else if (_cr.all_of<Contact::Components::TagSelfWeak>(c)) {
|
|
|
|
synced_by_text += "\n wself";
|
|
|
|
} else {
|
|
|
|
synced_by_text += "\n >" + (_cr.all_of<Contact::Components::Name>(c) ? _cr.get<Contact::Components::Name>(c).name : "<unk>");
|
|
|
|
}
|
2024-01-12 16:45:52 +01:00
|
|
|
const int64_t seconds_ago = (int64_t(syned_ts / 1000u) - now_ts_s) * -1;
|
|
|
|
synced_by_text += " (" + std::to_string(seconds_ago) + "sec ago)";
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
2024-01-12 16:45:52 +01:00
|
|
|
|
|
|
|
ImGui::TextDisabled("%s", synced_by_text.c_str());
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: remove?
|
|
|
|
if (reg.all_of<Message::Components::Remote::TimestampReceived>(e)) {
|
|
|
|
std::string synced_by_text {"receivedBy:"};
|
|
|
|
const int64_t now_ts_s = int64_t(Message::getTimeMS() / 1000u);
|
|
|
|
|
|
|
|
for (const auto& [c, syned_ts] : reg.get<Message::Components::Remote::TimestampReceived>(e).ts) {
|
|
|
|
if (_cr.all_of<Contact::Components::TagSelfStrong>(c)) {
|
|
|
|
synced_by_text += "\n sself(!)"; // makes no sense
|
|
|
|
} else if (_cr.all_of<Contact::Components::TagSelfWeak>(c)) {
|
|
|
|
synced_by_text += "\n wself";
|
|
|
|
} else {
|
|
|
|
synced_by_text += "\n >" + (_cr.all_of<Contact::Components::Name>(c) ? _cr.get<Contact::Components::Name>(c).name : "<unk>");
|
|
|
|
}
|
|
|
|
const int64_t seconds_ago = (int64_t(syned_ts / 1000u) - now_ts_s) * -1;
|
|
|
|
synced_by_text += " (" + std::to_string(seconds_ago) + "sec ago)";
|
|
|
|
}
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::TextDisabled("%s", synced_by_text.c_str());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChatGui4::renderContactList(void) {
|
|
|
|
if (ImGui::BeginChild("contacts", {TEXT_BASE_WIDTH*35, 0})) {
|
|
|
|
//for (const auto& c : _cm.getBigContacts()) {
|
|
|
|
for (const auto& c : _cr.view<Contact::Components::TagBig>()) {
|
2023-08-06 16:07:50 +02:00
|
|
|
if (renderContactListContactBig(c, _selected_contact.has_value() && *_selected_contact == c)) {
|
2023-07-28 18:03:45 +02:00
|
|
|
_selected_contact = c;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
|
|
}
|
|
|
|
|
2023-08-06 16:07:50 +02:00
|
|
|
bool ChatGui4::renderContactListContactBig(const Contact3 c, const bool selected) {
|
2023-07-28 18:03:45 +02:00
|
|
|
// TODO:
|
|
|
|
// - unread message
|
|
|
|
// - avatar img
|
|
|
|
// - connection status
|
|
|
|
// - user status
|
|
|
|
// - status message
|
|
|
|
// - context menu n shit?
|
|
|
|
|
|
|
|
// +------+
|
|
|
|
// | | *Name (Alias?)
|
|
|
|
// |Avatar| Satus Message
|
|
|
|
// | | user status (online/away/busy)-direct/relayed / offline
|
|
|
|
// +------+
|
|
|
|
|
|
|
|
auto label = "###" + std::to_string(entt::to_integral(c));
|
|
|
|
|
2023-07-29 20:07:59 +02:00
|
|
|
const bool request_incoming = _cr.all_of<Contact::Components::RequestIncoming>(c);
|
|
|
|
const bool request_outgoing = _cr.all_of<Contact::Components::TagRequestOutgoing>(c);
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
ImVec2 orig_curser_pos = ImGui::GetCursorPos();
|
2023-07-29 20:07:59 +02:00
|
|
|
// HACK: fake selected to make it draw a box for us
|
2023-08-06 16:07:50 +02:00
|
|
|
const bool show_selected = request_incoming || request_outgoing || selected;
|
2023-07-29 20:07:59 +02:00
|
|
|
if (request_incoming) {
|
|
|
|
// TODO: theming
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Header, {0.98f, 0.41f, 0.26f, 0.52f});
|
|
|
|
} else if (request_outgoing) {
|
|
|
|
// TODO: theming
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Header, {0.98f, 0.26f, 0.41f, 0.52f});
|
|
|
|
}
|
|
|
|
|
2023-08-06 16:07:50 +02:00
|
|
|
const bool got_selected = ImGui::Selectable(label.c_str(), show_selected, 0, {0,3*TEXT_BASE_HEIGHT});
|
2023-07-29 20:07:59 +02:00
|
|
|
|
|
|
|
if (request_incoming || request_outgoing) {
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
}
|
2023-07-28 18:03:45 +02:00
|
|
|
ImVec2 post_curser_pos = ImGui::GetCursorPos();
|
|
|
|
|
|
|
|
ImVec2 img_curser {
|
|
|
|
orig_curser_pos.x + ImGui::GetStyle().FramePadding.x,
|
|
|
|
orig_curser_pos.y + ImGui::GetStyle().FramePadding.y
|
|
|
|
};
|
|
|
|
|
|
|
|
float img_y {
|
|
|
|
//(post_curser_pos.y - orig_curser_pos.y) - ImGui::GetStyle().FramePadding.y*2
|
|
|
|
TEXT_BASE_HEIGHT*3 - ImGui::GetStyle().FramePadding.y*2
|
|
|
|
};
|
|
|
|
|
|
|
|
ImGui::SetCursorPos(img_curser);
|
|
|
|
const ImVec4 color_online_direct{0.3, 1, 0, 1};
|
|
|
|
const ImVec4 color_online_cloud{0, 1, 0.8, 1};
|
|
|
|
const ImVec4 color_offline{0.4, 0.4, 0.4, 1};
|
|
|
|
|
|
|
|
ImVec4 color_current = color_offline;
|
|
|
|
if (_cr.all_of<Contact::Components::ConnectionState>(c)) {
|
|
|
|
const auto c_state = _cr.get<Contact::Components::ConnectionState>(c).state;
|
|
|
|
if (c_state == Contact::Components::ConnectionState::State::direct) {
|
|
|
|
color_current = color_online_direct;
|
|
|
|
} else if (c_state == Contact::Components::ConnectionState::State::cloud) {
|
|
|
|
color_current = color_online_cloud;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// avatar
|
2023-07-31 20:47:22 +02:00
|
|
|
const auto [id, width, height] = _contact_tc.get(c);
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::Image(
|
|
|
|
id,
|
|
|
|
ImVec2{img_y, img_y},
|
|
|
|
{0, 0},
|
|
|
|
{1, 1},
|
|
|
|
{1, 1, 1, 1},
|
|
|
|
color_current
|
|
|
|
);
|
|
|
|
|
2023-10-04 02:11:06 +02:00
|
|
|
// TODO: move this out of chat gui
|
|
|
|
any_unread = false;
|
|
|
|
|
2023-07-28 18:03:45 +02:00
|
|
|
ImGui::SameLine();
|
|
|
|
ImGui::BeginGroup();
|
|
|
|
{
|
2023-09-29 18:15:18 +02:00
|
|
|
// TODO: is there a better way?
|
|
|
|
// maybe cache mm?
|
|
|
|
bool has_unread = false;
|
2023-10-02 15:40:32 +02:00
|
|
|
if (const auto* mm = _rmm.get(c); mm != nullptr) {
|
|
|
|
if (const auto* unread_storage = mm->storage<Message::Components::TagUnread>(); unread_storage != nullptr && !unread_storage->empty()) {
|
2023-10-02 17:03:00 +02:00
|
|
|
#if 0
|
|
|
|
assert(unread_storage.size() == 0);
|
|
|
|
assert(unread_storage.cbegin() == unread_storage.cend());
|
|
|
|
std::cout << "UNREAD ";
|
|
|
|
for (const auto e : mm->view<Message::Components::TagUnread>()) {
|
|
|
|
std::cout << entt::to_integral(e) << " ";
|
|
|
|
}
|
|
|
|
std::cout << "\n";
|
|
|
|
#endif
|
2023-10-02 15:40:32 +02:00
|
|
|
has_unread = true;
|
2023-10-04 02:11:06 +02:00
|
|
|
any_unread = true;
|
2023-10-02 15:40:32 +02:00
|
|
|
}
|
2023-09-29 18:15:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ImGui::Text("%s%s", has_unread?"* ":"", (_cr.all_of<Contact::Components::Name>(c) ? _cr.get<Contact::Components::Name>(c).name.c_str() : "<unk>"));
|
2023-07-29 20:07:59 +02:00
|
|
|
if (request_incoming) {
|
|
|
|
ImGui::TextUnformatted("Incoming request/invite");
|
|
|
|
} else if (request_outgoing) {
|
|
|
|
ImGui::TextUnformatted("Outgoing request/invite");
|
|
|
|
}
|
2023-07-28 18:03:45 +02:00
|
|
|
//ImGui::Text("status message...");
|
|
|
|
//ImGui::TextDisabled("hi");
|
|
|
|
//ImGui::RenderTextEllipsis
|
|
|
|
}
|
|
|
|
ImGui::EndGroup();
|
|
|
|
|
|
|
|
ImGui::SetCursorPos(post_curser_pos);
|
2023-08-06 16:07:50 +02:00
|
|
|
return got_selected;
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
|
2023-08-06 16:07:50 +02:00
|
|
|
bool ChatGui4::renderContactListContactSmall(const Contact3 c, const bool selected) const {
|
2023-07-28 18:03:45 +02:00
|
|
|
std::string label;
|
|
|
|
|
|
|
|
label += (_cr.all_of<Contact::Components::Name>(c) ? _cr.get<Contact::Components::Name>(c).name.c_str() : "<unk>");
|
|
|
|
label += "###";
|
|
|
|
label += std::to_string(entt::to_integral(c));
|
|
|
|
|
2023-08-06 16:07:50 +02:00
|
|
|
return ImGui::Selectable(label.c_str(), selected);
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
|
2023-08-06 16:07:50 +02:00
|
|
|
bool ChatGui4::renderSubContactListContact(const Contact3 c, const bool selected) const {
|
2023-07-28 18:03:45 +02:00
|
|
|
std::string label;
|
|
|
|
|
|
|
|
if (_cr.all_of<Contact::Components::ConnectionState>(c)) {
|
|
|
|
const auto c_state = _cr.get<Contact::Components::ConnectionState>(c).state;
|
|
|
|
if (c_state == Contact::Components::ConnectionState::State::direct) {
|
|
|
|
label += "[D] ";
|
|
|
|
} else if (c_state == Contact::Components::ConnectionState::State::cloud) {
|
|
|
|
label += "[C] ";
|
|
|
|
} else {
|
|
|
|
label += "[-] ";
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
label += "[?] ";
|
|
|
|
}
|
|
|
|
|
|
|
|
label += (_cr.all_of<Contact::Components::Name>(c) ? _cr.get<Contact::Components::Name>(c).name.c_str() : "<unk>");
|
|
|
|
label += "###";
|
|
|
|
label += std::to_string(entt::to_integral(c));
|
|
|
|
|
2023-08-06 16:07:50 +02:00
|
|
|
return ImGui::Selectable(label.c_str(), selected);
|
2023-07-28 18:03:45 +02:00
|
|
|
}
|
|
|
|
|
2023-10-11 21:57:36 +02:00
|
|
|
void ChatGui4::pasteFile(const char* mime_type) {
|
|
|
|
size_t data_size = 0;
|
|
|
|
void* data = SDL_GetClipboardData(mime_type, &data_size);
|
|
|
|
|
|
|
|
// if image
|
|
|
|
|
|
|
|
std::cout << "CG: pasted image of size " << data_size << " mime " << mime_type << "\n";
|
|
|
|
|
|
|
|
_sip.sendMemory(
|
|
|
|
static_cast<const uint8_t*>(data), data_size,
|
|
|
|
[this](const auto& img_data, const auto file_ext) {
|
|
|
|
// create file name
|
|
|
|
// TODO: move this into sip
|
|
|
|
std::ostringstream tmp_file_name {"tomato_Image_", std::ios_base::ate};
|
|
|
|
{
|
|
|
|
const auto now = std::chrono::system_clock::now();
|
|
|
|
const auto ctime = std::chrono::system_clock::to_time_t(now);
|
|
|
|
tmp_file_name
|
|
|
|
<< std::put_time(std::localtime(&ctime), "%F_%H-%M-%S")
|
|
|
|
<< "."
|
|
|
|
<< std::setfill('0') << std::setw(3)
|
|
|
|
<< std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch() - std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch())).count()
|
|
|
|
<< file_ext
|
|
|
|
;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::cout << "tmp image path " << tmp_file_name.str() << "\n";
|
|
|
|
|
|
|
|
const std::filesystem::path tmp_send_file_path = "tmp_send_files";
|
|
|
|
std::filesystem::create_directories(tmp_send_file_path);
|
|
|
|
const auto tmp_file_path = tmp_send_file_path / tmp_file_name.str();
|
|
|
|
|
|
|
|
std::ofstream(tmp_file_path, std::ios_base::out | std::ios_base::binary)
|
|
|
|
.write(reinterpret_cast<const char*>(img_data.data()), img_data.size());
|
|
|
|
|
|
|
|
_rmm.sendFilePath(*_selected_contact, tmp_file_name.str(), tmp_file_path.u8string());
|
|
|
|
},
|
|
|
|
[](){}
|
|
|
|
);
|
|
|
|
SDL_free(data); // free data
|
|
|
|
}
|
|
|
|
|