Compare commits

...

11 Commits

17 changed files with 592 additions and 74 deletions

View File

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

View File

@ -17,10 +17,8 @@
#include "./media_meta_info_loader.hpp"
#include "./sdl_clipboard_utils.hpp"
#include "SDL_clipboard.h"
#include <cctype>
#include <cstdint>
#include <ctime>
#include <cstdio>
#include <chrono>
@ -28,10 +26,7 @@
#include <fstream>
#include <iomanip>
#include <sstream>
#include <string>
#include <variant>
#include <vector>
namespace Components {
@ -42,7 +37,7 @@ namespace Components {
} // Components
static float lerp(float a, float b, float t) {
static constexpr float lerp(float a, float b, float t) {
return a + t * (b - a);
}
@ -72,28 +67,78 @@ static std::string file_path_url_escape(const std::string&& value) {
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(
ConfigModelI& conf,
RegistryMessageModel& rmm,
Contact3Registry& cr,
TextureUploaderI& tu
) : _conf(conf), _rmm(rmm), _cr(cr), _tal(_cr), _contact_tc(_tal, tu), _msg_tc(_mil, tu), _sip(tu) {
TextureUploaderI& tu,
ContactTextureCache& contact_tc,
MessageTextureCache& msg_tc
) : _conf(conf), _rmm(rmm), _cr(cr), _contact_tc(contact_tc), _msg_tc(msg_tc), _sip(tu) {
}
void ChatGui4::render(float time_delta) {
if (!_cr.storage<Contact::Components::TagAvatarInvalidate>().empty()) { // handle force-reloads for avatars
std::vector<Contact3> to_purge;
_cr.view<Contact::Components::TagAvatarInvalidate>().each([&to_purge](const Contact3 c) {
to_purge.push_back(c);
});
_cr.remove<Contact::Components::TagAvatarInvalidate>(to_purge.cbegin(), to_purge.cend());
_contact_tc.invalidate(to_purge);
}
// ACTUALLY NOT IF RENDERED, MOVED LOGIC TO ABOVE
// it might unload textures, so it needs to be done before rendering
_contact_tc.update();
_msg_tc.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) {
_fss.render();
_sip.render(time_delta);
@ -514,7 +559,7 @@ void ChatGui4::render(float time_delta) {
_fss.requestFile(
[](const auto& path) -> bool { return std::filesystem::is_regular_file(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());
},
[](){}
);
@ -571,13 +616,12 @@ void ChatGui4::render(float time_delta) {
}
ImGui::End();
_contact_tc.workLoadQueue();
_msg_tc.workLoadQueue();
return 1000.f; // TODO: higher min fps?
}
void ChatGui4::sendFilePath(const char* file_path) {
if (_selected_contact && std::filesystem::is_regular_file(file_path)) {
_rmm.sendFilePath(*_selected_contact, std::filesystem::path(file_path).filename().u8string(), file_path);
_rmm.sendFilePath(*_selected_contact, std::filesystem::path(file_path).filename().generic_u8string(), file_path);
}
}
@ -671,10 +715,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<Contact::Components::TagBig>()) {
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);
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 +805,19 @@ void ChatGui4::renderMessageBodyFile(Message3Registry& reg, const Message3 e) {
const auto& local_info = reg.get<Message::Components::Transfer::FileInfoLocal>(e);
if (local_info.file_list.size() > i && ImGui::BeginPopupContextItem("##file_c")) {
if (ImGui::MenuItem("open")) {
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";
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();
}
}
@ -1065,7 +1118,7 @@ void ChatGui4::pasteFile(const char* mime_type) {
std::ofstream(tmp_file_path, std::ios_base::out | std::ios_base::binary)
.write(reinterpret_cast<const char*>(img_data.data()), img_data.size());
_rmm.sendFilePath(*_selected_contact, tmp_file_name.str(), tmp_file_path.u8string());
_rmm.sendFilePath(*_selected_contact, tmp_file_name.str(), tmp_file_path.generic_u8string());
},
[](){}
);

View File

@ -10,18 +10,24 @@
#include "./file_selector.hpp"
#include "./send_image_popup.hpp"
#include <entt/container/dense_map.hpp>
#include <cstdint>
#include <vector>
#include <set>
#include <string>
#include <mutex>
#include <memory>
using ContactTextureCache = TextureCache<void*, Contact3, ToxAvatarLoader>;
using MessageTextureCache = TextureCache<void*, Message3Handle, MessageImageLoader>;
class ChatGui4 {
ConfigModelI& _conf;
RegistryMessageModel& _rmm;
Contact3Registry& _cr;
ToxAvatarLoader _tal;
TextureCache<void*, Contact3, ToxAvatarLoader> _contact_tc;
MessageImageLoader _mil;
TextureCache<void*, Message3Handle, MessageImageLoader> _msg_tc;
ContactTextureCache& _contact_tc;
MessageTextureCache& _msg_tc;
FileSelector _fss;
SendImagePopup _sip;
@ -37,16 +43,25 @@ class ChatGui4 {
float TEXT_BASE_WIDTH {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:
ChatGui4(
ConfigModelI& conf,
RegistryMessageModel& rmm,
Contact3Registry& cr,
TextureUploaderI& tu
TextureUploaderI& tu,
ContactTextureCache& contact_tc,
MessageTextureCache& msg_tc
);
~ChatGui4(void);
public:
void render(float time_delta);
float render(float time_delta);
public:
bool any_unread {false};

View File

@ -47,7 +47,7 @@ void FileSelector::render(void) {
std::filesystem::path current_path = _current_file_path;
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
constexpr ImGuiTableFlags table_flags =
@ -175,7 +175,7 @@ void FileSelector::render(void) {
}
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()) {
@ -206,7 +206,7 @@ void FileSelector::render(void) {
}
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()) {

View File

@ -178,9 +178,13 @@ int main(int argc, char** argv) {
//)
//));
const float min_delay = std::min<float>(
screen->nextTick() - time_delta_tick,
screen->nextRender() - time_delta_render
const float min_delay =
std::min<float>(
std::min<float>(
screen->nextTick() - time_delta_tick,
screen->nextRender() - time_delta_render
),
0.25f // dont sleep too long
) * 1000.f;
if (min_delay > 0.f) {

View File

@ -1,10 +1,13 @@
#include "./main_screen.hpp"
#include <solanaceae/contact/components.hpp>
#include <imgui/imgui.h>
#include <SDL3/SDL.h>
#include <memory>
#include <cmath>
MainScreen::MainScreen(SDL_Renderer* renderer_, std::string save_path, std::string save_password, std::vector<std::string> plugins) :
renderer(renderer_),
@ -16,10 +19,15 @@ MainScreen::MainScreen(SDL_Renderer* renderer_, std::string save_path, std::stri
tcm(cr, tc, tc),
tmm(rmm, cr, tcm, tc, tc),
ttm(rmm, cr, tcm, tc, tc),
tffom(cr, rmm, tcm, tc, tc),
mmil(rmm),
tam(rmm, cr, conf),
sdlrtu(renderer_),
cg(conf, rmm, cr, sdlrtu),
tal(cr),
contact_tc(tal, sdlrtu),
mil(),
msg_tc(mil, sdlrtu),
cg(conf, rmm, cr, sdlrtu, contact_tc, msg_tc),
sw(conf),
tuiu(tc, conf),
tdch(tpi)
@ -166,7 +174,22 @@ Screen* MainScreen::render(float time_delta, bool&) {
const float pm_interval = pm.render(time_delta); // render
cg.render(time_delta); // render
// TODO: move this somewhere else!!!
// needs both tal and tc <.<
if (!cr.storage<Contact::Components::TagAvatarInvalidate>().empty()) { // handle force-reloads for avatars
std::vector<Contact3> to_purge;
cr.view<Contact::Components::TagAvatarInvalidate>().each([&to_purge](const Contact3 c) {
to_purge.push_back(c);
});
cr.remove<Contact::Components::TagAvatarInvalidate>(to_purge.cbegin(), to_purge.cend());
contact_tc.invalidate(to_purge);
}
// ACTUALLY NOT IF RENDERED, MOVED LOGIC TO ABOVE
// it might unload textures, so it needs to be done before rendering
const float ctc_interval = contact_tc.update();
const float msgtc_interval = msg_tc.update();
const float cg_interval = cg.render(time_delta); // render
sw.render(); // render
tuiu.render(); // render
tdch.render(); // render
@ -215,20 +238,136 @@ Screen* MainScreen::render(float time_delta, bool&) {
ImGui::ShowDemoWindow();
}
if (
_fps_perf_mode > 1 // TODO: magic
) {
// powersave forces 250ms
_render_interval = 1.f/4.f;
} else if (
_time_since_event > 1.f && ( // 1sec cool down
_fps_perf_mode == 1 || // TODO: magic
_window_hidden
float tc_unfinished_queue_interval;
{ // load rendered but not loaded textures
bool unfinished_work_queue = contact_tc.workLoadQueue();
unfinished_work_queue = unfinished_work_queue || msg_tc.workLoadQueue();
if (unfinished_work_queue) {
tc_unfinished_queue_interval = 0.1f; // so we can get images loaded faster
} else {
tc_unfinished_queue_interval = 1.f; // TODO: higher min fps?
}
}
// calculate interval for next frame
// normal:
// - if < 1.5sec since last event
// - min all and clamp(1/60, 1/1)
// - if < 30sec since last event
// - min all (anim + everything else) clamp(1/60, 1/1) (maybe less?)
// - else
// - min without anim and clamp(1/60, 1/1) (maybe more?)
// reduced:
// - if < 1sec since last event
// - min all and clamp(1/60, 1/1)
// - if < 10sec since last event
// - min all (anim + everything else) clamp(1/10, 1/1)
// - else
// - min without anim and max clamp(1/10, 1/1)
// powersave:
// - if < 0sec since last event
// - (ignored)
// - if < 1sec since last event
// - min all (anim + everything else) clamp(1/8, 1/1)
// - else
// - min without anim and clamp(1/1, 1/1)
struct PerfProfileRender {
float low_delay_window {1.5f};
float low_delay_min {1.f/60.f};
float low_delay_max {1.f/30.f};
float mid_delay_window {30.f};
float mid_delay_min {1.f/60.f};
float mid_delay_max {1.f/2.f};
// also when main window hidden
float else_delay_min {1.f/60.f};
float else_delay_max {1.f/2.f};
};
const static PerfProfileRender normalPerfProfile{
//1.5f, // low_delay_window
//1.f/60.f, // low_delay_min
//1.f/30.f, // low_delay_max
//30.f, // mid_delay_window
//1.f/60.f, // mid_delay_min
//1.f/2.f, // mid_delay_max
//1.f/60.f, // else_delay_min
//1.f/2.f, // else_delay_max
};
const static PerfProfileRender reducedPerfProfile{
1.f, // low_delay_window
1.f/60.f, // low_delay_min
1.f/30.f, // low_delay_max
10.f, // mid_delay_window
1.f/10.f, // mid_delay_min
1.f/4.f, // mid_delay_max
1.f/10.f, // else_delay_min
1.f, // else_delay_max
};
// TODO: fix powersave by adjusting it in the events handler (make ppr member)
const static PerfProfileRender powersavePerfProfile{
// no window -> ignore first case
0.f, // low_delay_window
1.f, // low_delay_min
1.f, // low_delay_max
1.f, // mid_delay_window
1.f/8.f, // mid_delay_min
1.f/4.f, // mid_delay_max
1.f, // else_delay_min
1.f, // else_delay_max
};
const PerfProfileRender& curr_profile =
// TODO: magic
_fps_perf_mode > 1
? powersavePerfProfile
: (
_fps_perf_mode == 1
? reducedPerfProfile
: normalPerfProfile
)
) {
_render_interval = std::min<float>(1.f/4.f, pm_interval);
;
// min over non animations in all cases
_render_interval = std::min<float>(pm_interval, cg_interval);
_render_interval = std::min<float>(_render_interval, tc_unfinished_queue_interval);
// low delay time window
if (!_window_hidden && _time_since_event < curr_profile.low_delay_window) {
_render_interval = std::min<float>(_render_interval, ctc_interval);
_render_interval = std::min<float>(_render_interval, msgtc_interval);
_render_interval = std::clamp(
_render_interval,
curr_profile.low_delay_min,
curr_profile.low_delay_max
);
// mid delay time window
} else if (!_window_hidden && _time_since_event < curr_profile.mid_delay_window) {
_render_interval = std::min<float>(_render_interval, ctc_interval);
_render_interval = std::min<float>(_render_interval, msgtc_interval);
_render_interval = std::clamp(
_render_interval,
curr_profile.mid_delay_min,
curr_profile.mid_delay_max
);
// timed out or window hidden
} else {
_render_interval = std::min<float>(1.f/60.f, pm_interval);
// no animation timing here
_render_interval = std::clamp(
_render_interval,
curr_profile.else_delay_min,
curr_profile.else_delay_max
);
}
_time_since_event += time_delta;
@ -241,6 +380,8 @@ Screen* MainScreen::tick(float time_delta, bool& quit) {
tcm.iterate(time_delta); // compute
const float fo_interval = tffom.tick(time_delta);
tam.iterate(); // compute
const float pm_interval = pm.tick(time_delta); // compute
@ -250,9 +391,17 @@ Screen* MainScreen::tick(float time_delta, bool& quit) {
mts.iterate(); // compute
_min_tick_interval = std::min<float>(
tc.toxIterationInterval()/1000.f,
// HACK: pow by 1.6 to increase 50 -> ~500 (~522)
// and it does not change 1
std::pow(tc.toxIterationInterval(), 1.6f)/1000.f,
pm_interval
);
_min_tick_interval = std::min<float>(
_min_tick_interval,
fo_interval
);
//std::cout << "MS: min tick interval: " << _min_tick_interval << "\n";
switch (_compute_perf_mode) {
// normal 1ms lower bound

View File

@ -21,10 +21,15 @@
#include "./tox_avatar_manager.hpp"
#include "./sdlrenderer_texture_uploader.hpp"
#include "./texture_cache.hpp"
#include "./tox_avatar_loader.hpp"
#include "./message_image_loader.hpp"
#include "./chat_gui4.hpp"
#include "./settings_window.hpp"
#include "./tox_ui_utils.hpp"
#include "./tox_dht_cap_histo.hpp"
#include "./tox_friend_faux_offline_messaging.hpp"
#include <string>
#include <iostream>
@ -52,6 +57,7 @@ struct MainScreen final : public Screen {
ToxContactModel2 tcm;
ToxMessageManager tmm;
ToxTransferManager ttm;
ToxFriendFauxOfflineMessaging tffom;
MediaMetaInfoLoader mmil;
ToxAvatarManager tam;
@ -59,6 +65,11 @@ struct MainScreen final : public Screen {
SDLRendererTextureUploader sdlrtu;
//OpenGLTextureUploader ogltu;
ToxAvatarLoader tal;
TextureCache<void*, Contact3, ToxAvatarLoader> contact_tc;
MessageImageLoader mil;
TextureCache<void*, Message3Handle, MessageImageLoader> msg_tc;
ChatGui4 cg;
SettingsWindow sw;
ToxUIUtils tuiu;
@ -67,7 +78,7 @@ struct MainScreen final : public Screen {
bool _show_tool_style_editor {false};
bool _window_hidden {false};
bool _window_hidden_ts {0};
uint64_t _window_hidden_ts {0};
float _time_since_event {0.f};
MainScreen(SDL_Renderer* renderer_, std::string save_path, std::string save_password, std::vector<std::string> plugins);

View File

@ -32,7 +32,7 @@ struct SendImagePopup {
Rect crop_rect;
Rect crop_before_drag;
bool cropping {false};
bool cropping {true};
bool dragging_last_frame_ul {false};
bool dragging_last_frame_lr {false};

View File

@ -2,8 +2,9 @@
#include <chrono>
#include <array>
#include <limits>
void TextureEntry::doAnimation(const int64_t ts_now) {
int64_t TextureEntry::doAnimation(const int64_t ts_now) {
if (frame_duration.size() > 1) { // is animation
do { // why is this loop so ugly
const int64_t duration = getDuration();
@ -11,11 +12,13 @@ void TextureEntry::doAnimation(const int64_t ts_now) {
timestamp_last_rendered += duration;
next();
} else {
break;
// return ts for next frame
return timestamp_last_rendered + duration;
}
} while(true);
} while (true);
} else {
timestamp_last_rendered = ts_now;
return std::numeric_limits<int64_t>::max(); // static image
}
}

View File

@ -50,7 +50,8 @@ struct TextureEntry {
current_texture = (current_texture + 1) % frame_duration.size();
}
void doAnimation(const int64_t ts_now);
// returns ts for next frame
int64_t doAnimation(const int64_t ts_now);
template<typename TextureType>
TextureType getID(void) {
@ -133,14 +134,16 @@ struct TextureCache {
}
}
void update(void) {
float update(void) {
const uint64_t ts_now = Message::getTimeMS();
uint64_t ts_min_next = ts_now + ms_before_purge;
std::vector<KeyType> to_purge;
for (auto&& [key, te] : _cache) {
if (te.rendered_this_frame) {
te.doAnimation(ts_now);
const uint64_t ts_next = te.doAnimation(ts_now);
te.rendered_this_frame = false;
ts_min_next = std::min(ts_min_next, ts_next);
} else if (_cache.size() > min_count_before_purge && ts_now - te.timestamp_last_rendered >= ms_before_purge) {
to_purge.push_back(key);
}
@ -148,7 +151,10 @@ struct TextureCache {
invalidate(to_purge);
// we ignore the default texture ts :)
_default_texture.doAnimation(ts_now);
return (ts_min_next - ts_now) / 1000.f;
}
void invalidate(const std::vector<KeyType>& to_purge) {
@ -162,16 +168,22 @@ struct TextureCache {
}
}
void workLoadQueue(void) {
for (auto it = _to_load.begin(); it != _to_load.end(); it++) {
// returns true if there is still work queued up
bool workLoadQueue(void) {
auto it = _to_load.begin();
for (; it != _to_load.end(); it++) {
auto new_entry_opt = _l.load(_tu, *it);
if (new_entry_opt.has_value()) {
_cache.emplace(*it, new_entry_opt.value());
_to_load.erase(it);
// TODO: not a good idea
it = _to_load.erase(it);
// TODO: not a good idea?
break; // end load from queue/onlyload 1 per update
}
}
// peak
return it != _to_load.end();
}
};

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 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");
return file_path.u8string();
return file_path.generic_u8string();
}
void ToxAvatarManager::addAvatarFileToContact(const Contact3 c, const ToxKey& key) {

View File

@ -0,0 +1,213 @@
#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>
//#include <iostream>
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) {
_tep.subscribe(this, Tox_Event_Type::TOX_EVENT_FRIEND_CONNECTION_STATUS);
}
float ToxFriendFauxOfflineMessaging::tick(float time_delta) {
_interval_timer -= time_delta;
if (_interval_timer > 0.f) {
return std::max(_interval_timer, 0.001f); // TODO: min next timer
}
// interval ~ once per minute
_interval_timer = 60.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);
auto* mr = static_cast<const RegistryMessageModel&>(_rmm).get(c);
if (mr != nullptr) {
mr->storage<Message::Components::LastSendAttempt>().clear();
}
}
} else {
if (!_cr.all_of<Contact::Components::NextSendAttempt>(c)) {
if (false) { // has unsent messages
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 ret = doFriendMessageCheck(c, tfe);
if (ret == dfmc_Ret::SENT_THIS_TICK) {
const auto ts = _cr.get<Contact::Components::NextSendAttempt>(c).ts = ts_now + uint64_t(_delay_inbetween*1000);
min_next_attempt_ts = std::min(min_next_attempt_ts, ts);
} else if (ret == dfmc_Ret::TOO_SOON) {
// TODO: set to _delay_inbetween? prob expensive for no good reason
min_next_attempt_ts = std::min(min_next_attempt_ts, _cr.get<Contact::Components::NextSendAttempt>(c).ts);
} else {
_cr.remove<Contact::Components::NextSendAttempt>(c);
}
}
}
});
if (min_next_attempt_ts <= ts_now) {
// we (probably) sent this iterate
_interval_timer = 0.1f; // TODO: ugly magic
} else if (min_next_attempt_ts == std::numeric_limits<uint64_t>::max()) {
// nothing to sync or all offline that need syncing
} else {
_interval_timer = std::min(_interval_timer, (min_next_attempt_ts - ts_now) / 1000.f);
}
//std::cout << "TFFOM: iterate (i:" << _interval_timer << ")\n";
return _interval_timer;
}
ToxFriendFauxOfflineMessaging::dfmc_Ret 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 dfmc_Ret::NO_MSG;
}
const uint64_t ts_now = Message::getTimeMS();
// filter for unconfirmed messages
// we assume sorted
// ("reverse" iteration <.<)
auto msg_view = mr->view<Message::Components::Timestamp>();
bool valid_unsent {false};
// 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
}
if (mr->get<Message::Components::ContactTo>(msg).c != c) {
continue; // not outbound (in private)
}
valid_unsent = true;
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 dfmc_Ret::SENT_THIS_TICK;
}
if (!valid_unsent) {
// somehow cleanup lsa
mr->storage<Message::Components::LastSendAttempt>().clear();
//std::cout << "TFFOM: all sent, deleting lsa\n";
return dfmc_Ret::NO_MSG;
}
return dfmc_Ret::TOO_SOON;
}
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
_interval_timer = 0.f;
return false;
}

View File

@ -0,0 +1,55 @@
#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:
enum class dfmc_Ret {
TOO_SOON,
SENT_THIS_TICK,
NO_MSG,
};
// only called for online friends
// returns true if a message was sent
// dont call this too often
dfmc_Ret doFriendMessageCheck(const Contact3 c, const Contact::Components::ToxFriendEphemeral& tfe);
protected:
bool onToxEvent(const Tox_Event_Friend_Connection_Status* e) override;
};