#include "./chat_gui4.hpp" #include #include #include #include #include #include #include #include #include "./frame_streams/voip_model.hpp" // HACK: remove them #include #include #include #include #include #include #include "./chat_gui/contact_list.hpp" #include "./media_meta_info_loader.hpp" #include "./sdl_clipboard_utils.hpp" #include "os_comps.hpp" #include "./string_formatter_utils.hpp" #include #include #include #include #include #include #include #include #include #include namespace Components { struct UnreadFade { // fades form 1 to 0 float fade {1.f}; }; struct ConvertedTimeCache { // calling localtime is expensive af int tm_year {0}; int tm_yday {0}; int tm_mon {0}; int tm_mday {0}; int tm_hour {0}; int tm_min {0}; }; } // Components namespace Context { // TODO: move back to chat log window and keep per window instead of per contact struct CGView { // set to the ts of the newest rendered msg Message3Handle begin{}; // set to the ts of the oldest rendered msg Message3Handle end{}; }; } // Context static constexpr float lerp(float a, float b, float t) { return a + t * (b - a); } static std::string file_path_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(c)) || // more normal c == '/' // special bc its a file:// ) { escaped << c; } else { escaped << std::uppercase << '%' << std::setw(2) << static_cast((static_cast(c))) << std::nouppercase ; } } return escaped.str(); } static std::string file_path_to_file_url(const std::filesystem::path& path) { const auto can_path = std::filesystem::canonical(path); std::string url {"file://"}; // special windows detection <.< // we detect a drive letter here if (can_path.has_root_name() && can_path.root_name().generic_u8string().back() == ':') { const std::string root_name = can_path.root_name().generic_u8string(); // drive letters have a colon, which needs skipping the url escaping url += "/"; url += root_name; //url += "/"; // bugged, does not work (but it should, open msvc stl issue?) //url += file_path_url_escape(can_path.lexically_proximate(can_path.root_name()).generic_u8string()); // remove drive letter url += file_path_url_escape(can_path.generic_u8string().substr(root_name.size())); } else { url += file_path_url_escape(can_path.generic_u8string()); } return url; } const void* clipboard_callback(void* userdata, const char* mime_type, size_t* size) { if (mime_type == nullptr) { // cleared or new data is set return nullptr; } if (userdata == nullptr) { // error return nullptr; } auto* cg = static_cast(userdata); std::lock_guard lg{cg->_set_clipboard_data_mutex}; if (!cg->_set_clipboard_data.count(mime_type)) { return nullptr; } const auto& sh_vec = cg->_set_clipboard_data.at(mime_type); if (!static_cast(sh_vec)) { // error, empty shared pointer return nullptr; } *size = sh_vec->size(); return sh_vec->data(); } void ChatGui4::setClipboardData(std::vector mime_types, std::shared_ptr>&& data) { if (!static_cast(data)) { std::cerr << "CG error: tried to set clipboard with empty shp\n"; return; } if (data->empty()) { std::cerr << "CG error: tried to set clipboard with empty data\n"; return; } std::vector tmp_mimetype_list; { std::lock_guard lg{_set_clipboard_data_mutex}; for (const auto& mime_type : mime_types) { tmp_mimetype_list.push_back(mime_type.data()); _set_clipboard_data[mime_type] = data; } // release lock, since on some platforms the callback is called immediatly } SDL_SetClipboardData(clipboard_callback, nullptr, this, tmp_mimetype_list.data(), tmp_mimetype_list.size()); } ChatGui4::ChatGui4( ConfigModelI& conf, ObjectStore2& os, RegistryMessageModelI& rmm, ContactStore4I& cs, TextureUploaderI& tu, ContactTextureCache& contact_tc, MessageTextureCache& msg_tc, Theme& theme ) : _conf(conf), _os(os), _os_sr(_os.newSubRef(this)), _rmm(rmm), _cs(cs), _contact_tc(contact_tc), _msg_tc(msg_tc), _b_tc(_bil, tu), _theme(theme), _sip(tu) { _os_sr.subscribe(ObjectStore_Event::object_update); } ChatGui4::~ChatGui4(void) { // TODO: this is bs SDL_ClearClipboardData(); // this might be better, need to see if this works (docs needs improving) //for (const auto& [k, _] : _set_clipboard_data) { //const auto* tmp_mime_type = k.c_str(); //SDL_SetClipboardData(nullptr, nullptr, nullptr, &tmp_mime_type, 1); //} } float ChatGui4::render(float time_delta, bool window_hidden, bool window_focused) { _fss.render(); _sip.render(time_delta); _b_tc.update(); _b_tc.workLoadQueue(); if (window_hidden) { // annoying, but all of the above needs to continue while not rendering return 1000.f; } 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) { auto& cr = _cs.registry(); const std::string chat_label = "chat " + std::to_string(entt::to_integral(*_selected_contact)); const std::vector* sub_contacts = nullptr; if (cr.all_of(*_selected_contact)) { sub_contacts = &cr.get(*_selected_contact).subs; } const bool highlight_private {!cr.all_of(*_selected_contact)}; if (ImGui::BeginChild(chat_label.c_str(), {0, 0}, ImGuiChildFlags_Border, ImGuiWindowFlags_MenuBar)) { if (ImGui::BeginMenuBar()) { // check if contact has voip model // use activesessioncomp instead? if (cr.all_of(*_selected_contact)) { if (ImGui::BeginMenu("VoIP")) { auto* voip_model = cr.get(*_selected_contact); std::vector contact_sessions; std::vector acceptable_sessions; for (const auto& [ov, o_vm, sc] : _os.registry().view().each()) { if (o_vm != voip_model) { continue; } if (sc.c != *_selected_contact) { continue; } auto o = _os.objectHandle(ov); contact_sessions.push_back(o); if (!o.all_of()) { continue; // not incoming } // state is ringing/not yet accepted const auto* session_state = o.try_get(); if (session_state == nullptr) { continue; } if (session_state->state != Components::VoIP::SessionState::State::RINGING) { continue; } acceptable_sessions.push_back(o); } static Components::VoIP::DefaultConfig g_default_connections{ true, true, true, false }; if (ImGui::BeginMenu("default connections")) { ImGui::MenuItem("incoming audio", nullptr, &g_default_connections.incoming_audio); ImGui::MenuItem("incoming video", nullptr, &g_default_connections.incoming_video); ImGui::Separator(); ImGui::MenuItem("outgoing audio", nullptr, &g_default_connections.outgoing_audio); ImGui::MenuItem("outgoing video", nullptr, &g_default_connections.outgoing_video); ImGui::EndMenu(); } if (acceptable_sessions.size() < 2) { if (ImGui::MenuItem("accept call", nullptr, false, !acceptable_sessions.empty())) { voip_model->accept(acceptable_sessions.front(), g_default_connections); } } else { if (ImGui::BeginMenu("accept call", !acceptable_sessions.empty())) { for (const auto o : acceptable_sessions) { std::string label = "accept #"; label += std::to_string(entt::to_integral(entt::to_entity(o.entity()))); if (ImGui::MenuItem(label.c_str())) { voip_model->accept(o, g_default_connections); } } ImGui::EndMenu(); } } // TODO: disable if already in call? if (ImGui::Button(" call ")) { voip_model->enter(*_selected_contact, g_default_connections); } if (contact_sessions.size() < 2) { if (ImGui::MenuItem("leave/reject call", nullptr, false, !contact_sessions.empty())) { voip_model->leave(contact_sessions.front()); } } else { if (ImGui::BeginMenu("leave/reject call")) { // list for (const auto o : contact_sessions) { std::string label = "end #"; label += std::to_string(entt::to_integral(entt::to_entity(o.entity()))); if (ImGui::MenuItem(label.c_str())) { voip_model->leave(o); } } ImGui::EndMenu(); } } ImGui::EndMenu(); } } if (ImGui::BeginMenu("debug")) { ImGui::Checkbox("show extra info", &_show_chat_extra_info); ImGui::Checkbox("show avatar transfers", &_show_chat_avatar_tf); ImGui::SeparatorText("tox"); // TODO: cheese it and rename to copy id? if (cr.all_of(*_selected_contact)) { if (ImGui::MenuItem("copy ngc chatid")) { const auto& chat_id = cr.get(*_selected_contact).chat_id.data; const auto chat_id_str = bin2hex(std::vector{chat_id.begin(), chat_id.end()}); ImGui::SetClipboardText(chat_id_str.c_str()); } } ImGui::EndMenu(); } ImGui::EndMenuBar(); } renderContactBig(_theme, _contact_tc, {cr, *_selected_contact}, 3, false, false, false); ImGui::Separator(); if (sub_contacts != nullptr && !cr.all_of(*_selected_contact) && cr.all_of(*_selected_contact)) { if (!sub_contacts->empty()) { if (ImGui::BeginChild("subcontacts", {TEXT_BASE_WIDTH * 18.f, -100.f}, true)) { ImGui::Text("subs: %zu", sub_contacts->size()); ImGui::Separator(); for (const auto& c : *sub_contacts) { // TODO: can a sub be selected? no //if (renderSubContactListContact(c, _selected_contact.has_value() && *_selected_contact == c)) { if (renderContactBig(_theme, _contact_tc, {cr, c}, 1)) { _text_input_buffer.insert(0, (cr.all_of(c) ? cr.get(c).name : "") + ": "); } } } ImGui::EndChild(); ImGui::SameLine(); } } const bool request_incoming = cr.all_of(*_selected_contact); const bool request_outgoing = cr.all_of(*_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(*_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 { //ImGui::TextUnformatted(""); ImGui::Dummy({0, TEXT_BASE_HEIGHT}); } static std::string password; if (ri.password) { ImGui::InputText("password to join with", &password); } else { ////ImGui::TextUnformatted(""); ImGui::Dummy({0, TEXT_BASE_HEIGHT}); } if (ImGui::Button("Accept")) { cr.get(*_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(); } if (ImGui::BeginChild("message_log", {0, -100}, ImGuiChildFlags_None)) { // TODO: background image? //auto p_min = ImGui::GetCursorScreenPos(); //auto a_max = ImGui::GetContentRegionAvail(); //ImGui::GetWindowDrawList()->AddImage(0, p_min, {p_min.x+a_max.x, p_min.y+a_max.y}); auto* msg_reg_ptr = _rmm.get(*_selected_contact); constexpr ImGuiTableFlags table_flags = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit ; if (msg_reg_ptr != nullptr && ImGui::BeginTable("chat_table", 5, table_flags)) { ImGui::TableSetupColumn("name", 0, TEXT_BASE_WIDTH * 16.f); ImGui::TableSetupColumn("message", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("delivered/read"); ImGui::TableSetupColumn("timestamp"); ImGui::TableSetupColumn("extra_info", _show_chat_extra_info ? ImGuiTableColumnFlags_None : ImGuiTableColumnFlags_Disabled); Message3Handle message_view_oldest; // oldest visible message Message3Handle message_view_newest; // last visible message // 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; // do systems TODO: extract if (window_focused) { // fade system std::vector to_remove; msg_reg.view().each([&to_remove, time_delta](const Message3 e, Components::UnreadFade& fade) { // TODO: configurable const float fade_duration = 5.f; fade.fade -= 1.f/fade_duration * std::min(time_delta, 1.f/10.f); // fps but not below 10 for smooth-ish fade if (fade.fade <= 0.f) { to_remove.push_back(e); } }); msg_reg.remove(to_remove.cbegin(), to_remove.cend()); } //auto tmp_view = msg_reg.view(); //tmp_view.use(); //tmp_view.each([&](const Message3 e, Message::Components::ContactFrom& c_from, Message::Components::ContactTo& c_to, Message::Components::Timestamp ts //) { //uint64_t prev_ts {0}; Components::ConvertedTimeCache prev_time {}; auto tmp_view = msg_reg.view(); 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(e)) { continue; } Message::Components::ContactFrom& c_from = msg_reg.get(e); Message::Components::ContactTo& c_to = msg_reg.get(e); Message::Components::Timestamp ts = tmp_view.get(e); // TODO: why? ImGui::TableNextRow(0, TEXT_BASE_HEIGHT); if (msg_reg.all_of(e)) { // check if date changed // TODO: move conversion up? const auto& next_time = msg_reg.get(e); if ( prev_time.tm_yday != next_time.tm_yday || prev_time.tm_year != next_time.tm_year // making sure ) { // name if (ImGui::TableNextColumn()) { //ImGui::TextDisabled("---"); } // msg if (ImGui::TableNextColumn()) { ImGui::TextDisabled("DATE CHANGED from %d.%d.%d to %d.%d.%d", 1900+prev_time.tm_year, 1+prev_time.tm_mon, prev_time.tm_mday, 1900+next_time.tm_year, 1+next_time.tm_mon, next_time.tm_mday ); } ImGui::TableNextRow(0, TEXT_BASE_HEIGHT); } prev_time = next_time; } ImGui::PushID(entt::to_integral(e)); // name if (ImGui::TableNextColumn()) { const float img_y {TEXT_BASE_HEIGHT - ImGui::GetStyle().FramePadding.y*2}; renderAvatar(_theme, _contact_tc, {cr, c_from.c}, {img_y, img_y}); ImGui::SameLine(0.f, ImGui::GetStyle().ItemSpacing.x*0.5f); if (cr.all_of(c_from.c)) { ImGui::TextUnformatted(cr.get(c_from.c).name.c_str()); } else { ImGui::TextUnformatted(""); } // use username as visibility test if (ImGui::IsItemVisible()) { if (msg_reg.all_of(e)) { if (!msg_reg.all_of(e)) { if (msg_reg.all_of(e)) { // skip fade, we might get here by merging msg_reg.remove(e); } else { // get time now const uint64_t ts_now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); msg_reg.emplace_or_replace(e, ts_now); msg_reg.emplace_or_replace(e, 1.f); } _rmm.throwEventUpdate(msg_reg, e); } else if (window_focused) { // remove unread early, when we focus the window msg_reg.remove(e); _rmm.throwEventUpdate(msg_reg, e); } } // track view if (!static_cast(message_view_oldest)) { message_view_oldest = {msg_reg, e}; message_view_newest = {msg_reg, e}; } else if (static_cast(message_view_newest)) { // update to latest message_view_newest = {msg_reg, e}; } } // highlight self if (cr.any_of(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); } std::optional row_bg; // private group message if (highlight_private && cr.any_of(c_to.c)) { 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(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(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)); } } // content (msgtext/file) ImGui::TableNextColumn(); if (msg_reg.all_of(e)) { renderMessageBodyText(msg_reg, e); } else if (msg_reg.any_of(e)) { renderMessageBodyFile(msg_reg, e); } else { ImGui::TextDisabled("---"); } // remote received and read state if (ImGui::TableNextColumn()) { // TODO: theming for hardcoded values if (!msg_reg.all_of(e)) { // TODO: dedup? ImGui::TextDisabled("_"); } else { const auto& list = msg_reg.get(e).ts; // wrongly assumes contacts never get removed from a group if (sub_contacts != nullptr && list.size() < sub_contacts->size()) { // if partically delivered ImGui::TextColored(ImVec4{0.8f, 0.8f, 0.1f, 0.7f}, "d"); } else { // if fully delivered ImGui::TextColored(ImVec4{0.1f, 0.8f, 0.1f, 0.7f}, "D"); } if (ImGui::BeginItemTooltip()) { std::string synced_by_text {"delivery confirmed by:"}; const int64_t now_ts_s = int64_t(getTimeMS() / 1000u); size_t other_contacts {0}; for (const auto& [c, syned_ts] : list) { if (cr.all_of(c)) { //synced_by_text += "\n sself(!)"; // makes no sense continue; } else if (cr.all_of(c)) { synced_by_text += "\n wself"; // TODO: add name? } else { synced_by_text += "\n >" + (cr.all_of(c) ? cr.get(c).name : ""); } other_contacts += 1; const int64_t seconds_ago = (int64_t(syned_ts / 1000u) - now_ts_s) * -1; synced_by_text += " (" + std::to_string(seconds_ago) + "sec ago)"; } if (other_contacts > 0) { ImGui::Text("%s", synced_by_text.c_str()); } else { ImGui::TextUnformatted("no delivery confirmation"); } ImGui::EndTooltip(); } } ImGui::SameLine(); // TODO: dedup if (msg_reg.all_of(e)) { const auto list = msg_reg.get(e).ts; // wrongly assumes contacts never get removed from a group if (sub_contacts != nullptr && list.size() < sub_contacts->size()) { // if partially read ImGui::TextColored(ImVec4{0.8f, 0.8f, 0.1f, 0.7f}, "r"); } else { // if fully read ImGui::TextColored(ImVec4{0.1f, 0.8f, 0.1f, 0.7f}, "R"); } if (ImGui::BeginItemTooltip()) { std::string synced_by_text {"read confirmed by:"}; const int64_t now_ts_s = int64_t(getTimeMS() / 1000u); for (const auto& [c, syned_ts] : list) { if (cr.all_of(c)) { //synced_by_text += "\n sself(!)"; // makes no sense continue; } else if (cr.all_of(c)) { synced_by_text += "\n wself"; } else { synced_by_text += "\n >" + (cr.all_of(c) ? cr.get(c).name : ""); } 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("_"); } } // ts if (ImGui::TableNextColumn()) { if (!msg_reg.all_of(e)) { auto time = std::chrono::system_clock::to_time_t( std::chrono::time_point{std::chrono::milliseconds{ts.ts}} ); auto localtime = std::localtime(&time); msg_reg.emplace( e, localtime->tm_year, localtime->tm_yday, localtime->tm_mon, localtime->tm_mday, localtime->tm_hour, localtime->tm_min ); } const auto& ctc = msg_reg.get(e); ImGui::Text("%.2d:%.2d", ctc.tm_hour, ctc.tm_min); } // extra if (ImGui::TableNextColumn()) { renderMessageExtra(msg_reg, e); } ImGui::PopID(); // ent } // fake empty placeholders //ImGui::TableNextRow(0, TEXT_BASE_HEIGHT); //ImGui::TableNextRow(0, TEXT_BASE_HEIGHT); { // update view cursers if (!msg_reg.ctx().contains()) { msg_reg.ctx().emplace(); } auto& cg_view = msg_reg.ctx().get(); // any message in view if (!static_cast(message_view_oldest)) { // no message in view, we setup a view at current time, so the next frags are loaded if (!static_cast(cg_view.begin) || !static_cast(cg_view.end)) { // fix invalid state if (static_cast(cg_view.begin)) { cg_view.begin.destroy(); _rmm.throwEventDestroy(cg_view.begin); } if (static_cast(cg_view.end)) { cg_view.end.destroy(); _rmm.throwEventDestroy(cg_view.end); } // create new cg_view.begin = {msg_reg, msg_reg.create()}; cg_view.end = {msg_reg, msg_reg.create()}; cg_view.begin.emplace_or_replace(cg_view.end); cg_view.end.emplace_or_replace(cg_view.begin); cg_view.begin.get_or_emplace().ts = getTimeMS(); cg_view.end.get_or_emplace().ts = getTimeMS(); std::cout << "CG: created view FRONT begin ts\n"; _rmm.throwEventConstruct(cg_view.begin); std::cout << "CG: created view FRONT end ts\n"; _rmm.throwEventConstruct(cg_view.end); } // else? we do nothing? } else { bool begincreated {false}; if (!static_cast(cg_view.begin)) { cg_view.begin = {msg_reg, msg_reg.create()}; begincreated = true; } bool endcreated {false}; if (!static_cast(cg_view.end)) { cg_view.end = {msg_reg, msg_reg.create()}; endcreated = true; } cg_view.begin.emplace_or_replace(cg_view.end); cg_view.end.emplace_or_replace(cg_view.begin); { auto& old_begin_ts = cg_view.begin.get_or_emplace().ts; if (old_begin_ts != message_view_newest.get().ts) { old_begin_ts = message_view_newest.get().ts; if (begincreated) { std::cout << "CG: created view begin ts with " << old_begin_ts << "\n"; _rmm.throwEventConstruct(cg_view.begin); } else { //std::cout << "CG: updated view begin ts to " << old_begin_ts << "\n"; _rmm.throwEventUpdate(cg_view.begin); } } } { auto& old_end_ts = cg_view.end.get_or_emplace().ts; if (old_end_ts != message_view_oldest.get().ts) { old_end_ts = message_view_oldest.get().ts; if (endcreated) { std::cout << "CG: created view end ts with " << old_end_ts << "\n"; _rmm.throwEventConstruct(cg_view.end); } else { //std::cout << "CG: updated view end ts to " << old_end_ts << "\n"; _rmm.throwEventUpdate(cg_view.end); } } } } } ImGui::EndTable(); } if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { ImGui::SetScrollHereY(1.f); } } ImGui::EndChild(); if (ImGui::BeginChild("text_input", {-150, 0})) { constexpr ImGuiInputTextFlags input_flags = //ImGuiInputTextFlags_AllowTabInput | ImGuiInputTextFlags_NoHorizontalScroll | ImGuiInputTextFlags_CallbackCharFilter; bool text_input_validate {false}; ImGui::InputTextMultiline( "##text_input", &_text_input_buffer, {-0.001f, -0.001f}, input_flags, +[](ImGuiInputTextCallbackData* data) -> int { // ignore unrelated callbacks if ((data->EventFlag & ImGuiInputTextFlags_CallbackCharFilter) == 0) { return 0; } // we let everything through, except enter without shift, in which case we signal outside if ( data->EventChar == '\n' && !ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_Enter) // also needs to be a key press, not a paste ) { *reinterpret_cast(data->UserData) = true; return 1; } return 0; }, &text_input_validate ); if (text_input_validate) { _rmm.sendText(*_selected_contact, _text_input_buffer); _text_input_buffer = ""; if (ImGuiInputTextState* input_state = ImGui::GetInputTextState(ImGui::GetItemID())) { //input_state->ReloadUserBufAndSelectAll(); input_state->ReloadUserBufAndMoveToEnd(); } ImGui::SetKeyboardFocusHere(-1); } // welcome to linux if (ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) { if (!ImGui::IsItemFocused()) { ImGui::SetKeyboardFocusHere(-1); } const char* primary_text = SDL_GetPrimarySelectionText(); if (primary_text != nullptr) { ImGui::GetIO().AddInputCharactersUTF8(primary_text); } } } 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){ _rmm.sendFilePath(*_selected_contact, path.filename().generic_u8string(), path.generic_u8string()); }, [](){} ); } // 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})) { if (const auto* imt = clipboardHasImage(); imt != nullptr) { // making sure pasteFile(imt); } else if (const auto* fpmt = clipboardHasFileList(); fpmt != nullptr) { pasteFile(fpmt); } } else if (ImGui::BeginPopupContextItem(nullptr, ImGuiMouseButton_Right)) { // TODO: use list instead const static std::vector image_mime_types { // add apng? "image/png", "image/webp", "image/gif", "image/jpeg", "image/bmp", "image/qoi", }; for (const char* mime_type : image_mime_types) { if (ImGui::MenuItem(mime_type)) { pasteFile(mime_type); } } ImGui::EndPopup(); } //ImGui::EndDisabled(); } ImGui::EndChild(); #if 0 // if preview window not open? if (ImGui::IsKeyPressed(ImGuiKey_V) && ImGui::IsKeyPressed(ImGuiMod_Shortcut, false)) { std::cout << "CG: paste?\n"; 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 std::cout << "CG: pasted image of size " << data_size << " mime " << mime_type << "\n"; } } #endif } ImGui::EndChild(); } } ImGui::End(); return 1000.f; // TODO: higher min fps? } void ChatGui4::sendFilePath(std::string_view file_path) { const auto path = std::filesystem::path(file_path); if (_selected_contact && std::filesystem::is_regular_file(path)) { _rmm.sendFilePath(*_selected_contact, path.filename().generic_u8string(), path.generic_u8string()); } } // has MessageText void ChatGui4::renderMessageBodyText(Message3Registry& reg, const Message3 e) { const auto& msgtext = reg.get(e).text; #if 0 // TODO: set word wrap 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( "##text", const_cast(msgtext.c_str()), // ugly const cast msgtext.size() + 1, // needs to include '\0' text_size, ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_NoHorizontalScroll ); if (ImGui::BeginPopupContextItem("##text")) { if (ImGui::MenuItem("quote")) { //text_buffer.insert(0, (cr.all_of(c) ? cr.get(c).name : "") + ": "); 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 += "> "; } } } if (ImGui::MenuItem("copy")) { ImGui::SetClipboardText(msgtext.c_str()); } ImGui::EndPopup(); } ImGui::PopStyleColor(); ImGui::PopStyleVar(); #else ImGui::PushTextWrapPos(0.0f); std::string_view msgtext_sv{msgtext}; size_t pos_prev {0}; size_t pos_next {msgtext_sv.find_first_of('\n')}; ImGui::BeginGroup(); do { const auto current_line = msgtext_sv.substr(pos_prev, pos_next - pos_prev); if (!current_line.empty() && current_line.front() == '>') { // TODO: theming ImGui::PushStyleColor(ImGuiCol_Text, {0.3f, 0.9f, 0.1f, 1.f}); ImGui::TextUnformatted(current_line.data(), current_line.data()+current_line.size()); ImGui::PopStyleColor(); } else { ImGui::TextUnformatted(current_line.data(), current_line.data()+current_line.size()); } if (pos_next != msgtext_sv.npos) { pos_next += 1; // skip past if (pos_next < msgtext_sv.size()) { pos_prev = pos_next; // old end is new start pos_next = msgtext_sv.find_first_of('\n', pos_next); } else { pos_prev = msgtext_sv.npos; pos_next = msgtext_sv.npos; } } else { pos_prev = msgtext_sv.npos; } } while (pos_prev != msgtext_sv.npos); ImGui::EndGroup(); ImGui::PopTextWrapPos(); if (ImGui::BeginPopupContextItem("##text")) { if (ImGui::MenuItem("quote")) { //text_buffer.insert(0, (cr.all_of(c) ? cr.get(c).name : "") + ": "); 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 += "> "; } } } if (ImGui::MenuItem("copy")) { ImGui::SetClipboardText(msgtext.c_str()); } ImGui::EndPopup(); } #endif } void ChatGui4::renderMessageBodyFile(Message3Registry& reg, const Message3 e) { auto o = reg.get(e).o; if (!o) { ImGui::TextDisabled("file message missing file object!"); return; } if ( !_show_chat_avatar_tf && ( o.all_of() && o.get().kind == 1 ) ) { // TODO: this looks ugly ImGui::TextDisabled("set avatar"); return; } const bool local_have_all = o.all_of(); ImGui::BeginGroup(); #if 0 if (msg_reg.all_of(e)) { switch (msg_reg.get(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 (o.all_of()) { ImGui::TextUnformatted("paused"); //} else if (reg.all_of(e)) { // ImGui::TextUnformatted("done"); } else { // TODO: missing other states ImGui::TextUnformatted("running"); } if (local_have_all) { ImGui::SameLine(); ImGui::TextUnformatted("(have all)"); } // if in offered state // paused, never started if ( !local_have_all && //reg.all_of(e) && o.all_of() && // TODO: how does restarting a broken/incomplete transfer look like? !o.all_of() && !o.all_of() ) { if (ImGui::Button("save to")) { _fss.requestFile( [](std::filesystem::path& path) -> bool { // remove file path path.remove_filename(); return std::filesystem::is_directory(path); }, [this, o](const auto& path) { if (static_cast(o)) { // still valid // TODO: trim file? o.emplace(path.generic_u8string()); //_rmm.throwEventUpdate(reg, e); // TODO: block recursion _os.throwEventUpdate(o); } }, [](){} ); } } // hacky const auto* fts = o.try_get(); if (fts != nullptr && o.any_of()) { const bool upload = local_have_all && fts->total_down <= 0; const int64_t total_size = o.all_of() ? o.get().file_size : o.get().total_size ; int64_t transfer_total {0u}; float transfer_rate {0.f}; if (upload) { // if have all AND no dl -> show upload progress ImGui::TextUnformatted(" up"); transfer_total = fts->total_up; transfer_rate = fts->rate_up; } else { // else show download progress ImGui::TextUnformatted("down"); transfer_total = fts->total_down; transfer_rate = fts->rate_down; } ImGui::SameLine(); float fraction{0.f}; if (total_size > 0) { fraction = float(transfer_total) / total_size; } else if (o.all_of()) { fraction = 1.f; } char overlay_buf[128]; if (transfer_rate > 0.000001f) { const char* byte_suffix = "???"; int64_t byte_divider = sizeToHumanReadable(transfer_rate, byte_suffix); int64_t ms_remaining = (total_size - transfer_total) / (transfer_rate/1000.f); if (ms_remaining > 0) { const char* duration_suffix = "???"; int64_t duration_divider = durationToHumanReadable(ms_remaining, duration_suffix); std::snprintf( overlay_buf, sizeof(overlay_buf), "%.1f%% @ %.1f%s/s %.1f%s", fraction * 100 + 0.01f, transfer_rate/byte_divider, byte_suffix, double(ms_remaining)/duration_divider, duration_suffix ); } else { std::snprintf(overlay_buf, sizeof(overlay_buf), "%.1f%% @ %.1f%s/s", fraction * 100 + 0.01f, transfer_rate/byte_divider, byte_suffix); } } else { std::snprintf(overlay_buf, sizeof(overlay_buf), "%.1f%%", fraction * 100 + 0.01f); } if (local_have_all) { ImGui::PushStyleColor(ImGuiCol_PlotHistogram, _theme.getColor()); } if ( (!upload && !local_have_all && o.all_of()) || (upload && o.all_of()) ) { ImGui::BeginGroup(); // TODO: hights are all off ImGui::ProgressBar( fraction, {-FLT_MIN, TEXT_BASE_HEIGHT*0.66f}, overlay_buf ); ImVec2 orig_curser_pos = ImGui::GetCursorPos(); const ImVec2 bar_size{ImGui::GetContentRegionAvail().x, TEXT_BASE_HEIGHT*0.15f}; // deploy dummy and check visibility ImGui::Dummy(bar_size); if (ImGui::IsItemVisible()) { ImGui::SetCursorPos(orig_curser_pos); // reset before dummy auto const cursor_start_vec = ImGui::GetCursorScreenPos(); // TODO: replace with own version, so we dont have to internal ImGui::RenderFrame( cursor_start_vec, { cursor_start_vec.x + bar_size.x, cursor_start_vec.y + bar_size.y }, ImGui::GetColorU32(ImGuiCol_FrameBg), false ); auto [id, img_width, img_height] = _b_tc.get(o); ImGui::Image( id, bar_size, {0.f, 0.f}, // default {1.f, 1.f}, // default ImGui::GetStyleColorVec4(ImGuiCol_PlotHistogram) ); } ImGui::EndGroup(); } else { ImGui::ProgressBar( fraction, {-FLT_MIN, TEXT_BASE_HEIGHT}, overlay_buf ); } if (local_have_all) { ImGui::PopStyleColor(); } } else { // infinite scrolling progressbar fallback ImGui::TextUnformatted(" ??"); ImGui::SameLine(); ImGui::ProgressBar( -0.333f * ImGui::GetTime(), {-FLT_MIN, TEXT_BASE_HEIGHT}, "?%" ); } if (o.all_of()) { const auto& frame_dims = o.get(); // TODO: config const auto max_inline_height = 10*TEXT_BASE_HEIGHT; float width = frame_dims.w; float height = frame_dims.h; if (height > max_inline_height) { const float scale = max_inline_height / height; height = max_inline_height; width *= scale; } ImVec2 orig_curser_pos = ImGui::GetCursorPos(); // deploy dummy of framedim size and check visibility // +2 for border ImGui::Dummy(ImVec2{width+2, height+2}); if (ImGui::IsItemVisible() && o.all_of()) { ImGui::SetCursorPos(orig_curser_pos); // reset for actual img auto [id, img_width, img_height] = _msg_tc.get(Message3Handle{reg, e}); // if cache gives 0s, fall back to frame dims (eg if pic not loaded yet) //if (img_width == 0 || img_height == 0) { //width = frame_dims.width; //height = frame_dims.height; //} //if (height > max_inline_height) { //const float scale = max_inline_height / height; //height = max_inline_height; //width *= scale; //} ImGui::Image( id, ImVec2{width, height}, {0.f, 0.f}, // default {1.f, 1.f}, // default {1.f, 1.f, 1.f, 1.f}, // default {0.5f, 0.5f, 0.5f, 0.8f} // border ); // TODO: clickable to open in internal image viewer } } else if (o.all_of()) { // only show info if not inlined image // just filename const auto& si = o.get(); const char* byte_suffix = "???"; int64_t byte_divider = sizeToHumanReadable(si.file_size, byte_suffix); ImGui::Text("%s (%.2lf %s)", si.file_name.c_str(), double(si.file_size)/byte_divider, byte_suffix); } else if (o.all_of()) { // same old bulletpoint list const auto& file_list = o.get().file_list; // if has local, display save base path?, do we have base save path? for (size_t i = 0; i < file_list.size(); i++) { ImGui::PushID(i); const char* byte_suffix = "???"; int64_t byte_divider = sizeToHumanReadable(file_list[i].file_size, byte_suffix); // TODO: selectable text widget ? ImGui::Bullet(); ImGui::Text("%s (%.2lf %s)", file_list[i].file_name.c_str(), double(file_list[i].file_size)/byte_divider, byte_suffix); if (o.all_of()) { const auto& local_info = o.get(); if (local_info.file_list.size() > i && ImGui::BeginPopupContextItem("##file_c")) { if (ImGui::MenuItem("open")) { const std::string url {file_path_to_file_url(local_info.file_list.at(i).file_path)}; std::cout << "opening file '" << url << "'\n"; SDL_OpenURL(url.c_str()); } if (ImGui::MenuItem("copy file")) { const std::string url {file_path_to_file_url(local_info.file_list.at(i).file_path)}; //ImGui::SetClipboardText(url.c_str()); setClipboardData({"text/uri-list", "text/x-moz-url"}, std::make_shared>(url.begin(), url.end())); } if (ImGui::MenuItem("copy filepath")) { const auto file_path = std::filesystem::canonical(local_info.file_list.at(i).file_path).u8string(); //TODO: use generic over native? ImGui::SetClipboardText(file_path.c_str()); } ImGui::EndPopup(); } } ImGui::PopID(); } } else { ImGui::TextDisabled("neither info available"); } ImGui::EndGroup(); if (o.all_of()) { const auto& local_info = o.get(); if (!local_info.file_path.empty() && ImGui::BeginPopupContextItem("##file_c")) { if (ImGui::MenuItem("open")) { const std::string url {file_path_to_file_url(local_info.file_path)}; std::cout << "opening file '" << url << "'\n"; SDL_OpenURL(url.c_str()); } ImGui::Separator(); // TODO: better way to dif up/down if (!local_have_all) { if (ImGui::BeginMenu("dowload priority")) { using Priority = ObjComp::Ephemeral::File::DownloadPriority::Priority; auto& p_comp = o.get_or_emplace(); bool updated {false}; if (ImGui::MenuItem("highest", nullptr, p_comp.p == Priority::HIGHEST)) { p_comp.p = Priority::HIGHEST; updated = true; } if (ImGui::MenuItem("high", nullptr, p_comp.p == Priority::HIGH)) { p_comp.p = Priority::HIGH; updated = true; } if (ImGui::MenuItem("normal", nullptr, p_comp.p == Priority::NORMAL)) { p_comp.p = Priority::NORMAL; updated = true; } if (ImGui::MenuItem("low", nullptr, p_comp.p == Priority::LOW)) { p_comp.p = Priority::LOW; updated = true; } if (ImGui::MenuItem("lowest", nullptr, p_comp.p == Priority::LOWEST)) { p_comp.p = Priority::LOWEST; updated = true; } if (updated) { std::cout << "CG: updated download priority to " << int(p_comp.p) << "\n"; // TODO: dont do it here _os.throwEventUpdate(o); } ImGui::EndMenu(); } ImGui::Separator(); } if (ImGui::BeginMenu("forward", local_have_all)) { auto& cr = _cs.registry(); for (const auto& c : cr.view()) { // filter if (cr.any_of(c)) { continue; } // TODO: check for contact capability // or just error popup?/noti/toast if (renderContactBig(_theme, _contact_tc, {cr, c}, 1, false, true, false)) { // TODO: try object interface first instead, then fall back to send with SingleInfoLocal //_rmm.sendFileObj(c, o); std::filesystem::path path = o.get().file_path; _rmm.sendFilePath(c, path.filename().generic_u8string(), path.generic_u8string()); } } ImGui::EndMenu(); } ImGui::Separator(); if (ImGui::MenuItem("copy file")) { const std::string url {file_path_to_file_url(local_info.file_path)}; //ImGui::SetClipboardText(url.c_str()); setClipboardData({"text/uri-list", "text/x-moz-url"}, std::make_shared>(url.begin(), url.end())); } if (ImGui::MenuItem("copy filepath")) { const auto file_path = std::filesystem::canonical(local_info.file_path).u8string(); //TODO: use generic over native? ImGui::SetClipboardText(file_path.c_str()); } ImGui::EndPopup(); } } // TODO: how to collections? if (ImGui::BeginItemTooltip()) { if (o.all_of()) { ImGui::SeparatorText("single info"); const auto& si = o.get(); ImGui::Text("file name: '%s'", si.file_name.c_str()); const char* byte_suffix = "???"; int64_t byte_divider = sizeToHumanReadable(si.file_size, byte_suffix); ImGui::Text("file size: %.2lf %s (%lu Bytes)", double(si.file_size)/byte_divider, byte_suffix, si.file_size); if (o.all_of()) { ImGui::Text("local path: '%s'", o.get().file_path.c_str()); } } else if (o.all_of()) { ImGui::SeparatorText("collection info"); const auto& ci = o.get(); const char* byte_suffix = "???"; int64_t byte_divider = sizeToHumanReadable(ci.total_size, byte_suffix); ImGui::Text("total size: %.2lf %s (%lu Bytes)", double(ci.total_size)/byte_divider, byte_suffix, ci.total_size); } if (fts != nullptr) { ImGui::SeparatorText("transfer stats"); { const char* byte_suffix = "???"; int64_t byte_divider = sizeToHumanReadable(fts->rate_up, byte_suffix); ImGui::Text("rate up : %.2f %s/s", fts->rate_up/byte_divider, byte_suffix); } { const char* byte_suffix = "???"; int64_t byte_divider = sizeToHumanReadable(fts->rate_down, byte_suffix); ImGui::Text("rate down : %.2f %s/s", fts->rate_down/byte_divider, byte_suffix); } { const char* byte_suffix = "???"; int64_t byte_divider = sizeToHumanReadable(fts->total_up, byte_suffix); ImGui::Text("total up : %.3f %s", double(fts->total_up)/byte_divider, byte_suffix); } { const char* byte_suffix = "???"; int64_t byte_divider = sizeToHumanReadable(fts->total_down, byte_suffix); ImGui::Text("total down: %.3f %s", double(fts->total_down)/byte_divider, byte_suffix); } } ImGui::EndTooltip(); } } void ChatGui4::renderMessageExtra(Message3Registry& reg, const Message3 e) { if (reg.all_of(e)) { const auto o = reg.get(e).o; ImGui::TextDisabled("o:%u", entt::to_integral(o.entity())); if (o.all_of()) { ImGui::TextDisabled("fk:%lu", o.get().kind); } if (o.all_of()) { ImGui::TextDisabled("ttf:%u", o.get().transfer_number); } } if (reg.all_of(e)) { ImGui::TextDisabled("msgid:%u", reg.get(e).id); } const auto& cr = _cs.registry(); if (reg.all_of(e)) { std::string synced_by_text {"syncedBy:"}; const int64_t now_ts_s = int64_t(getTimeMS() / 1000u); for (const auto& [c, syned_ts] : reg.get(e).ts) { if (cr.all_of(c)) { synced_by_text += "\n sself"; } else if (cr.all_of(c)) { synced_by_text += "\n wself"; } else { synced_by_text += "\n >" + (cr.all_of(c) ? cr.get(c).name : ""); } 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::TextDisabled("%s", synced_by_text.c_str()); } // TODO: remove? if (reg.all_of(e)) { std::string synced_by_text {"receivedBy:"}; const int64_t now_ts_s = int64_t(getTimeMS() / 1000u); for (const auto& [c, syned_ts] : reg.get(e).ts) { if (cr.all_of(c)) { synced_by_text += "\n sself"; // required (except when synced externally) } else if (cr.all_of(c)) { synced_by_text += "\n wself"; } else { synced_by_text += "\n >" + (cr.all_of(c) ? cr.get(c).name : ""); } 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::TextDisabled("%s", synced_by_text.c_str()); } } void ChatGui4::renderContactList(void) { if (ImGui::BeginChild("contacts", {TEXT_BASE_WIDTH*35, 0})) { auto& cr = _cs.registry(); //for (const auto& c : _cm.getBigContacts()) { for (const auto& c : cr.view()) { const bool selected = _selected_contact.has_value() && *_selected_contact == c; // TODO: is there a better way? // maybe cache mm? bool has_unread = false; if (const auto* mm = _rmm.get(c); mm != nullptr) { if (const auto* unread_storage = mm->storage(); unread_storage != nullptr && !unread_storage->empty()) { has_unread = true; } } if (renderContactBig(_theme, _contact_tc, {cr, c}, 2, has_unread, true, selected)) { _selected_contact = c; } } } ImGui::EndChild(); } bool ChatGui4::renderContactListContactSmall(const Contact4 c, const bool selected) const { std::string label; const auto& cr = _cs.registry(); label += (cr.all_of(c) ? cr.get(c).name.c_str() : ""); label += "###"; label += std::to_string(entt::to_integral(c)); return ImGui::Selectable(label.c_str(), selected); } void ChatGui4::pasteFile(const char* mime_type) { if (!_selected_contact.has_value()) { return; } if (mimeIsImage(mime_type)) { size_t data_size = 0; void* data = SDL_GetClipboardData(mime_type, &data_size); std::cout << "CG: pasted image of size: " << data_size << " mimetype: " << mime_type << "\n"; _sip.sendMemory( static_cast(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(now.time_since_epoch() - std::chrono::duration_cast(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(img_data.data()), img_data.size()); _rmm.sendFilePath(*_selected_contact, tmp_file_name.str(), tmp_file_path.generic_u8string()); }, [](){} ); SDL_free(data); // free data } else if (mimeIsFileList(mime_type)) { size_t data_size = 0; void* data = SDL_GetClipboardData(mime_type, &data_size); std::cout << "CG: pasted file list of size " << data_size << " mime " << mime_type << "\n"; std::vector list; if (mime_type == std::string_view{"text/uri-list"}) { // lines starting with # are comments // every line is a link // line sep is CRLF std::string_view list_body{reinterpret_cast(data), data_size}; size_t start {0}; bool comment {false}; for (size_t i = 0; i < data_size; i++) { if (list_body[i] == '\r' || list_body[i] == '\n') { if (!comment && i - start > 0) { list.push_back(list_body.substr(start, i - start)); } start = i+1; comment = false; } else if (i == start && list_body[i] == '#') { comment = true; } } if (!comment && start+1 < data_size) { list.push_back(list_body.substr(start)); } } else if (mime_type == std::string_view{"text/x-moz-url"}) { assert(false && "implement me"); // every link line is followed by link-title line (for the prev link) // line sep unclear ("text/*" should always be CRLF, but its not explicitly specified) // (does not matter, we can account for both) } // TODO: remove debug log std::cout << "preprocessing:\n"; for (const auto it : list) { std::cout << " >" << it << "\n"; } // now we need to potentially convert file uris to file paths for (auto it = list.begin(); it != list.end();) { constexpr auto size_of_file_uri_prefix = std::string_view{"file://"}.size(); if (it->size() > size_of_file_uri_prefix && it->substr(0, size_of_file_uri_prefix) == std::string_view{"file://"}) { it->remove_prefix(size_of_file_uri_prefix); } std::filesystem::path path(*it); if (!std::filesystem::is_regular_file(path)) { it = list.erase(it); } else { it++; } } std::cout << "postprocessing:\n"; for (const auto it : list) { std::cout << " >" << it << "\n"; } sendFileList(list); SDL_free(data); // free data } } void ChatGui4::sendFileList(const std::vector& list) { // TODO: file collection sip if (list.size() > 1) { for (const auto it : list) { sendFilePath(it); } } else if (list.size() == 1) { const auto path = std::filesystem::path(list.front()); if (std::filesystem::is_regular_file(path)) { if (!_sip.sendFilePath( list.front(), [this](const auto& img_data, const auto file_ext) { // create file name // TODO: only create file if changed or from memory // 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(now.time_since_epoch() - std::chrono::duration_cast(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(img_data.data()), img_data.size()); _rmm.sendFilePath(*_selected_contact, tmp_file_name.str(), tmp_file_path.generic_u8string()); }, [](){} )) { // if sip fails to open the file sendFilePath(list.front()); } } else { // if not file (???) } } } bool ChatGui4::onEvent(const ObjectStore::Events::ObjectUpdate& e) { if (e.e.any_of()) { _b_tc.stale(e.e); } return false; }