Some checks are pending
ContinuousDelivery / linux-ubuntu (push) Waiting to run
ContinuousDelivery / android (map[ndk_abi:arm64-v8a vcpkg_toolkit:arm64-android]) (push) Waiting to run
ContinuousDelivery / android (map[ndk_abi:armeabi-v7a vcpkg_toolkit:arm-neon-android]) (push) Waiting to run
ContinuousDelivery / android (map[ndk_abi:x86_64 vcpkg_toolkit:x64-android]) (push) Waiting to run
ContinuousDelivery / windows (push) Waiting to run
ContinuousDelivery / windows-asan (push) Waiting to run
ContinuousDelivery / dumpsyms (push) Blocked by required conditions
ContinuousDelivery / release (push) Blocked by required conditions
ContinuousIntegration / linux (push) Waiting to run
ContinuousIntegration / android (map[ndk_abi:arm64-v8a vcpkg_toolkit:arm64-android]) (push) Waiting to run
ContinuousIntegration / android (map[ndk_abi:armeabi-v7a vcpkg_toolkit:arm-neon-android]) (push) Waiting to run
ContinuousIntegration / android (map[ndk_abi:x86_64 vcpkg_toolkit:x64-android]) (push) Waiting to run
ContinuousIntegration / macos (push) Waiting to run
ContinuousIntegration / windows (push) Waiting to run
1786 lines
59 KiB
C++
1786 lines
59 KiB
C++
#include "./chat_gui4.hpp"
|
|
|
|
#include <solanaceae/util/utils.hpp>
|
|
|
|
#include <solanaceae/contact/contact_store_i.hpp>
|
|
#include <solanaceae/contact/contact_model4.hpp>
|
|
|
|
#include <solanaceae/message3/components.hpp>
|
|
#include <solanaceae/tox_messages/msg_components.hpp>
|
|
#include <solanaceae/tox_messages/obj_components.hpp>
|
|
#include <solanaceae/object_store/meta_components_file.hpp>
|
|
#include <solanaceae/contact/components.hpp>
|
|
|
|
#include "./frame_streams/voip_model.hpp"
|
|
|
|
// HACK: remove them
|
|
#include <solanaceae/tox_contacts/components.hpp>
|
|
|
|
#include <entt/entity/entity.hpp>
|
|
|
|
#include <imgui/imgui.h>
|
|
#include <imgui/misc/cpp/imgui_stdlib.h>
|
|
#include <imgui/imgui_internal.h>
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
#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 <cctype>
|
|
#include <ctime>
|
|
#include <cstdio>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <variant>
|
|
|
|
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<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();
|
|
}
|
|
|
|
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<ChatGui4*>(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<bool>(sh_vec)) {
|
|
// error, empty shared pointer
|
|
return nullptr;
|
|
}
|
|
|
|
*size = sh_vec->size();
|
|
|
|
return sh_vec->data();
|
|
}
|
|
|
|
void ChatGui4::setClipboardData(std::vector<std::string> mime_types, std::shared_ptr<std::vector<uint8_t>>&& data) {
|
|
if (!static_cast<bool>(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<const char*> 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<Contact4>* sub_contacts = nullptr;
|
|
if (cr.all_of<Contact::Components::ParentOf>(*_selected_contact)) {
|
|
sub_contacts = &cr.get<Contact::Components::ParentOf>(*_selected_contact).subs;
|
|
}
|
|
|
|
const bool highlight_private {!cr.all_of<Contact::Components::TagPrivate>(*_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<VoIPModelI*>(*_selected_contact)) {
|
|
if (ImGui::BeginMenu("VoIP")) {
|
|
auto* voip_model = cr.get<VoIPModelI*>(*_selected_contact);
|
|
|
|
std::vector<ObjectHandle> contact_sessions;
|
|
std::vector<ObjectHandle> acceptable_sessions;
|
|
for (const auto& [ov, o_vm, sc] : _os.registry().view<VoIPModelI*, Components::VoIP::SessionContact>().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<Components::VoIP::Incoming>()) {
|
|
continue; // not incoming
|
|
}
|
|
|
|
// state is ringing/not yet accepted
|
|
const auto* session_state = o.try_get<Components::VoIP::SessionState>();
|
|
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<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());
|
|
}
|
|
}
|
|
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::EndMenuBar();
|
|
}
|
|
|
|
renderContactBig(_theme, _contact_tc, {cr, *_selected_contact}, 3, false, false, false);
|
|
ImGui::Separator();
|
|
|
|
if (sub_contacts != nullptr && !cr.all_of<Contact::Components::TagPrivate>(*_selected_contact) && cr.all_of<Contact::Components::TagGroup>(*_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<Contact::Components::Name>(c) ? cr.get<Contact::Components::Name>(c).name : "<unk>") + ": ");
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
ImGui::SameLine();
|
|
}
|
|
}
|
|
|
|
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 {
|
|
//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<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();
|
|
}
|
|
|
|
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<Message3> to_remove;
|
|
msg_reg.view<Components::UnreadFade>().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<float>(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<Message::Components::TagUnread, Components::UnreadFade>(to_remove.cbegin(), to_remove.cend());
|
|
}
|
|
|
|
//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
|
|
//) {
|
|
//uint64_t prev_ts {0};
|
|
Components::ConvertedTimeCache prev_time {};
|
|
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);
|
|
|
|
|
|
// TODO: why?
|
|
ImGui::TableNextRow(0, TEXT_BASE_HEIGHT);
|
|
|
|
if (msg_reg.all_of<Components::ConvertedTimeCache>(e)) { // check if date changed
|
|
// TODO: move conversion up?
|
|
const auto& next_time = msg_reg.get<Components::ConvertedTimeCache>(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<Contact::Components::Name>(c_from.c)) {
|
|
ImGui::TextUnformatted(cr.get<Contact::Components::Name>(c_from.c).name.c_str());
|
|
} else {
|
|
ImGui::TextUnformatted("<unk>");
|
|
}
|
|
|
|
// use username as visibility test
|
|
if (ImGui::IsItemVisible()) {
|
|
if (msg_reg.all_of<Message::Components::TagUnread>(e)) {
|
|
if (!msg_reg.all_of<Components::UnreadFade>(e)) {
|
|
if (msg_reg.all_of<Message::Components::Read>(e)) {
|
|
// skip fade, we might get here by merging
|
|
msg_reg.remove<Message::Components::TagUnread>(e);
|
|
} else {
|
|
// 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.emplace_or_replace<Components::UnreadFade>(e, 1.f);
|
|
}
|
|
_rmm.throwEventUpdate(msg_reg, e);
|
|
} else if (window_focused) {
|
|
// remove unread early, when we focus the window
|
|
msg_reg.remove<Message::Components::TagUnread>(e);
|
|
_rmm.throwEventUpdate(msg_reg, e);
|
|
}
|
|
}
|
|
|
|
// track view
|
|
if (!static_cast<bool>(message_view_oldest)) {
|
|
message_view_oldest = {msg_reg, e};
|
|
message_view_newest = {msg_reg, e};
|
|
} else if (static_cast<bool>(message_view_newest)) {
|
|
// update to latest
|
|
message_view_newest = {msg_reg, e};
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
std::optional<ImVec4> row_bg;
|
|
|
|
// private group message
|
|
if (highlight_private && cr.any_of<Contact::Components::TagSelfWeak, Contact::Components::TagSelfStrong>(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<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));
|
|
}
|
|
}
|
|
|
|
// 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::MessageFileObject>(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<Message::Components::ReceivedBy>(e)) {
|
|
// TODO: dedup?
|
|
ImGui::TextDisabled("_");
|
|
} else {
|
|
const auto& list = msg_reg.get<Message::Components::ReceivedBy>(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<Contact::Components::TagSelfStrong>(c)) {
|
|
//synced_by_text += "\n sself(!)"; // makes no sense
|
|
continue;
|
|
} else if (cr.all_of<Contact::Components::TagSelfWeak>(c)) {
|
|
synced_by_text += "\n wself"; // TODO: add name?
|
|
} else {
|
|
synced_by_text += "\n >" + (cr.all_of<Contact::Components::Name>(c) ? cr.get<Contact::Components::Name>(c).name : "<unk>");
|
|
}
|
|
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<Message::Components::ReadBy>(e)) {
|
|
const auto list = msg_reg.get<Message::Components::ReadBy>(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<Contact::Components::TagSelfStrong>(c)) {
|
|
//synced_by_text += "\n sself(!)"; // makes no sense
|
|
continue;
|
|
} 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("_");
|
|
}
|
|
}
|
|
|
|
// ts
|
|
if (ImGui::TableNextColumn()) {
|
|
if (!msg_reg.all_of<Components::ConvertedTimeCache>(e)) {
|
|
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);
|
|
msg_reg.emplace<Components::ConvertedTimeCache>(
|
|
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<Components::ConvertedTimeCache>(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<Context::CGView>()) {
|
|
msg_reg.ctx().emplace<Context::CGView>();
|
|
}
|
|
|
|
auto& cg_view = msg_reg.ctx().get<Context::CGView>();
|
|
|
|
// any message in view
|
|
if (!static_cast<bool>(message_view_oldest)) {
|
|
// no message in view, we setup a view at current time, so the next frags are loaded
|
|
if (!static_cast<bool>(cg_view.begin) || !static_cast<bool>(cg_view.end)) {
|
|
// fix invalid state
|
|
if (static_cast<bool>(cg_view.begin)) {
|
|
cg_view.begin.destroy();
|
|
_rmm.throwEventDestroy(cg_view.begin);
|
|
}
|
|
if (static_cast<bool>(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<Message::Components::ViewCurserBegin>(cg_view.end);
|
|
cg_view.end.emplace_or_replace<Message::Components::ViewCurserEnd>(cg_view.begin);
|
|
|
|
cg_view.begin.get_or_emplace<Message::Components::Timestamp>().ts = getTimeMS();
|
|
cg_view.end.get_or_emplace<Message::Components::Timestamp>().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<bool>(cg_view.begin)) {
|
|
cg_view.begin = {msg_reg, msg_reg.create()};
|
|
begincreated = true;
|
|
}
|
|
bool endcreated {false};
|
|
if (!static_cast<bool>(cg_view.end)) {
|
|
cg_view.end = {msg_reg, msg_reg.create()};
|
|
endcreated = true;
|
|
}
|
|
cg_view.begin.emplace_or_replace<Message::Components::ViewCurserBegin>(cg_view.end);
|
|
cg_view.end.emplace_or_replace<Message::Components::ViewCurserEnd>(cg_view.begin);
|
|
|
|
{
|
|
auto& old_begin_ts = cg_view.begin.get_or_emplace<Message::Components::Timestamp>().ts;
|
|
if (old_begin_ts != message_view_newest.get<Message::Components::Timestamp>().ts) {
|
|
old_begin_ts = message_view_newest.get<Message::Components::Timestamp>().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<Message::Components::Timestamp>().ts;
|
|
if (old_end_ts != message_view_oldest.get<Message::Components::Timestamp>().ts) {
|
|
old_end_ts = message_view_oldest.get<Message::Components::Timestamp>().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<bool*>(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<const char*> 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<Message::Components::MessageText>(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<char*>(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<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 += "> ";
|
|
}
|
|
}
|
|
}
|
|
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<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 += "> ";
|
|
}
|
|
}
|
|
}
|
|
if (ImGui::MenuItem("copy")) {
|
|
ImGui::SetClipboardText(msgtext.c_str());
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void ChatGui4::renderMessageBodyFile(Message3Registry& reg, const Message3 e) {
|
|
auto o = reg.get<Message::Components::MessageFileObject>(e).o;
|
|
if (!o) {
|
|
ImGui::TextDisabled("file message missing file object!");
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!_show_chat_avatar_tf
|
|
&& (
|
|
o.all_of<ObjComp::Tox::FileKind>()
|
|
&& o.get<ObjComp::Tox::FileKind>().kind == 1
|
|
)
|
|
) {
|
|
// TODO: this looks ugly
|
|
ImGui::TextDisabled("set avatar");
|
|
return;
|
|
}
|
|
|
|
const bool local_have_all = o.all_of<ObjComp::F::TagLocalHaveAll>();
|
|
|
|
ImGui::BeginGroup();
|
|
|
|
#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 (o.all_of<ObjComp::Ephemeral::File::TagTransferPaused>()) {
|
|
ImGui::TextUnformatted("paused");
|
|
//} else if (reg.all_of<Message::Components::Transfer::TagReceiving, Message::Components::Transfer::TagHaveAll>(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<Message::Components::Transfer::TagReceiving>(e) &&
|
|
o.all_of<ObjComp::Ephemeral::File::TagTransferPaused>() &&
|
|
// TODO: how does restarting a broken/incomplete transfer look like?
|
|
!o.all_of<ObjComp::F::SingleInfoLocal>() &&
|
|
!o.all_of<ObjComp::Ephemeral::File::ActionTransferAccept>()
|
|
) {
|
|
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<bool>(o)) { // still valid
|
|
// TODO: trim file?
|
|
o.emplace<ObjComp::Ephemeral::File::ActionTransferAccept>(path.generic_u8string());
|
|
//_rmm.throwEventUpdate(reg, e);
|
|
// TODO: block recursion
|
|
_os.throwEventUpdate(o);
|
|
}
|
|
},
|
|
[](){}
|
|
);
|
|
}
|
|
}
|
|
|
|
// hacky
|
|
const auto* fts = o.try_get<ObjComp::Ephemeral::File::TransferStats>();
|
|
if (fts != nullptr && o.any_of<ObjComp::F::SingleInfo, ObjComp::F::CollectionInfo>()) {
|
|
const bool upload = local_have_all && fts->total_down <= 0;
|
|
|
|
const int64_t total_size =
|
|
o.all_of<ObjComp::F::SingleInfo>() ?
|
|
o.get<ObjComp::F::SingleInfo>().file_size :
|
|
o.get<ObjComp::F::CollectionInfo>().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<ObjComp::F::TagLocalHaveAll>()) {
|
|
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<ThemeCol_Contact::ft_have_all>());
|
|
}
|
|
if (
|
|
(!upload && !local_have_all && o.all_of<ObjComp::F::LocalHaveBitset>()) ||
|
|
(upload && o.all_of<ObjComp::F::RemoteHaveBitset>())
|
|
) {
|
|
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<ObjComp::F::FrameDims>()) {
|
|
const auto& frame_dims = o.get<ObjComp::F::FrameDims>();
|
|
|
|
// 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<ObjComp::F::TagLocalHaveAll, ObjComp::F::SingleInfo, ObjComp::Ephemeral::BackendFile2>()) {
|
|
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<ObjComp::F::SingleInfo>()) { // only show info if not inlined image
|
|
// just filename
|
|
const auto& si = o.get<ObjComp::F::SingleInfo>();
|
|
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<ObjComp::F::CollectionInfo>()) {
|
|
// same old bulletpoint list
|
|
const auto& file_list = o.get<ObjComp::F::CollectionInfo>().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<ObjComp::F::CollectionInfoLocal>()) {
|
|
const auto& local_info = o.get<ObjComp::F::CollectionInfoLocal>();
|
|
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<std::vector<uint8_t>>(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<ObjComp::F::SingleInfoLocal>()) {
|
|
const auto& local_info = o.get<ObjComp::F::SingleInfoLocal>();
|
|
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<ObjComp::Ephemeral::File::DownloadPriority>();
|
|
|
|
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<Contact::Components::TagBig>()) {
|
|
// filter
|
|
if (cr.any_of<Contact::Components::RequestIncoming, Contact::Components::TagRequestOutgoing>(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<ObjComp::F::SingleInfoLocal>().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<std::vector<uint8_t>>(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<ObjComp::F::SingleInfo>()) {
|
|
ImGui::SeparatorText("single info");
|
|
const auto& si = o.get<ObjComp::F::SingleInfo>();
|
|
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<ObjComp::F::SingleInfoLocal>()) {
|
|
ImGui::Text("local path: '%s'", o.get<ObjComp::F::SingleInfoLocal>().file_path.c_str());
|
|
}
|
|
} else if (o.all_of<ObjComp::F::CollectionInfo>()) {
|
|
ImGui::SeparatorText("collection info");
|
|
const auto& ci = o.get<ObjComp::F::CollectionInfo>();
|
|
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<Message::Components::MessageFileObject>(e)) {
|
|
const auto o = reg.get<Message::Components::MessageFileObject>(e).o;
|
|
|
|
ImGui::TextDisabled("o:%u", entt::to_integral(o.entity()));
|
|
|
|
if (o.all_of<ObjComp::Tox::FileKind>()) {
|
|
ImGui::TextDisabled("fk:%lu", o.get<ObjComp::Tox::FileKind>().kind);
|
|
}
|
|
if (o.all_of<ObjComp::Ephemeral::ToxTransferFriend>()) {
|
|
ImGui::TextDisabled("ttf:%u", o.get<ObjComp::Ephemeral::ToxTransferFriend>().transfer_number);
|
|
}
|
|
}
|
|
|
|
if (reg.all_of<Message::Components::ToxGroupMessageID>(e)) {
|
|
ImGui::TextDisabled("msgid:%u", reg.get<Message::Components::ToxGroupMessageID>(e).id);
|
|
}
|
|
|
|
const auto& cr = _cs.registry();
|
|
|
|
if (reg.all_of<Message::Components::SyncedBy>(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<Message::Components::SyncedBy>(e).ts) {
|
|
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>");
|
|
}
|
|
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<Message::Components::ReceivedBy>(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<Message::Components::ReceivedBy>(e).ts) {
|
|
if (cr.all_of<Contact::Components::TagSelfStrong>(c)) {
|
|
synced_by_text += "\n sself"; // required (except when synced externally)
|
|
} 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::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<Contact::Components::TagBig>()) {
|
|
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<Message::Components::TagUnread>(); 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<Contact::Components::Name>(c) ? cr.get<Contact::Components::Name>(c).name.c_str() : "<unk>");
|
|
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<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.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<std::string_view> 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<const char*>(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<std::string_view>& 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<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.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<ObjComp::F::LocalHaveBitset, ObjComp::F::RemoteHaveBitset>()) {
|
|
_b_tc.stale(e.e);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|