Compare commits

...

4 Commits

9 changed files with 343 additions and 17 deletions

View File

@ -61,6 +61,9 @@ add_executable(tomato
./tox_dht_cap_histo.hpp ./tox_dht_cap_histo.hpp
./tox_dht_cap_histo.cpp ./tox_dht_cap_histo.cpp
./tox_friend_faux_offline_messaging.hpp
./tox_friend_faux_offline_messaging.cpp
./chat_gui4.hpp ./chat_gui4.hpp
./chat_gui4.cpp ./chat_gui4.cpp
) )

View File

@ -17,10 +17,8 @@
#include "./media_meta_info_loader.hpp" #include "./media_meta_info_loader.hpp"
#include "./sdl_clipboard_utils.hpp" #include "./sdl_clipboard_utils.hpp"
#include "SDL_clipboard.h"
#include <cctype> #include <cctype>
#include <cstdint>
#include <ctime> #include <ctime>
#include <cstdio> #include <cstdio>
#include <chrono> #include <chrono>
@ -28,10 +26,7 @@
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
#include <sstream> #include <sstream>
#include <string>
#include <variant> #include <variant>
#include <vector>
namespace Components { namespace Components {
@ -72,6 +67,56 @@ static std::string file_path_url_escape(const std::string&& value) {
return escaped.str(); return escaped.str();
} }
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;
}
SDL_SetClipboardData(clipboard_callback, nullptr, this, tmp_mimetype_list.data(), tmp_mimetype_list.size());
}
ChatGui4::ChatGui4( ChatGui4::ChatGui4(
ConfigModelI& conf, ConfigModelI& conf,
RegistryMessageModel& rmm, RegistryMessageModel& rmm,
@ -80,6 +125,17 @@ ChatGui4::ChatGui4(
) : _conf(conf), _rmm(rmm), _cr(cr), _tal(_cr), _contact_tc(_tal, tu), _msg_tc(_mil, tu), _sip(tu) { ) : _conf(conf), _rmm(rmm), _cr(cr), _tal(_cr), _contact_tc(_tal, tu), _msg_tc(_mil, tu), _sip(tu) {
} }
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);
//}
}
void ChatGui4::render(float time_delta) { void ChatGui4::render(float time_delta) {
if (!_cr.storage<Contact::Components::TagAvatarInvalidate>().empty()) { // handle force-reloads for avatars if (!_cr.storage<Contact::Components::TagAvatarInvalidate>().empty()) { // handle force-reloads for avatars
std::vector<Contact3> to_purge; std::vector<Contact3> to_purge;
@ -514,7 +570,7 @@ void ChatGui4::render(float time_delta) {
_fss.requestFile( _fss.requestFile(
[](const auto& path) -> bool { return std::filesystem::is_regular_file(path); }, [](const auto& path) -> bool { return std::filesystem::is_regular_file(path); },
[this](const auto& path){ [this](const auto& path){
_rmm.sendFilePath(*_selected_contact, path.filename().u8string(), path.u8string()); _rmm.sendFilePath(*_selected_contact, path.filename().generic_u8string(), path.generic_u8string());
}, },
[](){} [](){}
); );
@ -577,7 +633,7 @@ void ChatGui4::render(float time_delta) {
void ChatGui4::sendFilePath(const char* file_path) { void ChatGui4::sendFilePath(const char* file_path) {
if (_selected_contact && std::filesystem::is_regular_file(file_path)) { if (_selected_contact && std::filesystem::is_regular_file(file_path)) {
_rmm.sendFilePath(*_selected_contact, std::filesystem::path(file_path).filename().u8string(), file_path); _rmm.sendFilePath(*_selected_contact, std::filesystem::path(file_path).filename().generic_u8string(), file_path);
} }
} }
@ -671,10 +727,10 @@ void ChatGui4::renderMessageBodyFile(Message3Registry& reg, const Message3 e) {
//for (const auto& c : _cr.view<entt::get_t<Contact::Components::TagBig>, entt::exclude_t<Contact::Components::RequestIncoming, Contact::Components::TagRequestOutgoing>>()) { //for (const auto& c : _cr.view<entt::get_t<Contact::Components::TagBig>, entt::exclude_t<Contact::Components::RequestIncoming, Contact::Components::TagRequestOutgoing>>()) {
for (const auto& c : _cr.view<Contact::Components::TagBig>()) { for (const auto& c : _cr.view<Contact::Components::TagBig>()) {
if (renderContactListContactSmall(c, false)) { if (renderContactListContactSmall(c, false)) {
//_rmm.sendFilePath(*_selected_contact, path.filename().u8string(), path.u8string()); //_rmm.sendFilePath(*_selected_contact, path.filename().generic_u8string(), path.generic_u8string());
const auto& fil = reg.get<Message::Components::Transfer::FileInfoLocal>(e); const auto& fil = reg.get<Message::Components::Transfer::FileInfoLocal>(e);
for (const auto& path : fil.file_list) { for (const auto& path : fil.file_list) {
_rmm.sendFilePath(c, std::filesystem::path{path}.filename().u8string(), path); _rmm.sendFilePath(c, std::filesystem::path{path}.filename().generic_u8string(), path);
} }
} }
} }
@ -761,10 +817,19 @@ void ChatGui4::renderMessageBodyFile(Message3Registry& reg, const Message3 e) {
const auto& local_info = reg.get<Message::Components::Transfer::FileInfoLocal>(e); const auto& local_info = reg.get<Message::Components::Transfer::FileInfoLocal>(e);
if (local_info.file_list.size() > i && ImGui::BeginPopupContextItem("##file_c")) { if (local_info.file_list.size() > i && ImGui::BeginPopupContextItem("##file_c")) {
if (ImGui::MenuItem("open")) { if (ImGui::MenuItem("open")) {
std::string url{"file://" + file_path_url_escape(std::filesystem::canonical(local_info.file_list.at(i)).u8string())}; const std::string url{"file://" + file_path_url_escape(std::filesystem::canonical(local_info.file_list.at(i)).generic_u8string())};
std::cout << "opening file '" << url << "'\n"; std::cout << "opening file '" << url << "'\n";
SDL_OpenURL(url.c_str()); SDL_OpenURL(url.c_str());
} }
if (ImGui::MenuItem("copy file")) {
const std::string url{"file://" + file_path_url_escape(std::filesystem::canonical(local_info.file_list.at(i)).generic_u8string())};
//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)).u8string(); //TODO: use generic over native?
ImGui::SetClipboardText(file_path.c_str());
}
ImGui::EndPopup(); ImGui::EndPopup();
} }
} }
@ -1065,7 +1130,7 @@ void ChatGui4::pasteFile(const char* mime_type) {
std::ofstream(tmp_file_path, std::ios_base::out | std::ios_base::binary) std::ofstream(tmp_file_path, std::ios_base::out | std::ios_base::binary)
.write(reinterpret_cast<const char*>(img_data.data()), img_data.size()); .write(reinterpret_cast<const char*>(img_data.data()), img_data.size());
_rmm.sendFilePath(*_selected_contact, tmp_file_name.str(), tmp_file_path.u8string()); _rmm.sendFilePath(*_selected_contact, tmp_file_name.str(), tmp_file_path.generic_u8string());
}, },
[](){} [](){}
); );

View File

@ -9,9 +9,13 @@
#include "./message_image_loader.hpp" #include "./message_image_loader.hpp"
#include "./file_selector.hpp" #include "./file_selector.hpp"
#include "./send_image_popup.hpp" #include "./send_image_popup.hpp"
#include "entt/container/dense_map.hpp"
#include <cstdint>
#include <vector> #include <vector>
#include <set> #include <string>
#include <mutex>
#include <memory>
class ChatGui4 { class ChatGui4 {
ConfigModelI& _conf; ConfigModelI& _conf;
@ -37,6 +41,12 @@ class ChatGui4 {
float TEXT_BASE_WIDTH {1}; float TEXT_BASE_WIDTH {1};
float TEXT_BASE_HEIGHT {1}; float TEXT_BASE_HEIGHT {1};
// mimetype -> data
entt::dense_map<std::string, std::shared_ptr<std::vector<uint8_t>>> _set_clipboard_data;
std::mutex _set_clipboard_data_mutex; // might be called out of order
friend const void* clipboard_callback(void* userdata, const char* mime_type, size_t* size);
void setClipboardData(std::vector<std::string> mime_types, std::shared_ptr<std::vector<uint8_t>>&& data);
public: public:
ChatGui4( ChatGui4(
ConfigModelI& conf, ConfigModelI& conf,
@ -44,6 +54,7 @@ class ChatGui4 {
Contact3Registry& cr, Contact3Registry& cr,
TextureUploaderI& tu TextureUploaderI& tu
); );
~ChatGui4(void);
public: public:
void render(float time_delta); void render(float time_delta);

View File

@ -47,7 +47,7 @@ void FileSelector::render(void) {
std::filesystem::path current_path = _current_file_path; std::filesystem::path current_path = _current_file_path;
current_path.remove_filename(); current_path.remove_filename();
ImGui::Text("path: %s", _current_file_path.u8string().c_str()); ImGui::Text("path: %s", _current_file_path.generic_u8string().c_str());
// begin table with selectables // begin table with selectables
constexpr ImGuiTableFlags table_flags = constexpr ImGuiTableFlags table_flags =
@ -175,7 +175,7 @@ void FileSelector::render(void) {
} }
if (ImGui::TableNextColumn()) { if (ImGui::TableNextColumn()) {
ImGui::TextUnformatted((dir_entry.path().filename().u8string() + "/").c_str()); ImGui::TextUnformatted((dir_entry.path().filename().generic_u8string() + "/").c_str());
} }
if (ImGui::TableNextColumn()) { if (ImGui::TableNextColumn()) {
@ -206,7 +206,7 @@ void FileSelector::render(void) {
} }
if (ImGui::TableNextColumn()) { if (ImGui::TableNextColumn()) {
ImGui::TextUnformatted(dir_entry.path().filename().u8string().c_str()); ImGui::TextUnformatted(dir_entry.path().filename().generic_u8string().c_str());
} }
if (ImGui::TableNextColumn()) { if (ImGui::TableNextColumn()) {

View File

@ -16,6 +16,7 @@ MainScreen::MainScreen(SDL_Renderer* renderer_, std::string save_path, std::stri
tcm(cr, tc, tc), tcm(cr, tc, tc),
tmm(rmm, cr, tcm, tc, tc), tmm(rmm, cr, tcm, tc, tc),
ttm(rmm, cr, tcm, tc, tc), ttm(rmm, cr, tcm, tc, tc),
tffom(cr, rmm, tcm, tc, tc),
mmil(rmm), mmil(rmm),
tam(rmm, cr, conf), tam(rmm, cr, conf),
sdlrtu(renderer_), sdlrtu(renderer_),
@ -241,6 +242,8 @@ Screen* MainScreen::tick(float time_delta, bool& quit) {
tcm.iterate(time_delta); // compute tcm.iterate(time_delta); // compute
const float fo_interval = tffom.tick(time_delta);
tam.iterate(); // compute tam.iterate(); // compute
const float pm_interval = pm.tick(time_delta); // compute const float pm_interval = pm.tick(time_delta); // compute
@ -253,6 +256,10 @@ Screen* MainScreen::tick(float time_delta, bool& quit) {
tc.toxIterationInterval()/1000.f, tc.toxIterationInterval()/1000.f,
pm_interval pm_interval
); );
_min_tick_interval = std::min<float>(
_min_tick_interval,
fo_interval
);
switch (_compute_perf_mode) { switch (_compute_perf_mode) {
// normal 1ms lower bound // normal 1ms lower bound

View File

@ -25,6 +25,7 @@
#include "./settings_window.hpp" #include "./settings_window.hpp"
#include "./tox_ui_utils.hpp" #include "./tox_ui_utils.hpp"
#include "./tox_dht_cap_histo.hpp" #include "./tox_dht_cap_histo.hpp"
#include "./tox_friend_faux_offline_messaging.hpp"
#include <string> #include <string>
#include <iostream> #include <iostream>
@ -52,6 +53,7 @@ struct MainScreen final : public Screen {
ToxContactModel2 tcm; ToxContactModel2 tcm;
ToxMessageManager tmm; ToxMessageManager tmm;
ToxTransferManager ttm; ToxTransferManager ttm;
ToxFriendFauxOfflineMessaging tffom;
MediaMetaInfoLoader mmil; MediaMetaInfoLoader mmil;
ToxAvatarManager tam; ToxAvatarManager tam;
@ -67,7 +69,7 @@ struct MainScreen final : public Screen {
bool _show_tool_style_editor {false}; bool _show_tool_style_editor {false};
bool _window_hidden {false}; bool _window_hidden {false};
bool _window_hidden_ts {0}; uint64_t _window_hidden_ts {0};
float _time_since_event {0.f}; float _time_since_event {0.f};
MainScreen(SDL_Renderer* renderer_, std::string save_path, std::string save_password, std::vector<std::string> plugins); MainScreen(SDL_Renderer* renderer_, std::string save_path, std::string save_password, std::vector<std::string> plugins);

View File

@ -83,7 +83,7 @@ std::string ToxAvatarManager::getAvatarPath(const ToxKey& key) const {
const std::string_view avatar_save_path {_conf.get_string("ToxAvatarManager", "save_path").value()}; const std::string_view avatar_save_path {_conf.get_string("ToxAvatarManager", "save_path").value()};
const auto pub_key_string = bin2hex({key.data.cbegin(), key.data.cend()}); const auto pub_key_string = bin2hex({key.data.cbegin(), key.data.cend()});
const auto file_path = std::filesystem::path(avatar_save_path) / (pub_key_string + ".png"); const auto file_path = std::filesystem::path(avatar_save_path) / (pub_key_string + ".png");
return file_path.u8string(); return file_path.generic_u8string();
} }
void ToxAvatarManager::addAvatarFileToContact(const Contact3 c, const ToxKey& key) { void ToxAvatarManager::addAvatarFileToContact(const Contact3 c, const ToxKey& key) {

View File

@ -0,0 +1,188 @@
#include "./tox_friend_faux_offline_messaging.hpp"
#include <solanaceae/toxcore/tox_interface.hpp>
#include <solanaceae/contact/components.hpp>
#include <solanaceae/tox_contacts/components.hpp>
#include <solanaceae/message3/components.hpp>
#include <solanaceae/tox_messages/components.hpp>
#include <limits>
#include <cstdint>
namespace Message::Components {
struct LastSendAttempt {
uint64_t ts {0};
};
} // Message::Components
namespace Contact::Components {
struct NextSendAttempt {
uint64_t ts {0};
};
} // Contact::Components
ToxFriendFauxOfflineMessaging::ToxFriendFauxOfflineMessaging(
Contact3Registry& cr,
RegistryMessageModel& rmm,
ToxContactModel2& tcm,
ToxI& t,
ToxEventProviderI& tep
) : _cr(cr), _rmm(rmm), _tcm(tcm), _t(t), _tep(tep) {
}
float ToxFriendFauxOfflineMessaging::tick(float time_delta) {
// hard limit interval to once per minute
_interval_timer += time_delta;
if (_interval_timer < 1.f * 60.f) {
return std::max(60.f - _interval_timer, 0.001f); // TODO: min next timer
}
_interval_timer = 0.f;
const uint64_t ts_now = Message::getTimeMS();
// check ALL
// for each online tox friend
uint64_t min_next_attempt_ts {std::numeric_limits<uint64_t>::max()};
_cr.view<Contact::Components::ToxFriendEphemeral, Contact::Components::ConnectionState>()
.each([this, &min_next_attempt_ts, ts_now](const Contact3 c, const auto& tfe, const auto& cs) {
if (cs.state == Contact::Components::ConnectionState::disconnected) {
// cleanup
if (_cr.all_of<Contact::Components::NextSendAttempt>(c)) {
_cr.remove<Contact::Components::NextSendAttempt>(c);
}
} else {
if (!_cr.all_of<Contact::Components::NextSendAttempt>(c)) {
const auto& nsa = _cr.emplace<Contact::Components::NextSendAttempt>(c, ts_now + uint64_t(_delay_after_cc*1000)); // wait before first message is sent
min_next_attempt_ts = std::min(min_next_attempt_ts, nsa.ts);
} else {
auto& next_attempt = _cr.get<Contact::Components::NextSendAttempt>(c).ts;
if (doFriendMessageCheck(c, tfe)) {
next_attempt = ts_now + uint64_t(_delay_inbetween*1000);
}
min_next_attempt_ts = std::min(min_next_attempt_ts, next_attempt);
}
}
});
if (min_next_attempt_ts <= ts_now) {
// we (probably) sent this iterate
_interval_timer = 60.f - 0.1f; // TODO: ugly magic
return 0.1f;
} else if (min_next_attempt_ts == std::numeric_limits<uint64_t>::max()) {
// nothing to sync or all offline that need syncing
return 60.f; // TODO: ugly magic
} else {
// TODO: ugly magic
return _interval_timer = 60.f - std::min(60.f, (min_next_attempt_ts - ts_now) / 1000.f);
}
}
bool ToxFriendFauxOfflineMessaging::doFriendMessageCheck(const Contact3 c, const Contact::Components::ToxFriendEphemeral& tfe) {
// walk all messages and check if
// unacked message
// timeouts for exising unacked messages expired (send)
auto* mr = static_cast<const RegistryMessageModel&>(_rmm).get(c);
if (mr == nullptr) {
// no messages
return false;
}
const uint64_t ts_now = Message::getTimeMS();
// filter for unconfirmed messages
// we assume sorted
// ("reverse" iteration <.<)
auto msg_view = mr->view<Message::Components::Timestamp>();
// we search for the oldest, not too recently sent, unconfirmed message
for (auto it = msg_view.rbegin(), view_end = msg_view.rend(); it != view_end; it++) {
const Message3 msg = *it;
// require
if (!mr->all_of<
Message::Components::MessageText, // text only for now
Message::Components::ContactTo
>(msg)
) {
continue; // skip
}
// exclude
if (mr->any_of<
Message::Components::Remote::TimestampReceived // this acts like a tag, which is wrong in groups
>(msg)
) {
continue; // skip
}
uint64_t msg_ts = msg_view.get<Message::Components::Timestamp>(msg).ts;
if (mr->all_of<Message::Components::TimestampWritten>(msg)) {
msg_ts = mr->get<Message::Components::TimestampWritten>(msg).ts;
}
if (mr->all_of<Message::Components::LastSendAttempt>(msg)) {
const auto lsa = mr->get<Message::Components::LastSendAttempt>(msg).ts;
if (lsa > msg_ts) {
msg_ts = lsa;
}
}
if (ts_now < (msg_ts + uint64_t(_delay_retry * 1000))) {
// not time yet
continue;
}
// it is time
const auto [msg_id, _] = _t.toxFriendSendMessage(
tfe.friend_number,
(
mr->all_of<Message::Components::TagMessageIsAction>(msg)
? Tox_Message_Type::TOX_MESSAGE_TYPE_ACTION
: Tox_Message_Type::TOX_MESSAGE_TYPE_NORMAL
),
mr->get<Message::Components::MessageText>(msg).text
);
// TODO: this is ugly
mr->emplace_or_replace<Message::Components::LastSendAttempt>(msg, ts_now);
if (msg_id.has_value()) {
// tmm will pick this up for us
mr->emplace_or_replace<Message::Components::ToxFriendMessageID>(msg, msg_id.value());
} // else error
// we sent our message, no point further iterating
return true;
}
// TODO: somehow cleanup lsa
return false;
}
bool ToxFriendFauxOfflineMessaging::onToxEvent(const Tox_Event_Friend_Connection_Status* e) {
const auto friend_number = tox_event_friend_connection_status_get_friend_number(e);
const auto friend_status = tox_event_friend_connection_status_get_connection_status(e);
if (friend_status == Tox_Connection::TOX_CONNECTION_NONE) {
return false; // skip
// maybe cleanup?
}
auto c = _tcm.getContactFriend(friend_number);
if (!static_cast<bool>(c) || !c.all_of<Contact::Components::ToxFriendEphemeral, Contact::Components::ConnectionState>()) {
// UH error??
return false;
}
_cr.emplace_or_replace<Contact::Components::NextSendAttempt>(c, Message::getTimeMS() + uint64_t(_delay_after_cc*1000)); // wait before first message is sent
// TODO: ugly magic
_interval_timer = 60.f - 0.1f;
return false;
}

View File

@ -0,0 +1,50 @@
#pragma once
#include <solanaceae/toxcore/tox_event_interface.hpp>
#include <solanaceae/tox_contacts/tox_contact_model2.hpp>
#include <solanaceae/contact/contact_model3.hpp>
#include <solanaceae/message3/registry_message_model.hpp>
// fwd
struct ToxI;
namespace Contact::Components {
struct ToxFriendEphemeral;
}
// resends unconfirmed messages.
// timers get reset on connection changes, and send order is preserved.
class ToxFriendFauxOfflineMessaging : public ToxEventI {
Contact3Registry& _cr;
RegistryMessageModel& _rmm;
ToxContactModel2& _tcm;
ToxI& _t;
ToxEventProviderI& _tep;
float _interval_timer{0.f};
// TODO: increase timer?
const float _delay_after_cc {4.5f};
const float _delay_inbetween {0.3f};
const float _delay_retry {10.f}; // retry sending after 10s
public:
ToxFriendFauxOfflineMessaging(
Contact3Registry& cr,
RegistryMessageModel& rmm,
ToxContactModel2& tcm,
ToxI& t,
ToxEventProviderI& tep
);
float tick(float time_delta);
private:
// only called for online friends
// returns true if a message was sent
// dont call this too often
bool doFriendMessageCheck(const Contact3 c, const Contact::Components::ToxFriendEphemeral& tfe);
protected:
bool onToxEvent(const Tox_Event_Friend_Connection_Status* e) override;
};