diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index db10498..5f04bfd 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -89,6 +89,8 @@ target_sources(tomato PUBLIC
 	./chat_gui/icons/group.cpp
 	./chat_gui/contact_list.hpp
 	./chat_gui/contact_list.cpp
+	./chat_gui/contact_list_sorter.hpp
+	./chat_gui/contact_list_sorter.cpp
 	./chat_gui/file_selector.hpp
 	./chat_gui/file_selector.cpp
 	./chat_gui/image_viewer_popup.hpp
diff --git a/src/chat_gui/contact_list_sorter.cpp b/src/chat_gui/contact_list_sorter.cpp
new file mode 100644
index 0000000..e58e03c
--- /dev/null
+++ b/src/chat_gui/contact_list_sorter.cpp
@@ -0,0 +1,138 @@
+#include "./contact_list_sorter.hpp"
+
+#include <solanaceae/contact/components.hpp>
+
+ContactListSorter::comperator_fn ContactListSorter::getSortGroupsOverPrivates(void) {
+	return [](const ContactRegistry4& cr, const Contact4 lhs, const Contact4 rhs) -> std::optional<bool> {
+
+		// - groups (> privates)
+		if (cr.all_of<Contact::Components::TagGroup>(lhs) && !cr.all_of<Contact::Components::TagGroup>(rhs)) {
+			return true;
+		} else if (!cr.all_of<Contact::Components::TagGroup>(lhs) && cr.all_of<Contact::Components::TagGroup>(rhs)) {
+			return false;
+		}
+
+		return std::nullopt;
+	};
+}
+
+ContactListSorter::comperator_fn ContactListSorter::getSortAcitivty(void) {
+	return [](const ContactRegistry4& cr, const Contact4 lhs, const Contact4 rhs) -> std::optional<bool> {
+		// - activity (exists)
+		if (cr.all_of<Contact::Components::LastActivity>(lhs) && !cr.all_of<Contact::Components::LastActivity>(rhs)) {
+			return true;
+		} else if (!cr.all_of<Contact::Components::LastActivity>(lhs) && cr.all_of<Contact::Components::LastActivity>(rhs)) {
+			return false;
+		}
+		// else - we can assume both have or dont have LastActivity
+
+		// - activity new > old
+		if (cr.all_of<Contact::Components::LastActivity>(lhs)) {
+			const auto l = cr.get<Contact::Components::LastActivity>(lhs).ts;
+			const auto r = cr.get<Contact::Components::LastActivity>(rhs).ts;
+			if (l > r) {
+				return true;
+			} else if (l < r) {
+				return false;
+			}
+		}
+
+		return std::nullopt;
+	};
+}
+
+ContactListSorter::comperator_fn ContactListSorter::getSortFirstSeen(void) {
+	return [](const ContactRegistry4& cr, const Contact4 lhs, const Contact4 rhs) -> std::optional<bool> {
+		// - first seen (exists)
+		if (cr.all_of<Contact::Components::FirstSeen>(lhs) && !cr.all_of<Contact::Components::FirstSeen>(rhs)) {
+			return true;
+		} else if (!cr.all_of<Contact::Components::FirstSeen>(lhs) && cr.all_of<Contact::Components::FirstSeen>(rhs)) {
+			return false;
+		}
+		// else - we can assume both have or dont have FirstSeen
+
+		// - first seen new > old
+		if (cr.all_of<Contact::Components::FirstSeen>(lhs)) {
+			const auto l = cr.get<Contact::Components::FirstSeen>(lhs).ts;
+			const auto r = cr.get<Contact::Components::FirstSeen>(rhs).ts;
+			if (l > r) {
+				return true;
+			} else if (l < r) {
+				return false;
+			}
+		}
+
+		return std::nullopt;
+	};
+}
+
+bool ContactListSorter::resolveStack(const std::vector<comperator_fn>& stack, const ContactRegistry4& cr, const Contact4 lhs, const Contact4 rhs) {
+	for (const auto& fn : stack) {
+		auto res = fn(cr, lhs, rhs);
+		if (res.has_value()) {
+			return res.value();
+		}
+	}
+
+	// fallback to numberical ordering, making sure the ordering can be strong
+	return entt::to_integral(lhs) > entt::to_integral(rhs);
+}
+
+ContactListSorter::ContactListSorter(ContactStore4I& cs) :
+	_cs(cs), _cs_sr(_cs.newSubRef(this))
+{
+	_sort_stack.reserve(3);
+	_sort_stack.emplace_back(getSortGroupsOverPrivates());
+	_sort_stack.emplace_back(getSortAcitivty());
+	_sort_stack.emplace_back(getSortFirstSeen());
+
+	_cs_sr
+		.subscribe(ContactStore4_Event::contact_construct)
+		.subscribe(ContactStore4_Event::contact_update)
+		.subscribe(ContactStore4_Event::contact_destroy)
+	;
+}
+
+ContactListSorter::~ContactListSorter(void) {
+}
+
+void ContactListSorter::sort(void) {
+	// TODO: timer
+	if (!_dirty) {
+		return;
+	}
+
+	auto& cr = _cs.registry();
+
+	// first: make sure every cantact we want to have in the list has the tag
+	// do we pass exclusion to the list widget, or update sort comp? - the later
+	cr.clear<Contact::Components::ContactSortTag>();
+	for (const auto cv : cr.view<Contact::Components::TagBig>()) {
+		(void)cr.get_or_emplace<Contact::Components::ContactSortTag>(cv);
+	}
+
+	// second: sort
+	cr.sort<Contact::Components::ContactSortTag>(
+		[this, &cr](const Contact4 lhs, const Contact4 rhs) -> bool {
+			return resolveStack(_sort_stack, cr, lhs, rhs);
+		},
+		entt::insertion_sort{} // o(n) in >90% of cases
+	);
+
+	_dirty = false;
+}
+
+bool ContactListSorter::onEvent(const ContactStore::Events::Contact4Construct&) {
+	_dirty = true;
+	return false;
+}
+
+bool ContactListSorter::onEvent(const ContactStore::Events::Contact4Update&) {
+	_dirty = true;
+	return false;
+}
+
+bool ContactListSorter::onEvent(const ContactStore::Events::Contact4Destory&) {
+	_dirty = true;
+	return false;
+}
diff --git a/src/chat_gui/contact_list_sorter.hpp b/src/chat_gui/contact_list_sorter.hpp
new file mode 100644
index 0000000..5505e0b
--- /dev/null
+++ b/src/chat_gui/contact_list_sorter.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <solanaceae/contact/contact_store_events.hpp>
+#include <solanaceae/contact/contact_store_i.hpp>
+
+#include "./contact_list.hpp"
+
+#include <functional>
+#include <optional>
+#include <vector>
+
+namespace Contact::Components {
+
+	// empty contact comp that is sorted in the set for displaying
+	struct ContactSortTag {};
+
+} // Contact::Components
+
+
+class ContactListSorter : public ContactStore4EventI {
+	public:
+		using comperator_fn = std::function<std::optional<bool>(const ContactRegistry4& cr, const Contact4 lhs, const Contact4 rhs)>;
+
+		static comperator_fn getSortGroupsOverPrivates(void);
+		static comperator_fn getSortAcitivty(void);
+		static comperator_fn getSortFirstSeen(void);
+
+	private:
+		ContactStore4I& _cs;
+		ContactStore4I::SubscriptionReference _cs_sr;
+		std::vector<comperator_fn> _sort_stack;
+
+		bool _dirty {true};
+		// TODO: timer, to guarantie a sort ever X seconds?
+		// (turns out we dont throw on new messages <.<)
+
+	private:
+		static bool resolveStack(const std::vector<comperator_fn>& stack, const ContactRegistry4& cr, const Contact4 lhs, const Contact4 rhs);
+
+	public:
+		// TODO: expose sort stack
+		ContactListSorter(ContactStore4I& cs);
+		~ContactListSorter(void);
+
+		// optionally perform the sort
+		void sort(void);
+
+	protected:
+		bool onEvent(const ContactStore::Events::Contact4Construct&) override;
+		bool onEvent(const ContactStore::Events::Contact4Update&) override;
+		bool onEvent(const ContactStore::Events::Contact4Destory&) override;
+};
diff --git a/src/chat_gui4.cpp b/src/chat_gui4.cpp
index aa51d6e..58bac4f 100644
--- a/src/chat_gui4.cpp
+++ b/src/chat_gui4.cpp
@@ -62,9 +62,6 @@ namespace Components {
 		int tm_min {0};
 	};
 
-	// empty contact comp that is sorted in the set for displaying
-	struct ContactSortTag {};
-
 } // Components
 
 namespace Context {
@@ -207,7 +204,8 @@ ChatGui4::ChatGui4(
 	_b_tc(_bil, tu),
 	_theme(theme),
 	_sip(tu),
-	_ivp(_msg_tc)
+	_ivp(_msg_tc),
+	_cls(cs)
 {
 	_os_sr.subscribe(ObjectStore_Event::object_update);
 }
@@ -258,57 +256,9 @@ float ChatGui4::render(float time_delta, bool window_hidden, bool window_focused
 		}
 
 		renderContactList();
+		// after vis check
 		if (_contact_list_sortable) {
-			// TODO: extract this; with timer and events to dirty
-			// !! events !!
-			auto& cr = _cs.registry();
-
-			// first: make sure every cantact we want to have in the list has the tag
-			// do we pass exclusion to the list widget, or update sort comp? - the later
-			// TODO: re do from sratch every time?
-			//cr.clear<Components::ContactSortTag>();
-			for (const auto cv : cr.view<Contact::Components::TagBig>()) {
-				(void)cr.get_or_emplace<Components::ContactSortTag>(cv);
-			}
-
-			// second: sort
-			cr.sort<Components::ContactSortTag>(
-				[&](const Contact4 lhs, const Contact4 rhs) -> bool {
-					// TODO: custom sort rules, order
-
-					// - groups (> privates)
-					if (cr.all_of<Contact::Components::TagGroup>(lhs) && !cr.all_of<Contact::Components::TagGroup>(rhs)) {
-						return true;
-					} else if (!cr.all_of<Contact::Components::TagGroup>(lhs) && cr.all_of<Contact::Components::TagGroup>(rhs)) {
-						return false;
-					}
-
-					// - activity (exists)
-					if (cr.all_of<Contact::Components::LastActivity>(lhs) && !cr.all_of<Contact::Components::LastActivity>(rhs)) {
-						return true;
-					} else if (!cr.all_of<Contact::Components::LastActivity>(lhs) && cr.all_of<Contact::Components::LastActivity>(rhs)) {
-						return false;
-					}
-					// else - we can assume both have or dont have LastActivity
-
-					// - activity new > old
-					if (cr.all_of<Contact::Components::LastActivity>(lhs)) {
-						const auto l = cr.get<Contact::Components::LastActivity>(lhs).ts;
-						const auto r = cr.get<Contact::Components::LastActivity>(rhs).ts;
-						if (l > r) {
-							return true;
-						} else if (l < r) {
-							return false;
-						}
-					}
-
-					// - first seen new > old
-					// TODO: implement
-
-					return false;
-				},
-				entt::insertion_sort{} // o(n) in 99% of cases
-			);
+			_cls.sort();
 		}
 		ImGui::SameLine();
 
@@ -1715,7 +1665,7 @@ void ChatGui4::renderContactList(void) {
 			_rmm,
 			_theme,
 			_contact_tc,
-			contact_const_runtime_view{}.iterate(cr.storage<Components::ContactSortTag>()),
+			contact_const_runtime_view{}.iterate(cr.storage<Contact::Components::ContactSortTag>()),
 			selected_contact
 		)) {
 			_selected_contact = selected_contact.entity();
diff --git a/src/chat_gui4.hpp b/src/chat_gui4.hpp
index 9e8ff12..db6aadd 100644
--- a/src/chat_gui4.hpp
+++ b/src/chat_gui4.hpp
@@ -15,6 +15,7 @@
 #include "./chat_gui/file_selector.hpp"
 #include "./chat_gui/send_image_popup.hpp"
 #include "./chat_gui/image_viewer_popup.hpp"
+#include "./chat_gui/contact_list_sorter.hpp"
 
 #include <entt/container/dense_map.hpp>
 
@@ -41,9 +42,9 @@ class ChatGui4 : public ObjectStoreEventI {
 	FileSelector _fss;
 	SendImagePopup _sip;
 	ImageViewerPopup _ivp;
+	ContactListSorter _cls;
 
 	// set to true if not hovered
-	// TODO: add timer?
 	bool _contact_list_sortable {false};
 
 	// TODO: refactor this to allow multiple open contacts