diff --git a/CMakeLists.txt b/CMakeLists.txt
index d609a04029..627e8e6615 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -253,6 +253,8 @@ set(${BINARY_NAME}_SOURCES
src/friendlist.h
src/conferencelist.cpp
src/conferencelist.h
+ src/grouplist.cpp
+ src/grouplist.h
src/ipc.cpp
src/ipc.h
src/nexus.cpp
@@ -309,6 +311,10 @@ set(${BINARY_NAME}_SOURCES
src/core/icoreconferencemessagesender.h
src/core/icoreconferencequery.cpp
src/core/icoreconferencequery.h
+ src/core/icoregroupmessagesender.cpp
+ src/core/icoregroupmessagesender.h
+ src/core/icoregroupquery.cpp
+ src/core/icoregroupquery.h
src/core/icoreidhandler.cpp
src/core/icoreidhandler.h
src/core/idebugsettings.cpp
@@ -324,6 +330,8 @@ set(${BINARY_NAME}_SOURCES
src/core/toxid.h
src/core/conferenceid.cpp
src/core/conferenceid.h
+ src/core/groupid.cpp
+ src/core/groupid.h
src/core/toxlogger.cpp
src/core/toxlogger.h
src/core/toxoptions.cpp
@@ -348,6 +356,8 @@ set(${BINARY_NAME}_SOURCES
src/model/chatroom/friendchatroom.h
src/model/chatroom/conferenceroom.cpp
src/model/chatroom/conferenceroom.h
+ src/model/chatroom/grouproom.cpp
+ src/model/chatroom/grouproom.h
src/model/chat.cpp
src/model/chat.h
src/model/debug/debuglogmodel.cpp
@@ -374,6 +384,12 @@ set(${BINARY_NAME}_SOURCES
src/model/conferencemessagedispatcher.h
src/model/conference.cpp
src/model/conference.h
+ src/model/group.cpp
+ src/model/group.h
+ src/model/groupinvite.cpp
+ src/model/groupinvite.h
+ src/model/groupmessagedispatcher.cpp
+ src/model/groupmessagedispatcher.h
src/model/ibootstraplistgenerator.cpp
src/model/ibootstraplistgenerator.h
src/model/ichatlog.h
@@ -537,6 +553,12 @@ set(${BINARY_NAME}_SOURCES
src/widget/form/conferenceinviteform.h
src/widget/form/conferenceinvitewidget.cpp
src/widget/form/conferenceinvitewidget.h
+ src/widget/form/groupform.cpp
+ src/widget/form/groupform.h
+ src/widget/form/groupinviteform.cpp
+ src/widget/form/groupinviteform.h
+ src/widget/form/groupinvitewidget.cpp
+ src/widget/form/groupinvitewidget.h
src/widget/form/loadhistorydialog.cpp
src/widget/form/loadhistorydialog.h
src/widget/form/profileform.cpp
@@ -581,6 +603,8 @@ set(${BINARY_NAME}_SOURCES
src/widget/genericchatroomwidget.h
src/widget/conferencewidget.cpp
src/widget/conferencewidget.h
+ src/widget/groupwidget.cpp
+ src/widget/groupwidget.h
src/widget/loginscreen.cpp
src/widget/loginscreen.h
src/widget/maskablepixmapwidget.cpp
diff --git a/img/group.svg b/img/group.svg
new file mode 100644
index 0000000000..c380771532
--- /dev/null
+++ b/img/group.svg
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/img/group_dark.svg b/img/group_dark.svg
new file mode 100644
index 0000000000..8cc315753c
--- /dev/null
+++ b/img/group_dark.svg
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/res.qrc b/res.qrc
index 9dae976b22..233421de13 100644
--- a/res.qrc
+++ b/res.qrc
@@ -14,6 +14,8 @@
img/contact_dark.svg
img/contact.svg
img/debug.svg
+ img/group_dark.svg
+ img/group.svg
img/icons/qtox.svg
img/login_logo.svg
img/others/logout-icon.svg
diff --git a/src/chatlog/chatmessage.cpp b/src/chatlog/chatmessage.cpp
index 67528e23a7..29c8f55411 100644
--- a/src/chatlog/chatmessage.cpp
+++ b/src/chatlog/chatmessage.cpp
@@ -43,7 +43,8 @@ ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QSt
MessageType type, bool isMe, MessageState state,
const QDateTime& date, DocumentCache& documentCache,
SmileyPack& smileyPack, Settings& settings,
- Style& style, bool colorizeName)
+ Style& style, bool colorizeName, bool isPrivate,
+ const QString& recipientName)
{
ChatMessage::Ptr msg = std::make_shared(documentCache, settings, style);
@@ -71,6 +72,17 @@ ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QSt
text = TextFormatter::applyMarkdown(text, styleType == Settings::StyleType::WITH_CHARS);
}
+ if (isPrivate) {
+ QString badgeText = QObject::tr("private", "Label for private group messages");
+ if (!recipientName.isEmpty()) {
+ badgeText += QStringLiteral(" → %1").arg(recipientName.toHtmlEscaped());
+ }
+ const QString badge = QStringLiteral(
+ "%1 ")
+ .arg(badgeText);
+ text = badge + text;
+ }
+
switch (type) {
case NORMAL:
diff --git a/src/chatlog/chatmessage.h b/src/chatlog/chatmessage.h
index 4c24031431..9a8d8e1075 100644
--- a/src/chatlog/chatmessage.h
+++ b/src/chatlog/chatmessage.h
@@ -48,7 +48,9 @@ class ChatMessage : public ChatLine
MessageType type, bool isMe, MessageState state,
const QDateTime& date, DocumentCache& documentCache,
SmileyPack& smileyPack, Settings& settings,
- Style& style, bool colorizeName = false);
+ Style& style, bool colorizeName = false,
+ bool isPrivate = false,
+ const QString& recipientName = QString());
static ChatMessage::Ptr createChatInfoMessage(const QString& rawMessage, SystemMessageType type,
const QDateTime& date, DocumentCache& documentCache,
Settings& settings, Style& style);
diff --git a/src/chatlog/chatwidget.cpp b/src/chatlog/chatwidget.cpp
index e612055cfd..31ea2b0a60 100644
--- a/src/chatlog/chatwidget.cpp
+++ b/src/chatlog/chatwidget.cpp
@@ -57,10 +57,13 @@ ChatMessage::Ptr createMessage(const QString& displayName, bool isSelf, bool col
messageType = ChatMessage::MessageType::ALERT;
}
+ const bool isPrivate = !chatLogMessage.message.recipient.isEmpty();
+ const QString recipientName = isSelf ? chatLogMessage.message.recipientName : QString();
const auto timestamp = chatLogMessage.message.timestamp;
return ChatMessage::createChatMessage(displayName, chatLogMessage.message.content, messageType,
isSelf, chatLogMessage.state, timestamp, documentCache,
- smileyPack, settings, style, colorizeNames);
+ smileyPack, settings, style, colorizeNames, isPrivate,
+ recipientName);
}
void renderMessageRaw(const QString& displayName, bool isSelf, bool colorizeNames,
@@ -1075,7 +1078,7 @@ void ChatWidget::onWorkerTimeout()
return;
}
- if (static_cast(workerLastIndex) >= chatLineStorage->size()) {
+ if (workerLastIndex >= chatLineStorage->size()) {
break;
}
diff --git a/src/core/conferenceid.cpp b/src/core/conferenceid.cpp
index 5eea8b740f..efb7e4ddcb 100644
--- a/src/core/conferenceid.cpp
+++ b/src/core/conferenceid.cpp
@@ -26,11 +26,11 @@ ConferenceId::ConferenceId()
/**
* @brief Constructs a ConferenceId from bytes.
* @param rawId The bytes to construct the ConferenceId from. The length must be exactly
- * ConferenceId::size, else the ConferenceId will be empty.
+ * TOX_CONFERENCE_ID_SIZE, else the ConferenceId will be empty.
*/
ConferenceId::ConferenceId(const QByteArray& rawId)
: ChatId([rawId]() {
- assert(rawId.length() == size);
+ assert(rawId.length() == TOX_CONFERENCE_ID_SIZE);
return rawId;
}())
{
@@ -39,10 +39,10 @@ ConferenceId::ConferenceId(const QByteArray& rawId)
/**
* @brief Constructs a ConferenceId from bytes.
* @param rawId The bytes to construct the ConferenceId from, will read exactly
- * ConferenceId::size from the specified buffer.
+ * TOX_CONFERENCE_ID_SIZE from the specified buffer.
*/
ConferenceId::ConferenceId(const uint8_t* rawId)
- : ChatId(QByteArray(reinterpret_cast(rawId), size))
+ : ChatId(QByteArray(reinterpret_cast(rawId), TOX_CONFERENCE_ID_SIZE))
{
}
@@ -52,7 +52,7 @@ ConferenceId::ConferenceId(const uint8_t* rawId)
*/
int ConferenceId::getSize() const
{
- return size;
+ return TOX_CONFERENCE_ID_SIZE;
}
std::unique_ptr ConferenceId::clone() const
diff --git a/src/core/conferenceid.h b/src/core/conferenceid.h
index 00249e1f56..1d76d773ce 100644
--- a/src/core/conferenceid.h
+++ b/src/core/conferenceid.h
@@ -10,11 +10,11 @@
#include
#include
+#include
class ConferenceId : public ChatId
{
public:
- static constexpr int size = 32;
ConferenceId();
explicit ConferenceId(const QByteArray& rawId);
explicit ConferenceId(const uint8_t* rawId);
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 7edff5ebd9..fc939fd814 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -14,6 +14,7 @@
#include "src/core/toxoptions.h"
#include "src/core/toxstring.h"
#include "src/model/conferenceinvite.h"
+#include "src/model/groupinvite.h"
#include "src/model/ibootstraplistgenerator.h"
#include "src/model/status.h"
#include "util/toxcoreerrorparser.h"
@@ -21,6 +22,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -34,6 +36,10 @@
const QString Core::TOX_EXT = ".tox";
+// how often to force a group reconnect (workaround for toxcore not restoring
+// group connections/roles after a restart)
+static constexpr int GROUP_RECONNECT_INTERVAL_MS = 10000;
+
#define ASSERT_CORE_THREAD assert(QThread::currentThread() == coreThread.get())
namespace {
@@ -57,9 +63,10 @@ Core::Core(QThread* coreThread_, IBootstrapListGenerator& bootstrapListGenerator
{
assert(toxTimer);
// need to migrate Settings and History if this changes
- assert(ToxPk::size == tox_public_key_size());
- assert(ConferenceId::size == tox_conference_id_size());
- assert(ToxId::size == tox_address_size());
+ assert(TOX_PUBLIC_KEY_SIZE == tox_public_key_size());
+ assert(TOX_CONFERENCE_ID_SIZE == tox_conference_id_size());
+ assert(TOX_GROUP_CHAT_ID_SIZE == tox_group_chat_id_size());
+ assert(TOX_ADDRESS_SIZE == tox_address_size());
toxTimer->setSingleShot(true);
connect(toxTimer, &QTimer::timeout, this, &Core::process);
connect(coreThread_, &QThread::finished, toxTimer, &QTimer::stop);
@@ -96,6 +103,23 @@ void Core::registerCallbacks(Tox* tox)
tox_callback_conference_peer_list_changed(tox, onConferencePeerListChange);
tox_callback_conference_peer_name(tox, onConferencePeerNameChange);
tox_callback_conference_title(tox, onConferenceTitleChange);
+
+ tox_callback_group_invite(tox, onGroupInvite);
+ tox_callback_group_message(tox, onGroupMessage);
+ tox_callback_group_private_message(tox, onGroupPrivateMessage);
+ tox_callback_group_peer_join(tox, onGroupPeerJoin);
+ tox_callback_group_peer_exit(tox, onGroupPeerExit);
+ tox_callback_group_peer_name(tox, onGroupPeerNameChange);
+ tox_callback_group_peer_status(tox, onGroupPeerStatusChange);
+ tox_callback_group_self_join(tox, onGroupSelfJoin);
+ tox_callback_group_topic(tox, onGroupTopic);
+ tox_callback_group_join_fail(tox, onGroupJoinFail);
+ tox_callback_group_moderation(tox, onGroupModeration);
+ tox_callback_group_password(tox, onGroupPassword);
+ tox_callback_group_peer_limit(tox, onGroupPeerLimit);
+ tox_callback_group_topic_lock(tox, onGroupTopicLock);
+ tox_callback_group_voice_state(tox, onGroupVoiceState);
+ tox_callback_group_privacy_state(tox, onGroupPrivacyState);
}
/**
@@ -214,6 +238,7 @@ void Core::onStarted()
loadFriends();
loadConferences();
+ loadGroups();
process(); // starts its own timer
}
@@ -530,6 +555,193 @@ void Core::onConferenceTitleChange(Tox* tox, uint32_t conferenceId, uint32_t pee
}
+void Core::onGroupInvite(Tox* tox, uint32_t friendId, const uint8_t* inviteData, size_t length,
+ const uint8_t* groupName, size_t groupNameLength, void* vCore)
+{
+ std::ignore = tox;
+ Core* core = static_cast(vCore);
+ const QByteArray data(reinterpret_cast(inviteData), length);
+ const QString name = ToxString(groupName, groupNameLength).getQString();
+ const GroupInvite inviteInfo(friendId, data, name);
+ qDebug() << "Group invite by friend" << friendId << "to group" << name;
+ emit core->groupInviteReceived(inviteInfo);
+}
+
+void Core::onGroupMessage(Tox* tox, uint32_t groupNumber, uint32_t peerId, Tox_Message_Type type,
+ const uint8_t* cMessage, size_t length, Tox_Group_Message_Id messageId,
+ void* vCore)
+{
+ std::ignore = tox;
+ std::ignore = messageId;
+ Core* core = static_cast(vCore);
+ const bool isAction = type == TOX_MESSAGE_TYPE_ACTION;
+ const QString message = ToxString(cMessage, length).getQString();
+ emit core->groupMessageReceived(groupNumber, peerId, message, isAction);
+}
+
+void Core::onGroupPrivateMessage(Tox* tox, uint32_t groupNumber, uint32_t peerId, Tox_Message_Type type,
+ const uint8_t* cMessage, size_t length, Tox_Group_Message_Id messageId,
+ void* vCore)
+{
+ std::ignore = tox;
+ std::ignore = messageId;
+ Core* core = static_cast(vCore);
+ const bool isAction = type == TOX_MESSAGE_TYPE_ACTION;
+ const QString message = ToxString(cMessage, length).getQString();
+ emit core->groupPrivateMessageReceived(groupNumber, peerId, message, isAction);
+}
+
+void Core::onGroupPeerJoin(Tox* tox, uint32_t groupNumber, uint32_t peerId, void* vCore)
+{
+ std::ignore = tox;
+ auto* const core = static_cast(vCore);
+ qDebug("Group %u peer %u joined", groupNumber, peerId);
+ ++core->groupPeerCounts[groupNumber];
+ core->stopGroupReconnectTimer(groupNumber);
+ emit core->groupPeerJoined(groupNumber, peerId);
+}
+
+void Core::onGroupPeerExit(Tox* tox, uint32_t groupNumber, uint32_t peerId, Tox_Group_Exit_Type exitType,
+ const uint8_t* name, size_t nameLength, const uint8_t* partMessage,
+ size_t partMessageLength, void* vCore)
+{
+ std::ignore = tox;
+ std::ignore = name;
+ std::ignore = nameLength;
+ std::ignore = partMessage;
+ std::ignore = partMessageLength;
+ auto* const core = static_cast(vCore);
+ qDebug("Group %u peer %u left, exit type %d", groupNumber, peerId, static_cast(exitType));
+ auto it = core->groupPeerCounts.find(groupNumber);
+ if (it != core->groupPeerCounts.end() && it.value() > 0) {
+ if (it.value() == 1) {
+ core->startGroupReconnectTimer(groupNumber);
+ core->groupPeerCounts.erase(it);
+ } else {
+ --it.value();
+ }
+ }
+ emit core->groupPeerExited(groupNumber, peerId);
+}
+
+void Core::onGroupPeerNameChange(Tox* tox, uint32_t groupNumber, uint32_t peerId,
+ const uint8_t* name, size_t length, void* vCore)
+{
+ std::ignore = tox;
+ const auto newName = ToxString(name, length).getQString();
+ qDebug().nospace() << "Group " << groupNumber << ", peer " << peerId << ", name " << newName;
+ auto* core = static_cast(vCore);
+ emit core->groupPeerNameChanged(groupNumber, peerId, newName);
+}
+
+void Core::onGroupPeerStatusChange(Tox* tox, uint32_t groupNumber, uint32_t peerId,
+ Tox_User_Status status, void* vCore)
+{
+ std::ignore = tox;
+ qDebug().nospace() << "Group " << groupNumber << ", peer " << peerId
+ << ", status " << static_cast(status);
+ auto* core = static_cast(vCore);
+ emit core->groupPeerStatusChanged(groupNumber, peerId, static_cast(status));
+}
+
+void Core::onGroupSelfJoin(Tox* tox, uint32_t groupNumber, void* vCore)
+{
+ std::ignore = tox;
+ auto* const core = static_cast(vCore);
+ qDebug("Joined group %u", groupNumber);
+ const GroupId groupId = core->getGroupPersistentId(groupNumber);
+ if (!groupId.isEmpty()) {
+ core->numberToGroupId[groupNumber] = groupId;
+ core->groupIdToNumber[groupId] = groupNumber;
+ core->startGroupReconnectTimer(groupNumber);
+ }
+ emit core->groupSelfJoined(groupNumber);
+}
+
+void Core::onGroupTopic(Tox* tox, uint32_t groupNumber, uint32_t peerId, const uint8_t* topic,
+ size_t length, void* vCore)
+{
+ std::ignore = tox;
+ std::ignore = peerId;
+ auto* const core = static_cast(vCore);
+ const QString newTopic = ToxString(topic, length).getQString();
+ qDebug().nospace() << "Group " << groupNumber << " topic changed to " << newTopic;
+ emit core->saveRequest();
+ emit core->groupTopicChanged(groupNumber, newTopic);
+}
+
+void Core::onGroupJoinFail(Tox* tox, uint32_t groupNumber, Tox_Group_Join_Fail failType, void* vCore)
+{
+ std::ignore = tox;
+ auto* const core = static_cast(vCore);
+ qWarning() << "Group join failed for group" << groupNumber << "with error:" << failType;
+ core->stopGroupReconnectTimer(groupNumber);
+ core->groupPeerCounts.remove(groupNumber);
+ const auto groupIdIt = core->numberToGroupId.find(groupNumber);
+ if (groupIdIt != core->numberToGroupId.end()) {
+ core->groupIdToNumber.remove(*groupIdIt);
+ core->numberToGroupId.erase(groupIdIt);
+ }
+ emit core->groupJoinFailed(groupNumber, failType);
+}
+
+void Core::onGroupModeration(Tox* tox, uint32_t groupNumber, uint32_t sourcePeerId,
+ uint32_t targetPeerId, Tox_Group_Mod_Event modType, void* vCore)
+{
+ std::ignore = tox;
+ std::ignore = sourcePeerId;
+ std::ignore = targetPeerId;
+ std::ignore = modType;
+ auto* const core = static_cast(vCore);
+ qDebug() << "Group" << groupNumber << "moderation event, refreshing peer roles";
+ emit core->groupPeerRolesChanged(groupNumber);
+}
+
+void Core::onGroupPassword(Tox* tox, uint32_t groupNumber, const uint8_t* password, size_t length,
+ void* vCore)
+{
+ std::ignore = tox;
+ std::ignore = password;
+ auto* const core = static_cast(vCore);
+ qDebug() << "Group" << groupNumber << "password changed, has password:" << (length != 0);
+ emit core->groupPasswordChanged(groupNumber, length != 0);
+}
+
+void Core::onGroupPeerLimit(Tox* tox, uint32_t groupNumber, uint32_t peerLimit, void* vCore)
+{
+ std::ignore = tox;
+ auto* const core = static_cast(vCore);
+ qDebug() << "Group" << groupNumber << "peer limit changed to" << peerLimit;
+ emit core->groupPeerLimitChanged(groupNumber, static_cast(peerLimit));
+}
+
+void Core::onGroupTopicLock(Tox* tox, uint32_t groupNumber, Tox_Group_Topic_Lock topicLock,
+ void* vCore)
+{
+ std::ignore = tox;
+ auto* const core = static_cast(vCore);
+ qDebug() << "Group" << groupNumber << "topic lock changed to" << topicLock;
+ emit core->groupTopicLockChanged(groupNumber, static_cast(topicLock));
+}
+
+void Core::onGroupVoiceState(Tox* tox, uint32_t groupNumber, Tox_Group_Voice_State voiceState,
+ void* vCore)
+{
+ std::ignore = tox;
+ auto* const core = static_cast(vCore);
+ qDebug() << "Group" << groupNumber << "voice state changed to" << voiceState;
+ emit core->groupVoiceStateChanged(groupNumber, static_cast(voiceState));
+}
+
+void Core::onGroupPrivacyState(Tox* tox, uint32_t groupNumber, Tox_Group_Privacy_State privacyState,
+ void* vCore)
+{
+ std::ignore = tox;
+ auto* const core = static_cast(vCore);
+ qDebug() << "Group" << groupNumber << "privacy state changed to" << privacyState;
+ emit core->groupPrivacyStateChanged(groupNumber, static_cast(privacyState));
+}
+
void Core::onReadReceiptCallback(Tox* tox, uint32_t friendId, uint32_t receipt, void* core)
{
std::ignore = tox;
@@ -696,6 +908,77 @@ void Core::changeConferenceTitle(uint32_t conferenceId, const QString& title)
}
}
+void Core::sendGroupMessageWithType(uint32_t groupNumber, const QString& message,
+ Tox_Message_Type type)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const int size = message.toUtf8().size();
+ const auto maxSize = static_cast(tox_group_max_message_length());
+ if (size > maxSize) {
+ qCritical() << "Core::sendGroupMessageWithType called with message of size:" << size
+ << "when max is:" << maxSize << ". Ignoring.";
+ return;
+ }
+
+ const ToxString cMsg(message);
+ Tox_Err_Group_Send_Message error;
+ tox_group_send_message(tox.get(), groupNumber, type, cMsg.data(), cMsg.size(), &error);
+ if (!PARSE_ERR(error)) {
+ emit groupSentFailed(groupNumber);
+ return;
+ }
+}
+
+void Core::sendGroupMessage(uint32_t groupNumber, const QString& message)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ sendGroupMessageWithType(groupNumber, message, TOX_MESSAGE_TYPE_NORMAL);
+}
+
+void Core::sendGroupAction(uint32_t groupNumber, const QString& message)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ sendGroupMessageWithType(groupNumber, message, TOX_MESSAGE_TYPE_ACTION);
+}
+
+void Core::sendGroupPrivateMessage(uint32_t groupNumber, uint32_t peerId, const QString& message,
+ Tox_Message_Type type)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const int size = message.toUtf8().size();
+ const auto maxSize = static_cast(tox_group_max_message_length());
+ if (size > maxSize) {
+ qCritical() << "Core::sendGroupPrivateMessage called with message of size:" << size
+ << "when max is:" << maxSize << ". Ignoring.";
+ return;
+ }
+
+ const ToxString cMsg(message);
+ Tox_Err_Group_Send_Private_Message error;
+ tox_group_send_private_message(tox.get(), groupNumber, peerId, type,
+ cMsg.data(), cMsg.size(), &error);
+ if (!PARSE_ERR(error)) {
+ emit groupSentFailed(groupNumber);
+ }
+}
+
+void Core::changeGroupTopic(uint32_t groupNumber, const QString& topic)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const ToxString cTopic(topic);
+ Tox_Err_Group_Topic_Set error;
+ tox_group_set_topic(tox.get(), groupNumber, cTopic.data(), cTopic.size(), &error);
+ if (PARSE_ERR(error)) {
+ emit saveRequest();
+ emit groupTopicChanged(groupNumber, topic);
+ }
+}
+
void Core::removeFriend(uint32_t friendId)
{
const QMutexLocker ml{&coreLoopLock};
@@ -980,6 +1263,62 @@ void Core::loadConferences()
}
}
+void Core::loadGroups()
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const uint32_t groupCount = tox_group_get_group_list_size(tox.get());
+ QVector numbers(groupCount);
+ tox_group_get_group_list(tox.get(), numbers.data());
+
+ QSet alreadyLoaded;
+ for (const uint32_t groupNumber : numbers) {
+ const GroupId groupId = getGroupPersistentId(groupNumber);
+ if (groupId.isEmpty()) {
+ continue;
+ }
+ alreadyLoaded.insert(groupId);
+ if (groupIdToNumber.contains(groupId)) {
+ continue;
+ }
+ numberToGroupId[groupNumber] = groupId;
+ groupIdToNumber[groupId] = groupNumber;
+ startGroupReconnectTimer(groupNumber);
+ emit groupJoined(groupNumber, groupId);
+ }
+
+ const QStringList saved = settings.getSavedGroups();
+ for (const QString& groupIdHex : saved) {
+ if (groupIdHex.isEmpty()) {
+ continue;
+ }
+ const QByteArray rawId = QByteArray::fromHex(groupIdHex.toLatin1());
+ if (rawId.size() != TOX_GROUP_CHAT_ID_SIZE) {
+ qWarning() << "loadGroups: invalid saved group id" << groupIdHex;
+ continue;
+ }
+ const GroupId groupId(rawId);
+ if (groupIdToNumber.contains(groupId) || alreadyLoaded.contains(groupId)) {
+ continue;
+ }
+
+ const ToxString cSelfName(getUsername());
+ Tox_Err_Group_Join error;
+ const uint32_t groupNumber =
+ tox_group_join(tox.get(), groupId.getData(), cSelfName.data(), cSelfName.size(), nullptr,
+ 0, &error);
+ if (!PARSE_ERR(error)) {
+ qWarning() << "loadGroups: failed to rejoin group" << groupIdHex;
+ continue;
+ }
+
+ numberToGroupId[groupNumber] = groupId;
+ groupIdToNumber[groupId] = groupNumber;
+ startGroupReconnectTimer(groupNumber);
+ emit groupJoined(groupNumber, groupId);
+ }
+}
+
void Core::checkLastOnline(uint32_t friendId)
{
const QMutexLocker ml{&coreLoopLock};
@@ -1126,74 +1465,526 @@ bool Core::getConferenceAvEnabled(int conferenceId) const
return type == TOX_CONFERENCE_TYPE_AV;
}
+GroupId Core::getGroupPersistentId(uint32_t groupNumber) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ QByteArray idBuff(tox_group_chat_id_size(), 0x00);
+ Tox_Err_Group_State_Query error;
+ if (tox_group_get_chat_id(tox.get(), groupNumber, reinterpret_cast(idBuff.data()), &error)) {
+ return GroupId{reinterpret_cast(idBuff.data())};
+ }
+ qCritical() << "Failed to get chat id of group" << groupNumber;
+ return {};
+}
+
/**
- * @brief Accept a conference invite.
- * @param inviteInfo Object which contains info about conference invitation
- *
- * @return Conference number on success, UINT32_MAX on failure.
+ * @brief Get the number of peers in a group.
+ * @return The number of peers in the group. UINT32_MAX on failure.
*/
-uint32_t Core::joinConference(const ConferenceInvite& inviteInfo)
+uint32_t Core::getGroupNumberPeers(int groupNumber) const
{
const QMutexLocker ml{&coreLoopLock};
- const uint32_t friendId = inviteInfo.getFriendId();
- const uint8_t confType = inviteInfo.getType();
- const QByteArray invite = inviteInfo.getInvite();
- const auto* const cookie = reinterpret_cast(invite.data());
- const size_t cookieLength = invite.length();
- uint32_t conferenceNum{std::numeric_limits::max()};
- switch (confType) {
- case TOX_CONFERENCE_TYPE_TEXT: {
- qDebug() << "Trying to accept invite for text conference sent by friend" << friendId;
- Tox_Err_Conference_Join error;
- conferenceNum = tox_conference_join(tox.get(), friendId, cookie, cookieLength, &error);
- if (!PARSE_ERR(error)) {
- conferenceNum = std::numeric_limits::max();
- }
- break;
- }
- case TOX_CONFERENCE_TYPE_AV: {
- qDebug() << "Trying to join AV conference invite sent by friend" << friendId;
- conferenceNum = toxav_join_av_groupchat(tox.get(), friendId, cookie, cookieLength,
- CoreAV::conferenceCallCallback, this);
- break;
+ // NGC has no public peer enumeration API. The peer count is tracked
+ // client-side by the Group model.
+ std::ignore = groupNumber;
+ return std::numeric_limits::max();
+}
+
+/**
+ * @brief Get the self peer id of a group.
+ * @return The self peer id on success, UINT32_MAX on failure.
+ */
+uint32_t Core::getGroupSelfPeerId(int groupNumber) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Self_Query error;
+ const uint32_t peerId = tox_group_self_get_peer_id(tox.get(), groupNumber, &error);
+ if (!PARSE_ERR(error)) {
+ return std::numeric_limits::max();
}
- default:
- qWarning() << "joinConference: Unknown conference type" << confType;
+
+ return peerId;
+}
+
+/**
+ * @brief Get the name of a peer of a group
+ */
+QString Core::getGroupPeerName(int groupNumber, int peerId) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Peer_Query error;
+ const size_t length = tox_group_peer_get_name_size(tox.get(), groupNumber, peerId, &error);
+ if (!PARSE_ERR(error) || (length == 0u)) {
+ return QString{};
}
- if (conferenceNum != std::numeric_limits::max()) {
- emit saveRequest();
- emit conferenceJoined(conferenceNum, getConferencePersistentId(conferenceNum));
+
+ QByteArray nameBuf(length, 0x00);
+ tox_group_peer_get_name(tox.get(), groupNumber, peerId, reinterpret_cast(nameBuf.data()), &error);
+ if (!PARSE_ERR(error)) {
+ return QString{};
}
- return conferenceNum;
+
+ return ToxString(reinterpret_cast(nameBuf.data()), length).getQString();
}
-void Core::conferenceInviteFriend(uint32_t friendId, int conferenceId)
+/**
+ * @brief Get the public key of a peer of a group
+ */
+ToxPk Core::getGroupPeerPk(int groupNumber, int peerId) const
{
const QMutexLocker ml{&coreLoopLock};
- Tox_Err_Conference_Invite error;
- tox_conference_invite(tox.get(), friendId, conferenceId, &error);
- PARSE_ERR(error);
+ Tox_Err_Group_Self_Query selfError;
+ const uint32_t selfPeerId = tox_group_self_get_peer_id(tox.get(), groupNumber, &selfError);
+ if (PARSE_ERR(selfError) && selfPeerId == static_cast(peerId)) {
+ return getGroupSelfPk(groupNumber);
+ }
+
+ QByteArray peerPk(tox_public_key_size(), 0x00);
+ Tox_Err_Group_Peer_Query error;
+ tox_group_peer_get_public_key(tox.get(), groupNumber, peerId, reinterpret_cast(peerPk.data()), &error);
+ if (!PARSE_ERR(error)) {
+ return ToxPk{};
+ }
+
+ return ToxPk(reinterpret_cast(peerPk.data()));
}
-int Core::createConference(uint8_t type)
+/**
+ * @brief Get the public key identifying us in a group.
+ *
+ * Unlike friend chats, our identity within a group is a dedicated per-group
+ * peer public key generated by toxcore, not our profile's public key.
+ */
+ToxPk Core::getGroupSelfPk(int groupNumber) const
{
const QMutexLocker ml{&coreLoopLock};
- if (type == TOX_CONFERENCE_TYPE_TEXT) {
- Tox_Err_Conference_New error;
- const uint32_t conferenceId = tox_conference_new(tox.get(), &error);
- if (PARSE_ERR(error)) {
- emit saveRequest();
- emit emptyConferenceCreated(conferenceId, getConferencePersistentId(conferenceId));
- return conferenceId;
- }
- return std::numeric_limits::max();
+ QByteArray selfPk(tox_public_key_size(), 0x00);
+ Tox_Err_Group_Self_Query error;
+ tox_group_self_get_public_key(tox.get(), groupNumber,
+ reinterpret_cast(selfPk.data()), &error);
+ if (!PARSE_ERR(error)) {
+ return ToxPk{};
}
- if (type == TOX_CONFERENCE_TYPE_AV) {
- // unlike tox_conference_new, toxav_add_av_groupchat does not have an error enum, so -1
- // conference number is our only indication of an error
+
+ return ToxPk(reinterpret_cast(selfPk.data()));
+}
+
+/**
+ * @brief Get the role of a peer in a group
+ */
+GroupRole Core::getGroupPeerRole(int groupNumber, int peerId) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Peer_Query error;
+ const Tox_Group_Role role = tox_group_peer_get_role(tox.get(), groupNumber, peerId, &error);
+ if (!PARSE_ERR(error)) {
+ return GroupRole::Unknown;
+ }
+
+ return static_cast(role);
+}
+
+bool Core::setGroupPeerRole(int groupNumber, int peerId, GroupRole role)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const auto toxRole = static_cast(role);
+ Tox_Err_Group_Set_Role error;
+ const bool success = tox_group_set_role(tox.get(), groupNumber, peerId, toxRole, &error);
+ if (!success) {
+ qWarning() << "Failed to set role" << static_cast(role) << "for peer" << peerId
+ << "in group" << groupNumber << ":" << tox_err_group_set_role_to_string(error);
+ return false;
+ }
+
+ emit groupPeerRolesChanged(groupNumber);
+ return true;
+}
+
+bool Core::kickGroupPeer(int groupNumber, int peerId)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Kick_Peer error;
+ const bool success = tox_group_kick_peer(tox.get(), groupNumber, peerId, &error);
+ if (!success) {
+ qWarning() << "Failed to kick peer" << peerId << "from group" << groupNumber << ":"
+ << tox_err_group_kick_peer_to_string(error);
+ return false;
+ }
+
+ return true;
+}
+
+bool Core::setGroupPassword(int groupNumber, const QByteArray& password)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ if (password.size() > TOX_GROUP_MAX_PASSWORD_SIZE) {
+ qWarning() << "Failed to set password for group" << groupNumber << ": password too long";
+ return false;
+ }
+
+ const auto* const passwordData =
+ password.isEmpty() ? nullptr : reinterpret_cast(password.constData());
+ Tox_Err_Group_Set_Password error;
+ const bool success = tox_group_set_password(tox.get(), groupNumber, passwordData, password.size(),
+ &error);
+ if (!success) {
+ qWarning() << "Failed to set password for group" << groupNumber << ":"
+ << tox_err_group_set_password_to_string(error);
+ return false;
+ }
+
+ emit groupPasswordChanged(groupNumber, !password.isEmpty());
+ return true;
+}
+
+bool Core::setGroupPeerLimit(int groupNumber, uint16_t peerLimit)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Set_Peer_Limit error;
+ const bool success = tox_group_set_peer_limit(tox.get(), groupNumber, peerLimit, &error);
+ if (!success) {
+ qWarning() << "Failed to set peer limit" << peerLimit << "for group" << groupNumber << ":"
+ << tox_err_group_set_peer_limit_to_string(error);
+ return false;
+ }
+
+ emit groupPeerLimitChanged(groupNumber, peerLimit);
+ return true;
+}
+
+bool Core::setGroupTopicLock(int groupNumber, GroupTopicLock topicLock)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const auto toxTopicLock = static_cast(topicLock);
+ Tox_Err_Group_Set_Topic_Lock error;
+ const bool success = tox_group_set_topic_lock(tox.get(), groupNumber, toxTopicLock, &error);
+ if (!success) {
+ qWarning() << "Failed to set topic lock" << static_cast(topicLock) << "for group"
+ << groupNumber << ":" << tox_err_group_set_topic_lock_to_string(error);
+ return false;
+ }
+
+ emit groupTopicLockChanged(groupNumber, topicLock);
+ return true;
+}
+
+bool Core::setGroupVoiceState(int groupNumber, GroupVoiceState voiceState)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const auto toxVoiceState = static_cast(voiceState);
+ Tox_Err_Group_Set_Voice_State error;
+ const bool success = tox_group_set_voice_state(tox.get(), groupNumber, toxVoiceState, &error);
+ if (!success) {
+ qWarning() << "Failed to set voice state" << static_cast(voiceState) << "for group"
+ << groupNumber << ":" << tox_err_group_set_voice_state_to_string(error);
+ return false;
+ }
+
+ emit groupVoiceStateChanged(groupNumber, voiceState);
+ return true;
+}
+
+bool Core::setGroupPrivacyState(int groupNumber, GroupPrivacyState privacyState)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const auto toxPrivacyState = static_cast(privacyState);
+ Tox_Err_Group_Set_Privacy_State error;
+ const bool success = tox_group_set_privacy_state(tox.get(), groupNumber, toxPrivacyState, &error);
+ if (!success) {
+ qWarning() << "Failed to set privacy state" << static_cast(privacyState) << "for group"
+ << groupNumber << ":" << tox_err_group_set_privacy_state_to_string(error);
+ return false;
+ }
+
+ emit groupPrivacyStateChanged(groupNumber, privacyState);
+ return true;
+}
+
+bool Core::getGroupHasPassword(int groupNumber) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_State_Query error;
+ const size_t passwordSize = tox_group_get_password_size(tox.get(), groupNumber, &error);
+ if (PARSE_ERR(error)) {
+ return passwordSize != 0;
+ }
+
+ return false;
+}
+
+uint16_t Core::getGroupPeerLimit(int groupNumber) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_State_Query error;
+ const uint16_t peerLimit = tox_group_get_peer_limit(tox.get(), groupNumber, &error);
+ if (PARSE_ERR(error)) {
+ return peerLimit;
+ }
+
+ return 0;
+}
+
+GroupTopicLock Core::getGroupTopicLock(int groupNumber) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_State_Query error;
+ const auto topicLock = tox_group_get_topic_lock(tox.get(), groupNumber, &error);
+ if (PARSE_ERR(error)) {
+ return static_cast(topicLock);
+ }
+
+ return GroupTopicLock::Unknown;
+}
+
+GroupVoiceState Core::getGroupVoiceState(int groupNumber) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_State_Query error;
+ const auto voiceState = tox_group_get_voice_state(tox.get(), groupNumber, &error);
+ if (PARSE_ERR(error)) {
+ return static_cast(voiceState);
+ }
+
+ return GroupVoiceState::Unknown;
+}
+
+GroupPrivacyState Core::getGroupPrivacyState(int groupNumber) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_State_Query error;
+ const auto privacyState = tox_group_get_privacy_state(tox.get(), groupNumber, &error);
+ if (PARSE_ERR(error)) {
+ return static_cast(privacyState);
+ }
+
+ return GroupPrivacyState::Unknown;
+}
+
+/**
+ * @brief Get the name of a group
+ */
+QString Core::getGroupTitle(int groupNumber) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_State_Query error;
+ const size_t length = tox_group_get_name_size(tox.get(), groupNumber, &error);
+ if (!PARSE_ERR(error) || (length == 0u)) {
+ return QString{};
+ }
+
+ QByteArray nameBuf(length, 0x00);
+ tox_group_get_name(tox.get(), groupNumber, reinterpret_cast(nameBuf.data()), &error);
+ if (!PARSE_ERR(error)) {
+ return QString{};
+ }
+
+ return ToxString(reinterpret_cast(nameBuf.data()), length).getQString();
+}
+
+/**
+ * @brief Get the topic of a group
+ */
+QString Core::getGroupTopic(int groupNumber) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_State_Query error;
+ const size_t length = tox_group_get_topic_size(tox.get(), groupNumber, &error);
+ if (!PARSE_ERR(error) || (length == 0u)) {
+ return QString{};
+ }
+
+ QByteArray topicBuf(length, 0x00);
+ tox_group_get_topic(tox.get(), groupNumber, reinterpret_cast(topicBuf.data()), &error);
+ if (!PARSE_ERR(error)) {
+ return QString{};
+ }
+
+ return ToxString(reinterpret_cast(topicBuf.data()), length).getQString();
+}
+
+/**
+ * @brief Get the self name in a group
+ */
+QString Core::getGroupSelfName(int groupNumber) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Self_Query error;
+ const size_t length = tox_group_self_get_name_size(tox.get(), groupNumber, &error);
+ if (!PARSE_ERR(error) || (length == 0u)) {
+ return QString{};
+ }
+
+ QByteArray nameBuf(length, 0x00);
+ tox_group_self_get_name(tox.get(), groupNumber, reinterpret_cast(nameBuf.data()), &error);
+ if (!PARSE_ERR(error)) {
+ return QString{};
+ }
+
+ return ToxString(reinterpret_cast(nameBuf.data()), length).getQString();
+}
+
+/**
+ * @brief Set the self name in a group
+ */
+bool Core::setGroupSelfName(int groupNumber, const QString& name)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const ToxString toxName(name);
+ Tox_Err_Group_Self_Name_Set error;
+ const bool success = tox_group_self_set_name(tox.get(), groupNumber,
+ toxName.data(),
+ toxName.size(), &error);
+ if (!success) {
+ qWarning() << "Failed to set group self name for group" << groupNumber << ":"
+ << static_cast(error);
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * @brief Get the self status in a group
+ */
+Status::Status Core::getGroupSelfStatus(int groupNumber) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Self_Query error;
+ const Tox_User_Status status = tox_group_self_get_status(tox.get(), groupNumber, &error);
+ if (!PARSE_ERR(error)) {
+ return Status::Status::Offline;
+ }
+
+ return static_cast(status);
+}
+
+/**
+ * @brief Set the self status in a group
+ */
+bool Core::setGroupSelfStatus(int groupNumber, Status::Status status)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Self_Status_Set error;
+ const bool success = tox_group_self_set_status(tox.get(), groupNumber,
+ static_cast(status), &error);
+ if (!success) {
+ qWarning() << "Failed to set group self status for group" << groupNumber << ":"
+ << static_cast(error);
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * @brief Get the status of a peer in a group
+ */
+Status::Status Core::getGroupPeerStatus(int groupNumber, int peerId) const
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Peer_Query error;
+ const Tox_User_Status status = tox_group_peer_get_status(tox.get(), groupNumber, peerId, &error);
+ if (!PARSE_ERR(error)) {
+ return Status::Status::Offline;
+ }
+
+ return static_cast(status);
+}
+
+/**
+ * @brief Accept a conference invite.
+ * @param inviteInfo Object which contains info about conference invitation
+ *
+ * @return Conference number on success, UINT32_MAX on failure.
+ */
+uint32_t Core::joinConference(const ConferenceInvite& inviteInfo)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const uint32_t friendId = inviteInfo.getFriendId();
+ const uint8_t confType = inviteInfo.getType();
+ const QByteArray invite = inviteInfo.getInvite();
+ const auto* const cookie = reinterpret_cast(invite.data());
+ const size_t cookieLength = invite.length();
+ uint32_t conferenceNum{std::numeric_limits::max()};
+ switch (confType) {
+ case TOX_CONFERENCE_TYPE_TEXT: {
+ qDebug() << "Trying to accept invite for text conference sent by friend" << friendId;
+ Tox_Err_Conference_Join error;
+ conferenceNum = tox_conference_join(tox.get(), friendId, cookie, cookieLength, &error);
+ if (!PARSE_ERR(error)) {
+ conferenceNum = std::numeric_limits::max();
+ }
+ break;
+ }
+ case TOX_CONFERENCE_TYPE_AV: {
+ qDebug() << "Trying to join AV conference invite sent by friend" << friendId;
+ conferenceNum = toxav_join_av_groupchat(tox.get(), friendId, cookie, cookieLength,
+ CoreAV::conferenceCallCallback, this);
+ break;
+ }
+ default:
+ qWarning() << "joinConference: Unknown conference type" << confType;
+ }
+ if (conferenceNum != std::numeric_limits::max()) {
+ emit saveRequest();
+ emit conferenceJoined(conferenceNum, getConferencePersistentId(conferenceNum));
+ }
+ return conferenceNum;
+}
+
+void Core::conferenceInviteFriend(uint32_t friendId, int conferenceId)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Conference_Invite error;
+ tox_conference_invite(tox.get(), friendId, conferenceId, &error);
+ PARSE_ERR(error);
+}
+
+int Core::createConference(uint8_t type)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ if (type == TOX_CONFERENCE_TYPE_TEXT) {
+ Tox_Err_Conference_New error;
+ const uint32_t conferenceId = tox_conference_new(tox.get(), &error);
+ if (PARSE_ERR(error)) {
+ emit saveRequest();
+ emit emptyConferenceCreated(conferenceId, getConferencePersistentId(conferenceId));
+ return conferenceId;
+ }
+ return std::numeric_limits::max();
+ }
+ if (type == TOX_CONFERENCE_TYPE_AV) {
+ // unlike tox_conference_new, toxav_add_av_groupchat does not have an error enum, so -1
+ // conference number is our only indication of an error
const int conferenceId =
toxav_add_av_groupchat(tox.get(), CoreAV::conferenceCallCallback, this);
if (conferenceId != -1) {
@@ -1208,6 +1999,203 @@ int Core::createConference(uint8_t type)
return -1;
}
+void Core::groupInviteFriend(uint32_t friendId, int groupNumber)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Invite_Friend error;
+ tox_group_invite_friend(tox.get(), groupNumber, friendId, &error);
+ if (!PARSE_ERR(error)) {
+ qWarning() << "Failed to invite friend" << friendId << "to group" << groupNumber;
+ }
+}
+
+int Core::createGroup(const QString& groupName)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const ToxString cName(groupName);
+ const ToxString cSelfName(getUsername());
+ Tox_Err_Group_New error;
+ const uint32_t groupNumber =
+ tox_group_new(tox.get(), TOX_GROUP_PRIVACY_STATE_PUBLIC, cName.data(), cName.size(),
+ cSelfName.data(), cSelfName.size(), &error);
+ if (!PARSE_ERR(error)) {
+ qCritical() << "Failed to create group";
+ return -1;
+ }
+
+ const GroupId groupId = getGroupPersistentId(groupNumber);
+ numberToGroupId[groupNumber] = groupId;
+ groupIdToNumber[groupId] = groupNumber;
+ startGroupReconnectTimer(groupNumber);
+
+ emit saveRequest();
+ emit emptyGroupCreated(groupNumber, groupId, groupName);
+ return groupNumber;
+}
+
+/**
+ * @brief Accept a group invite.
+ * @param inviteInfo Object which contains info about group invitation
+ *
+ * @return Group number on success, UINT32_MAX on failure.
+ */
+uint32_t Core::joinGroup(const GroupInvite& inviteInfo)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ const uint32_t friendId = inviteInfo.getFriendId();
+ const QByteArray invite = inviteInfo.getInviteData();
+ const auto* const inviteData = reinterpret_cast(invite.constData());
+ const size_t inviteLength = invite.size();
+
+ if (inviteLength < TOX_GROUP_CHAT_ID_SIZE) {
+ qWarning() << "joinGroup: invite data too short";
+ return std::numeric_limits::max();
+ }
+
+ const GroupId groupId(inviteData);
+ if (groupIdToNumber.contains(groupId)) {
+ qDebug() << "joinGroup: already in group" << groupId.toString();
+ return groupIdToNumber[groupId];
+ }
+
+ const ToxString cSelfName(getUsername());
+
+ qDebug() << "Trying to accept invite for group sent by friend" << friendId;
+ Tox_Err_Group_Invite_Accept error;
+ const uint32_t groupNumber =
+ tox_group_invite_accept(tox.get(), friendId, inviteData, inviteLength, cSelfName.data(),
+ cSelfName.size(), nullptr, 0, &error);
+ if (!PARSE_ERR(error)) {
+ qWarning() << "Failed to accept group invite from friend" << friendId;
+ return std::numeric_limits::max();
+ }
+
+ numberToGroupId[groupNumber] = groupId;
+ groupIdToNumber[groupId] = groupNumber;
+ startGroupReconnectTimer(groupNumber);
+
+ emit saveRequest();
+ emit groupJoined(groupNumber, groupId);
+ return groupNumber;
+}
+
+/**
+ * @brief Join an NGC group by its chat id.
+ * @param groupId Chat ID of the group to join.
+ * @return Group number on success, -1 on failure.
+ */
+int Core::joinGroup(const GroupId& groupId)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ if (groupId.isEmpty()) {
+ qWarning() << "joinGroup: empty group id";
+ return -1;
+ }
+
+ if (groupIdToNumber.contains(groupId)) {
+ return groupIdToNumber[groupId];
+ }
+
+ const ToxString cSelfName(getUsername());
+ Tox_Err_Group_Join error;
+ const uint32_t groupNumber =
+ tox_group_join(tox.get(), groupId.getData(), cSelfName.data(), cSelfName.size(), nullptr, 0,
+ &error);
+ if (!PARSE_ERR(error)) {
+ qWarning() << "Failed to join group" << groupId.toString();
+ return -1;
+ }
+
+ numberToGroupId[groupNumber] = groupId;
+ groupIdToNumber[groupId] = groupNumber;
+ startGroupReconnectTimer(groupNumber);
+
+ emit saveRequest();
+ emit groupJoined(groupNumber, groupId);
+ return groupNumber;
+}
+
+void Core::quitGroup(int groupNumber)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Leave error;
+ tox_group_leave(tox.get(), groupNumber, nullptr, 0, &error);
+ if (PARSE_ERR(error)) {
+ const auto groupIdIt = numberToGroupId.find(groupNumber);
+ if (groupIdIt != numberToGroupId.end()) {
+ groupIdToNumber.remove(*groupIdIt);
+ numberToGroupId.erase(groupIdIt);
+ }
+ stopGroupReconnectTimer(groupNumber);
+ groupPeerCounts.remove(groupNumber);
+ emit saveRequest();
+ emit groupSelfDisconnected(groupNumber);
+ }
+}
+
+bool Core::reconnectGroup(uint32_t groupNumber)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ Tox_Err_Group_Reconnect error;
+ const bool success = tox_group_reconnect(tox.get(), groupNumber, &error);
+ if (!success) {
+ qWarning() << "Failed to reconnect group" << groupNumber << ":"
+ << tox_err_group_reconnect_to_string(error);
+ }
+ return success;
+}
+
+void Core::retryGroupReconnect(uint32_t groupNumber)
+{
+ const QMutexLocker ml{&coreLoopLock};
+
+ if (!numberToGroupId.contains(groupNumber)) {
+ stopGroupReconnectTimer(groupNumber);
+ return;
+ }
+
+ // group has other members, toxcore keeps it connected on its own
+ if (groupPeerCounts.value(groupNumber, 0) > 0) {
+ stopGroupReconnectTimer(groupNumber);
+ return;
+ }
+
+ reconnectGroup(groupNumber);
+}
+
+void Core::startGroupReconnectTimer(uint32_t groupNumber)
+{
+ QMetaObject::invokeMethod(this, [this, groupNumber] {
+ if (groupReconnectTimers.contains(groupNumber)) {
+ return;
+ }
+
+ auto* timer = new QTimer(this);
+ timer->setInterval(GROUP_RECONNECT_INTERVAL_MS);
+ connect(timer, &QTimer::timeout, this,
+ [this, groupNumber] { retryGroupReconnect(groupNumber); });
+ groupReconnectTimers[groupNumber] = timer;
+ timer->start();
+ });
+}
+
+void Core::stopGroupReconnectTimer(uint32_t groupNumber)
+{
+ QMetaObject::invokeMethod(this, [this, groupNumber] {
+ auto it = groupReconnectTimers.find(groupNumber);
+ if (it != groupReconnectTimers.end()) {
+ it.value()->deleteLater();
+ groupReconnectTimers.erase(it);
+ }
+ });
+}
+
/**
* @brief Checks if a friend is online. Unknown friends are considered offline.
*/
diff --git a/src/core/core.h b/src/core/core.h
index 62a81c6272..0a898a4998 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -7,9 +7,12 @@
#pragma once
#include "conferenceid.h"
+#include "groupid.h"
#include "icoreconferencemessagesender.h"
#include "icoreconferencequery.h"
#include "icorefriendmessagesender.h"
+#include "icoregroupmessagesender.h"
+#include "icoregroupquery.h"
#include "icoreidhandler.h"
#include "receiptnum.h"
#include "toxfile.h"
@@ -19,6 +22,7 @@
#include "src/model/conferenceinvite.h"
#include "src/model/status.h"
+#include
#include
#include
#include
@@ -31,6 +35,8 @@ class CoreAV;
class CoreFile;
class IAudioControl;
class ICoreSettings;
+class ConferenceInvite;
+class GroupInvite;
class Profile;
class Core;
class IBootstrapListGenerator;
@@ -42,7 +48,9 @@ class Core : public QObject,
public ICoreFriendMessageSender,
public ICoreIdHandler,
public ICoreConferenceMessageSender,
- public ICoreConferenceQuery
+ public ICoreConferenceQuery,
+ public ICoreGroupMessageSender,
+ public ICoreGroupQuery
{
Q_OBJECT
public:
@@ -81,11 +89,32 @@ class Core : public QObject,
ToxPk getFriendPublicKey(uint32_t friendNumber) const;
QString getFriendUsername(uint32_t friendNumber) const;
+ uint32_t getGroupNumberPeers(int groupNumber) const;
+ uint32_t getGroupSelfPeerId(int groupNumber) const override;
+ QString getGroupPeerName(int groupNumber, int peerId) const override;
+ ToxPk getGroupPeerPk(int groupNumber, int peerId) const override;
+ ToxPk getGroupSelfPk(int groupNumber) const override;
+ QString getGroupTitle(int groupNumber) const override;
+ QString getGroupTopic(int groupNumber) const override;
+ QString getGroupSelfName(int groupNumber) const override;
+ bool setGroupSelfName(int groupNumber, const QString& name) override;
+ Status::Status getGroupSelfStatus(int groupNumber) const override;
+ bool setGroupSelfStatus(int groupNumber, Status::Status status) override;
+ Status::Status getGroupPeerStatus(int groupNumber, int peerId) const override;
+ GroupRole getGroupPeerRole(int groupNumber, int peerId) const override;
+ bool setGroupPeerRole(int groupNumber, int peerId, GroupRole role) override;
+ bool kickGroupPeer(int groupNumber, int peerId) override;
+ GroupId getGroupPersistentId(uint32_t groupNumber) const;
+ bool reconnectGroup(uint32_t groupNumber);
+
bool isFriendOnline(uint32_t friendId) const;
bool hasFriendWithPublicKey(const ToxPk& publicKey) const;
uint32_t joinConference(const ConferenceInvite& inviteInfo);
void quitConference(int conferenceId) const;
+ uint32_t joinGroup(const GroupInvite& inviteInfo);
+ int joinGroup(const GroupId& groupId);
+
QString getUsername() const override;
Status::Status getStatus() const;
QString getStatusMessage() const;
@@ -105,6 +134,21 @@ public slots:
void conferenceInviteFriend(uint32_t friendId, int conferenceId);
int createConference(uint8_t type = TOX_CONFERENCE_TYPE_AV);
+ void groupInviteFriend(uint32_t friendId, int groupNumber);
+ int createGroup(const QString& groupName);
+ void quitGroup(int groupNumber);
+ void changeGroupTopic(uint32_t groupNumber, const QString& topic);
+ bool setGroupPassword(int groupNumber, const QByteArray& password) override;
+ bool setGroupPeerLimit(int groupNumber, uint16_t peerLimit) override;
+ bool setGroupTopicLock(int groupNumber, GroupTopicLock topicLock) override;
+ bool setGroupVoiceState(int groupNumber, GroupVoiceState voiceState) override;
+ bool setGroupPrivacyState(int groupNumber, GroupPrivacyState privacyState) override;
+ bool getGroupHasPassword(int groupNumber) const override;
+ uint16_t getGroupPeerLimit(int groupNumber) const override;
+ GroupTopicLock getGroupTopicLock(int groupNumber) const override;
+ GroupVoiceState getGroupVoiceState(int groupNumber) const override;
+ GroupPrivacyState getGroupPrivacyState(int groupNumber) const override;
+
void removeFriend(uint32_t friendId);
void removeConference(int conferenceId);
@@ -119,6 +163,11 @@ public slots:
bool sendAction(uint32_t friendId, const QString& action, ReceiptNum& receipt) override;
void sendTyping(uint32_t friendId, bool typing);
+ void sendGroupMessage(uint32_t groupNumber, const QString& message) override;
+ void sendGroupAction(uint32_t groupNumber, const QString& message) override;
+ void sendGroupPrivateMessage(uint32_t groupNumber, uint32_t peerId,
+ const QString& message, Tox_Message_Type type) override;
+
void setNospam(uint32_t nospam);
signals:
@@ -177,6 +226,30 @@ public slots:
void conferenceJoined(uint32_t conferencenumber, ConferenceId conferenceId);
void actionSentResult(uint32_t friendId, const QString& action, int success);
+ void emptyGroupCreated(uint32_t groupNumber, GroupId groupId, const QString& groupName);
+ void groupInviteReceived(const GroupInvite& inviteInfo);
+ void groupMessageReceived(uint32_t groupNumber, uint32_t peerId, const QString& message,
+ bool isAction);
+ void groupPrivateMessageReceived(uint32_t groupNumber, uint32_t peerId, const QString& message,
+ bool isAction);
+ void groupPeerJoined(uint32_t groupNumber, uint32_t peerId);
+ void groupPeerExited(uint32_t groupNumber, uint32_t peerId);
+ void groupPeerNameChanged(uint32_t groupNumber, uint32_t peerId, const QString& newName);
+ void groupPeerStatusChanged(uint32_t groupNumber, uint32_t peerId, Status::Status status);
+ void groupTitleChanged(uint32_t groupNumber, const QString& author, const QString& title);
+ void groupTopicChanged(uint32_t groupNumber, const QString& topic);
+ void groupSentFailed(uint32_t groupNumber);
+ void groupJoined(uint32_t groupNumber, GroupId groupId);
+ void groupSelfJoined(uint32_t groupNumber);
+ void groupSelfDisconnected(uint32_t groupNumber);
+ void groupJoinFailed(uint32_t groupNumber, Tox_Group_Join_Fail failType);
+ void groupPeerRolesChanged(uint32_t groupNumber);
+ void groupPasswordChanged(uint32_t groupNumber, bool hasPassword);
+ void groupPeerLimitChanged(uint32_t groupNumber, uint16_t peerLimit);
+ void groupTopicLockChanged(uint32_t groupNumber, GroupTopicLock topicLock);
+ void groupVoiceStateChanged(uint32_t groupNumber, GroupVoiceState voiceState);
+ void groupPrivacyStateChanged(uint32_t groupNumber, GroupPrivacyState privacyState);
+
void receiptReceived(uint32_t friendId, ReceiptNum receipt);
void failedToRemoveFriend(uint32_t friendId);
@@ -209,9 +282,44 @@ public slots:
static void onConferenceTitleChange(Tox* tox, uint32_t conferenceId, uint32_t peerId,
const uint8_t* cTitle, size_t length, void* vCore);
+ static void onGroupInvite(Tox* tox, uint32_t friendId, const uint8_t* inviteData,
+ size_t length, const uint8_t* groupName, size_t groupNameLength,
+ void* vCore);
+ static void onGroupMessage(Tox* tox, uint32_t groupNumber, uint32_t peerId,
+ Tox_Message_Type type, const uint8_t* cMessage, size_t length,
+ Tox_Group_Message_Id messageId, void* vCore);
+ static void onGroupPrivateMessage(Tox* tox, uint32_t groupNumber, uint32_t peerId,
+ Tox_Message_Type type, const uint8_t* cMessage, size_t length,
+ Tox_Group_Message_Id messageId, void* vCore);
+ static void onGroupPeerJoin(Tox* tox, uint32_t groupNumber, uint32_t peerId, void* vCore);
+ static void onGroupPeerExit(Tox* tox, uint32_t groupNumber, uint32_t peerId,
+ Tox_Group_Exit_Type exitType, const uint8_t* name, size_t nameLength,
+ const uint8_t* partMessage, size_t partMessageLength, void* vCore);
+ static void onGroupPeerNameChange(Tox* tox, uint32_t groupNumber, uint32_t peerId,
+ const uint8_t* name, size_t length, void* vCore);
+ static void onGroupPeerStatusChange(Tox* tox, uint32_t groupNumber, uint32_t peerId,
+ Tox_User_Status status, void* vCore);
+ static void onGroupSelfJoin(Tox* tox, uint32_t groupNumber, void* vCore);
+ static void onGroupTopic(Tox* tox, uint32_t groupNumber, uint32_t peerId, const uint8_t* topic,
+ size_t length, void* vCore);
+ static void onGroupJoinFail(Tox* tox, uint32_t groupNumber, Tox_Group_Join_Fail failType,
+ void* vCore);
+ static void onGroupModeration(Tox* tox, uint32_t groupNumber, uint32_t sourcePeerId,
+ uint32_t targetPeerId, Tox_Group_Mod_Event modType, void* vCore);
+ static void onGroupPassword(Tox* tox, uint32_t groupNumber, const uint8_t* password,
+ size_t length, void* vCore);
+ static void onGroupPeerLimit(Tox* tox, uint32_t groupNumber, uint32_t peerLimit, void* vCore);
+ static void onGroupTopicLock(Tox* tox, uint32_t groupNumber, Tox_Group_Topic_Lock topicLock,
+ void* vCore);
+ static void onGroupVoiceState(Tox* tox, uint32_t groupNumber, Tox_Group_Voice_State voiceState,
+ void* vCore);
+ static void onGroupPrivacyState(Tox* tox, uint32_t groupNumber,
+ Tox_Group_Privacy_State privacyState, void* vCore);
+
static void onReadReceiptCallback(Tox* tox, uint32_t friendId, uint32_t receipt, void* core);
void sendConferenceMessageWithType(int conferenceId, const QString& message, Tox_Message_Type type);
+ void sendGroupMessageWithType(uint32_t groupNumber, const QString& message, Tox_Message_Type type);
bool sendMessageWithType(uint32_t friendId, const QString& message, Tox_Message_Type type,
ReceiptNum& receipt);
bool checkConnection();
@@ -219,10 +327,15 @@ public slots:
void makeTox(QByteArray savedata, ICoreSettings* s);
void loadFriends();
void loadConferences();
+ void loadGroups();
void bootstrapDht();
void checkLastOnline(uint32_t friendId);
+ void startGroupReconnectTimer(uint32_t groupNumber);
+ void stopGroupReconnectTimer(uint32_t groupNumber);
+ void retryGroupReconnect(uint32_t groupNumber);
+
QString getFriendRequestErrorMessage(const ToxId& friendId, const QString& message) const;
static void registerCallbacks(Tox* tox);
@@ -261,4 +374,10 @@ private slots:
const ICoreSettings& settings;
bool isConnected = false;
int tolerance = CORE_DISCONNECT_TOLERANCE;
+
+ QHash numberToGroupId;
+ QHash groupIdToNumber;
+ QHash groupReconnectTimers;
+ // number of group members other than ourselves
+ QHash groupPeerCounts;
};
diff --git a/src/core/groupid.cpp b/src/core/groupid.cpp
new file mode 100644
index 0000000000..47a538c48d
--- /dev/null
+++ b/src/core/groupid.cpp
@@ -0,0 +1,61 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#include "groupid.h"
+
+#include
+#include
+
+#include
+
+/**
+ * @class GroupId
+ * @brief This class represents a long term persistent group chat identifier
+ * (the "Chat ID" of an NGC group).
+ */
+
+/**
+ * @brief The default constructor. Creates an empty Tox group ID.
+ */
+GroupId::GroupId()
+ : ChatId()
+{
+}
+
+/**
+ * @brief Constructs a GroupId from bytes.
+ * @param rawId The bytes to construct the GroupId from. The length must be exactly
+ * TOX_GROUP_CHAT_ID_SIZE, else the GroupId will be empty.
+ */
+GroupId::GroupId(const QByteArray& rawId)
+ : ChatId([rawId]() {
+ assert(rawId.length() == TOX_GROUP_CHAT_ID_SIZE);
+ return rawId;
+ }())
+{
+}
+
+/**
+ * @brief Constructs a GroupId from bytes.
+ * @param rawId The bytes to construct the GroupId from, will read exactly
+ * TOX_GROUP_CHAT_ID_SIZE from the specified buffer.
+ */
+GroupId::GroupId(const uint8_t* rawId)
+ : ChatId(QByteArray(reinterpret_cast(rawId), TOX_GROUP_CHAT_ID_SIZE))
+{
+}
+
+/**
+ * @brief Get size of public id in bytes.
+ * @return Size of public id in bytes.
+ */
+int GroupId::getSize() const
+{
+ return TOX_GROUP_CHAT_ID_SIZE;
+}
+
+std::unique_ptr GroupId::clone() const
+{
+ return std::make_unique(*this);
+}
diff --git a/src/core/groupid.h b/src/core/groupid.h
new file mode 100644
index 0000000000..ff64715456
--- /dev/null
+++ b/src/core/groupid.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#pragma once
+
+#include "src/core/chatid.h"
+
+#include
+
+#include
+#include
+
+class GroupId : public ChatId
+{
+public:
+ GroupId();
+ explicit GroupId(const QByteArray& rawId);
+ explicit GroupId(const uint8_t* rawId);
+ int getSize() const override;
+ std::unique_ptr clone() const override;
+};
diff --git a/src/core/icoregroupmessagesender.cpp b/src/core/icoregroupmessagesender.cpp
new file mode 100644
index 0000000000..92111a7683
--- /dev/null
+++ b/src/core/icoregroupmessagesender.cpp
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#include "icoregroupmessagesender.h"
+
+ICoreGroupMessageSender::~ICoreGroupMessageSender() = default;
diff --git a/src/core/icoregroupmessagesender.h b/src/core/icoregroupmessagesender.h
new file mode 100644
index 0000000000..d13cc38be0
--- /dev/null
+++ b/src/core/icoregroupmessagesender.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#pragma once
+
+#include
+
+#include
+
+#include
+
+class ICoreGroupMessageSender
+{
+public:
+ ICoreGroupMessageSender() = default;
+ virtual ~ICoreGroupMessageSender();
+ ICoreGroupMessageSender(const ICoreGroupMessageSender&) = default;
+ ICoreGroupMessageSender& operator=(const ICoreGroupMessageSender&) = default;
+ ICoreGroupMessageSender(ICoreGroupMessageSender&&) = default;
+ ICoreGroupMessageSender& operator=(ICoreGroupMessageSender&&) = default;
+
+ virtual void sendGroupAction(uint32_t groupNumber, const QString& message) = 0;
+ virtual void sendGroupMessage(uint32_t groupNumber, const QString& message) = 0;
+ virtual void sendGroupPrivateMessage(uint32_t groupNumber, uint32_t peerId,
+ const QString& message, Tox_Message_Type type) = 0;
+};
diff --git a/src/core/icoregroupquery.cpp b/src/core/icoregroupquery.cpp
new file mode 100644
index 0000000000..d817067002
--- /dev/null
+++ b/src/core/icoregroupquery.cpp
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#include "icoregroupquery.h"
+
+ICoreGroupQuery::~ICoreGroupQuery() = default;
diff --git a/src/core/icoregroupquery.h b/src/core/icoregroupquery.h
new file mode 100644
index 0000000000..96bbca6999
--- /dev/null
+++ b/src/core/icoregroupquery.h
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#pragma once
+
+#include "src/model/status.h"
+#include "toxpk.h"
+
+#include
+#include
+
+#include
+
+enum class GroupRole
+{
+ Founder = 0,
+ Moderator = 1,
+ User = 2,
+ Observer = 3,
+ Unknown = -1,
+};
+
+enum class GroupTopicLock
+{
+ Enabled = 0,
+ Disabled = 1,
+ Unknown = -1,
+};
+
+enum class GroupVoiceState
+{
+ All = 0,
+ Moderator = 1,
+ Founder = 2,
+ Unknown = -1,
+};
+
+enum class GroupPrivacyState
+{
+ Public = 0,
+ Private = 1,
+ Unknown = -1,
+};
+
+class ICoreGroupQuery
+{
+public:
+ ICoreGroupQuery() = default;
+ virtual ~ICoreGroupQuery();
+ ICoreGroupQuery(const ICoreGroupQuery&) = default;
+ ICoreGroupQuery& operator=(const ICoreGroupQuery&) = default;
+ ICoreGroupQuery(ICoreGroupQuery&&) = default;
+ ICoreGroupQuery& operator=(ICoreGroupQuery&&) = default;
+
+ virtual QString getGroupPeerName(int groupNumber, int peerId) const = 0;
+ virtual ToxPk getGroupPeerPk(int groupNumber, int peerId) const = 0;
+ virtual ToxPk getGroupSelfPk(int groupNumber) const = 0;
+ virtual QString getGroupTitle(int groupNumber) const = 0;
+ virtual QString getGroupTopic(int groupNumber) const = 0;
+ virtual QString getGroupSelfName(int groupNumber) const = 0;
+ virtual bool setGroupSelfName(int groupNumber, const QString& name) = 0;
+ virtual uint32_t getGroupSelfPeerId(int groupNumber) const = 0;
+ virtual Status::Status getGroupSelfStatus(int groupNumber) const = 0;
+ virtual bool setGroupSelfStatus(int groupNumber, Status::Status status) = 0;
+ virtual Status::Status getGroupPeerStatus(int groupNumber, int peerId) const = 0;
+ virtual GroupRole getGroupPeerRole(int groupNumber, int peerId) const = 0;
+ virtual bool setGroupPeerRole(int groupNumber, int peerId, GroupRole role) = 0;
+ virtual bool kickGroupPeer(int groupNumber, int peerId) = 0;
+ virtual bool setGroupPassword(int groupNumber, const QByteArray& password) = 0;
+ virtual bool setGroupPeerLimit(int groupNumber, uint16_t peerLimit) = 0;
+ virtual bool setGroupTopicLock(int groupNumber, GroupTopicLock topicLock) = 0;
+ virtual bool setGroupVoiceState(int groupNumber, GroupVoiceState voiceState) = 0;
+ virtual bool setGroupPrivacyState(int groupNumber, GroupPrivacyState privacyState) = 0;
+ virtual bool getGroupHasPassword(int groupNumber) const = 0;
+ virtual uint16_t getGroupPeerLimit(int groupNumber) const = 0;
+ virtual GroupTopicLock getGroupTopicLock(int groupNumber) const = 0;
+ virtual GroupVoiceState getGroupVoiceState(int groupNumber) const = 0;
+ virtual GroupPrivacyState getGroupPrivacyState(int groupNumber) const = 0;
+};
diff --git a/src/core/icoresettings.h b/src/core/icoresettings.h
index ceef5e6450..d0f490b5b9 100644
--- a/src/core/icoresettings.h
+++ b/src/core/icoresettings.h
@@ -48,6 +48,8 @@ class ICoreSettings
virtual QNetworkProxy getProxy() const = 0;
+ virtual QStringList getSavedGroups() const = 0;
+
DECLARE_SIGNAL(enableIPv6Changed, bool enabled);
DECLARE_SIGNAL(forceTCPChanged, bool enabled);
DECLARE_SIGNAL(enableLanDiscoveryChanged, bool enabled);
diff --git a/src/core/toxid.cpp b/src/core/toxid.cpp
index 1a0094128d..a444ec4c01 100644
--- a/src/core/toxid.cpp
+++ b/src/core/toxid.cpp
@@ -15,7 +15,7 @@
#include
const QRegularExpression
- ToxId::ToxIdRegEx(QString("(^|\\s)[A-Fa-f0-9]{%1}($|\\s)").arg(ToxId::numHexChars));
+ ToxId::ToxIdRegEx(QString("(^|\\s)[A-Fa-f0-9]{%1}($|\\s)").arg(TOX_ADDRESS_SIZE * 2));
/**
* @class ToxId
@@ -92,8 +92,8 @@ ToxId::ToxId(const QByteArray& rawId)
* If the given rawId isn't a valid Public Key or Tox ID a ToxId with all zero bytes is created.
*
* @param rawId Pointer to bytes to convert to ToxId object
- * @param len Number of bytes to read. Must be ToxPk::size for a Public Key or
- * ToxId::size for a Tox ID.
+ * @param len Number of bytes to read. Must be TOX_PUBLIC_KEY_SIZE for a Public Key or
+ * TOX_ADDRESS_SIZE for a Tox ID.
*/
ToxId::ToxId(const uint8_t* rawId, int len)
{
@@ -104,7 +104,7 @@ ToxId::ToxId(const uint8_t* rawId, int len)
void ToxId::constructToxId(const QByteArray& rawId)
{
- if (rawId.length() == ToxId::size && isToxId(QString::fromUtf8(rawId.toHex()).toUpper())) {
+ if (rawId.length() == TOX_ADDRESS_SIZE && isToxId(QString::fromUtf8(rawId.toHex()).toUpper())) {
toxId = QByteArray(rawId); // construct from full tox id
} else {
assert(!"ToxId constructed with invalid input");
@@ -169,7 +169,7 @@ const uint8_t* ToxId::getBytes() const
*/
ToxPk ToxId::getPublicKey() const
{
- const auto pkBytes = toxId.left(ToxPk::size);
+ const auto pkBytes = toxId.left(TOX_PUBLIC_KEY_SIZE);
if (pkBytes.isEmpty()) {
return ToxPk{};
}
@@ -182,8 +182,8 @@ ToxPk ToxId::getPublicKey() const
*/
QString ToxId::getNoSpamString() const
{
- if (toxId.length() == ToxId::size) {
- return QString::fromUtf8(toxId.mid(ToxPk::size, ToxId::nospamSize).toHex()).toUpper();
+ if (toxId.length() == TOX_ADDRESS_SIZE) {
+ return QString::fromUtf8(toxId.mid(TOX_PUBLIC_KEY_SIZE, TOX_NOSPAM_SIZE).toHex()).toUpper();
}
return {};
@@ -208,7 +208,7 @@ bool ToxId::isValidToxId(const QString& id)
*/
bool ToxId::isToxId(const QString& id)
{
- return id.length() == ToxId::numHexChars && id.contains(ToxIdRegEx);
+ return id.length() == TOX_ADDRESS_SIZE * 2 && id.contains(ToxIdRegEx);
}
/**
@@ -217,15 +217,16 @@ bool ToxId::isToxId(const QString& id)
*/
bool ToxId::isValid() const
{
- if (toxId.length() != ToxId::size) {
+ if (toxId.length() != TOX_ADDRESS_SIZE) {
return false;
}
- const int pkAndChecksum = ToxPk::size + ToxId::nospamSize;
+ constexpr int checksumSize = TOX_ADDRESS_SIZE - TOX_PUBLIC_KEY_SIZE - TOX_NOSPAM_SIZE;
+ const int pkAndChecksum = TOX_PUBLIC_KEY_SIZE + TOX_NOSPAM_SIZE;
QByteArray data = toxId.left(pkAndChecksum);
- const QByteArray checksum = toxId.right(ToxId::checksumSize);
- QByteArray calculated(ToxId::checksumSize, 0x00);
+ const QByteArray checksum = toxId.right(checksumSize);
+ QByteArray calculated(checksumSize, 0x00);
for (int i = 0; i < pkAndChecksum; i++) {
calculated[i % 2] = calculated[i % 2] ^ data[i];
diff --git a/src/core/toxid.h b/src/core/toxid.h
index 2a85ebeca3..136f5f25e6 100644
--- a/src/core/toxid.h
+++ b/src/core/toxid.h
@@ -16,13 +16,6 @@
class ToxId
{
public:
- static constexpr int nospamSize = 4;
- static constexpr int nospamNumHexChars = nospamSize * 2;
- static constexpr int checksumSize = 2;
- static constexpr int checksumNumHexChars = checksumSize * 2;
- static constexpr int size = 38;
- static constexpr int numHexChars = size * 2;
-
ToxId();
ToxId(const ToxId& other);
ToxId(ToxId&& other);
diff --git a/src/core/toxoptions.cpp b/src/core/toxoptions.cpp
index 4640f3eb94..432b505ad6 100644
--- a/src/core/toxoptions.cpp
+++ b/src/core/toxoptions.cpp
@@ -99,6 +99,10 @@ std::unique_ptr ToxOptions::makeToxOptions(const QByteArray& savedat
tox_options_set_savedata_data(toxOptions->get(),
reinterpret_cast(savedata.data()), savedata.size());
+ // Groups must be persisted in the tox save, otherwise they are rejoined on
+ // every start and the founder role is lost.
+ tox_options_set_experimental_groups_persistence(toxOptions->get(), true);
+
// IPv6 needed for LAN discovery, but can crash some weird routers. On by default, can be
// disabled in options.
const bool enableIPv6 = s.getEnableIPv6();
diff --git a/src/core/toxpk.cpp b/src/core/toxpk.cpp
index bba26ddbc7..0fd00949d3 100644
--- a/src/core/toxpk.cpp
+++ b/src/core/toxpk.cpp
@@ -23,24 +23,24 @@ ToxPk::ToxPk() = default;
/**
* @brief Constructs a ToxPk from bytes.
* @param rawId The bytes to construct the ToxPk from. The length must be exactly
- * ToxPk::size, else the ToxPk will be empty.
+ * TOX_PUBLIC_KEY_SIZE, else the ToxPk will be empty.
*/
ToxPk::ToxPk(QByteArray rawId)
: ChatId(std::move(rawId))
{
- if (id.length() != size) {
+ if (id.length() != TOX_PUBLIC_KEY_SIZE) {
qCritical("ToxPk constructed with invalid length (%u instead of %d)",
- static_cast(id.length()), size);
+ static_cast(id.length()), TOX_PUBLIC_KEY_SIZE);
}
}
/**
* @brief Constructs a ToxPk from bytes.
* @param rawId The bytes to construct the ToxPk from, will read exactly
- * ToxPk::size from the specified buffer.
+ * TOX_PUBLIC_KEY_SIZE from the specified buffer.
*/
ToxPk::ToxPk(const uint8_t* rawId)
- : ToxPk(QByteArray(reinterpret_cast(rawId), size))
+ : ToxPk(QByteArray(reinterpret_cast(rawId), TOX_PUBLIC_KEY_SIZE))
{
}
@@ -53,9 +53,9 @@ ToxPk::ToxPk(const uint8_t* rawId)
*/
ToxPk::ToxPk(const QString& pk)
: ToxPk([&pk]() {
- if (pk.length() != numHexChars) {
+ if (pk.length() != TOX_PUBLIC_KEY_SIZE * 2) {
qCritical("ToxPk constructed with invalid length string (%u instead of %d)",
- static_cast(pk.length()), numHexChars);
+ static_cast(pk.length()), TOX_PUBLIC_KEY_SIZE * 2);
}
return QByteArray::fromHex(pk.toLatin1());
}())
@@ -68,7 +68,7 @@ ToxPk::ToxPk(const QString& pk)
*/
int ToxPk::getSize() const
{
- return size;
+ return TOX_PUBLIC_KEY_SIZE;
}
std::unique_ptr ToxPk::clone() const
diff --git a/src/core/toxpk.h b/src/core/toxpk.h
index a67842f0b8..b1bc6e137d 100644
--- a/src/core/toxpk.h
+++ b/src/core/toxpk.h
@@ -10,12 +10,11 @@
#include
#include
+#include
class ToxPk : public ChatId
{
public:
- static constexpr int size = 32;
- static constexpr int numHexChars = size * 2;
ToxPk();
explicit ToxPk(QByteArray rawId);
explicit ToxPk(const uint8_t* rawId);
diff --git a/src/grouplist.cpp b/src/grouplist.cpp
new file mode 100644
index 0000000000..6ce5b39c18
--- /dev/null
+++ b/src/grouplist.cpp
@@ -0,0 +1,82 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#include "grouplist.h"
+
+#include "src/core/core.h"
+#include "src/model/group.h"
+
+#include
+#include
+
+Group* GroupList::addGroup(Core& core, uint32_t groupNum, const GroupId& groupId,
+ const QString& groupName, const QString& selfName, FriendList& friendList)
+{
+ auto checker = groupList.find(groupId);
+ if (checker != groupList.end()) {
+ qWarning() << "addGroup: groupId already taken";
+ }
+
+ auto* newGroup = new Group(groupNum, groupId, groupName, selfName, core, core, friendList);
+ groupList[groupId] = newGroup;
+ id2key[groupNum] = groupId;
+ return newGroup;
+}
+
+Group* GroupList::findGroup(const GroupId& groupId)
+{
+ auto g_it = groupList.find(groupId);
+ if (g_it != groupList.end()) {
+ return *g_it;
+ }
+
+ return nullptr;
+}
+
+const GroupId& GroupList::id2Key(uint32_t groupNum)
+{
+ return id2key[groupNum];
+}
+
+void GroupList::setToxGroupNum(uint32_t oldGroupNum, uint32_t newGroupNum, const GroupId& groupId)
+{
+ if (oldGroupNum != newGroupNum) {
+ id2key.remove(oldGroupNum);
+ id2key[newGroupNum] = groupId;
+ }
+}
+
+void GroupList::removeGroup(const GroupId& groupId, bool /*fake*/)
+{
+ auto g_it = groupList.find(groupId);
+ if (g_it != groupList.end()) {
+ groupList.erase(g_it);
+ }
+ for (auto it = id2key.begin(); it != id2key.end();) {
+ if (it.value() == groupId) {
+ it = id2key.erase(it);
+ } else {
+ ++it;
+ }
+ }
+}
+
+QList GroupList::getAllGroups()
+{
+ QList res;
+
+ for (auto* it : groupList) {
+ res.append(it);
+ }
+
+ return res;
+}
+
+void GroupList::clear()
+{
+ for (auto* groupptr : groupList) {
+ delete groupptr;
+ }
+ groupList.clear();
+}
diff --git a/src/grouplist.h b/src/grouplist.h
new file mode 100644
index 0000000000..a0cf21f195
--- /dev/null
+++ b/src/grouplist.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#pragma once
+
+#include "src/core/groupid.h"
+
+class Core;
+template
+class QHash;
+template
+class QList;
+class Group;
+class QString;
+class FriendList;
+
+class GroupList
+{
+public:
+ Group* addGroup(Core& core, uint32_t groupNum, const GroupId& persistentGroupId,
+ const QString& groupName, const QString& selfName, FriendList& friendList);
+ Group* findGroup(const GroupId& groupId);
+ const GroupId& id2Key(uint32_t groupNum);
+ void setToxGroupNum(uint32_t oldGroupNum, uint32_t newGroupNum, const GroupId& groupId);
+ void removeGroup(const GroupId& groupId, bool fake = false);
+ QList getAllGroups();
+ void clear();
+
+private:
+ QHash groupList;
+ QHash id2key;
+};
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index 5b631b841d..3b819a5411 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -966,6 +966,47 @@
+ -
+
+
+
+ 55
+ 35
+
+
+
+ Qt::NoFocus
+
+
+ Create a group
+
+
+ Group
+
+
+ Open group management page
+
+
+ false
+
+
+
+
+
+
+ :/img/group.svg:/img/group.svg
+
+
+
+ 15
+ 15
+
+
+
+ true
+
+
+
-
diff --git a/src/model/chathistory.cpp b/src/model/chathistory.cpp
index 3392a6c886..9008adcdba 100644
--- a/src/model/chathistory.cpp
+++ b/src/model/chathistory.cpp
@@ -62,12 +62,12 @@ bool handleActionPrefix(QString& content)
ChatHistory::ChatHistory(Chat& chat_, History* history_, const ICoreIdHandler& coreIdHandler_,
const Settings& settings_, IMessageDispatcher& messageDispatcher,
- FriendList& friendList, ConferenceList& conferenceList)
+ FriendList& friendList, ConferenceList& conferenceList, GroupList& groupList)
: chat(chat_)
, history(history_)
, settings(settings_)
, coreIdHandler(coreIdHandler_)
- , sessionChatLog(getInitialChatLogIdx(), coreIdHandler_, friendList, conferenceList)
+ , sessionChatLog(getInitialChatLogIdx(), coreIdHandler_, friendList, conferenceList, groupList)
{
connect(&messageDispatcher, &IMessageDispatcher::messageComplete, this,
&ChatHistory::onMessageComplete);
@@ -262,7 +262,8 @@ void ChatHistory::onMessageReceived(const ToxPk& sender, const Message& message)
content = ChatForm::ACTION_PREFIX + content;
}
- history->addNewMessage(chatId, content, sender, message.timestamp, true, displayName);
+ history->addNewMessage(chatId, content, sender, message.timestamp, true, displayName, {},
+ message.recipient, message.recipientName);
}
sessionChatLog.onMessageReceived(sender, message);
@@ -284,7 +285,7 @@ void ChatHistory::onMessageSent(DispatchedMessageId id, const Message& message)
auto onInsertion = [this, id](RowId historyId) { handleDispatchedMessage(id, historyId); };
history->addNewMessage(chatId, content, selfPk, message.timestamp, false, username,
- onInsertion);
+ onInsertion, message.recipient, message.recipientName);
}
sessionChatLog.onMessageSent(id, message);
@@ -370,7 +371,7 @@ void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const
// we hit IMessageDispatcher's signals which history listens for.
// Items added to history have already been sent so we know they already
// reflect what was sent/received.
- auto processedMessage = Message{isAction, messageContent, message.timestamp, {}};
+ auto processedMessage = Message{isAction, messageContent, message.timestamp, {}, message.recipient, message.recipientName};
auto dispatchedMessageIt =
std::find_if(dispatchedMessageRowIdMap.begin(), dispatchedMessageRowIdMap.end(),
diff --git a/src/model/chathistory.h b/src/model/chathistory.h
index dcc39b2602..55c2a1e386 100644
--- a/src/model/chathistory.h
+++ b/src/model/chathistory.h
@@ -17,6 +17,7 @@ class ICoreIdHandler;
class Settings;
class FriendList;
class ConferenceList;
+class GroupList;
class ChatHistory : public IChatLog
{
@@ -24,7 +25,7 @@ class ChatHistory : public IChatLog
public:
ChatHistory(Chat& chat_, History* history_, const ICoreIdHandler& coreIdHandler_,
const Settings& settings_, IMessageDispatcher& messageDispatcher,
- FriendList& friendList, ConferenceList& conferenceList);
+ FriendList& friendList, ConferenceList& conferenceList, GroupList& groupList);
const ChatLogItem& at(ChatLogIdx idx) const override;
SearchResult searchForward(SearchPos startIdx, const QString& phrase,
const ParameterSearch& parameter) const override;
diff --git a/src/model/chatmanager.cpp b/src/model/chatmanager.cpp
index f2f63c7941..41fdf3a3c9 100644
--- a/src/model/chatmanager.cpp
+++ b/src/model/chatmanager.cpp
@@ -9,26 +9,31 @@
#include "src/core/core.h"
#include "src/core/coreav.h"
#include "src/friendlist.h"
+#include "src/grouplist.h"
#include "src/model/chathistory.h"
#include "src/model/chatroom/conferenceroom.h"
#include "src/model/chatroom/friendchatroom.h"
+#include "src/model/chatroom/grouproom.h"
#include "src/model/conference.h"
#include "src/model/friend.h"
+#include "src/model/group.h"
#include "src/persistence/profile.h"
#include "src/persistence/settings.h"
#include
#include
+#include
ChatManager::ChatManager(Profile& profile_, Settings& settings_, FriendList& friendList_,
- ConferenceList& conferenceList_, IDialogsManager* dialogsManager_,
- QObject* parent)
+ ConferenceList& conferenceList_, GroupList& groupList_,
+ IDialogsManager* dialogsManager_, QObject* parent)
: QObject(parent)
, profile(profile_)
, settings(settings_)
, friendList(friendList_)
, conferenceList(conferenceList_)
+ , groupList(groupList_)
, dialogsManager(dialogsManager_)
, sharedMessageProcessorParams(
std::make_unique(Core::getMaxMessageSize()))
@@ -53,6 +58,24 @@ void ChatManager::connectToCore(Core& core_)
connect(core, &Core::conferencePeerlistChanged, this, &ChatManager::onConferencePeerlistChanged);
connect(core, &Core::conferencePeerNameChanged, this, &ChatManager::onConferencePeerNameChanged);
connect(core, &Core::conferenceTitleChanged, this, &ChatManager::onConferenceTitleChanged);
+ connect(core, &Core::groupMessageReceived, this, &ChatManager::onGroupMessageReceived);
+ connect(core, &Core::groupPrivateMessageReceived, this, &ChatManager::onGroupPrivateMessageReceived);
+ connect(core, &Core::emptyGroupCreated, this, &ChatManager::onEmptyGroupCreated);
+ connect(core, &Core::groupJoined, this, &ChatManager::onGroupJoined);
+ connect(core, &Core::groupPeerJoined, this, &ChatManager::onGroupPeerJoined);
+ connect(core, &Core::groupPeerExited, this, &ChatManager::onGroupPeerExited);
+ connect(core, &Core::groupPeerNameChanged, this, &ChatManager::onGroupPeerNameChanged);
+ connect(core, &Core::groupPeerStatusChanged, this, &ChatManager::onGroupPeerStatusChanged);
+ connect(core, &Core::groupTopicChanged, this, &ChatManager::onGroupTopicChanged);
+ connect(core, &Core::groupSelfJoined, this, &ChatManager::onGroupSelfJoined);
+ connect(core, &Core::groupSelfDisconnected, this, &ChatManager::onGroupSelfDisconnected);
+ connect(core, &Core::groupJoinFailed, this, &ChatManager::onGroupJoinFailed);
+ connect(core, &Core::groupPeerRolesChanged, this, &ChatManager::onGroupPeerRolesChanged);
+ connect(core, &Core::groupPasswordChanged, this, &ChatManager::onGroupPasswordChanged);
+ connect(core, &Core::groupPeerLimitChanged, this, &ChatManager::onGroupPeerLimitChanged);
+ connect(core, &Core::groupTopicLockChanged, this, &ChatManager::onGroupTopicLockChanged);
+ connect(core, &Core::groupVoiceStateChanged, this, &ChatManager::onGroupVoiceStateChanged);
+ connect(core, &Core::groupPrivacyStateChanged, this, &ChatManager::onGroupPrivacyStateChanged);
}
FriendMessageDispatcher* ChatManager::getFriendDispatcher(const ToxPk& friendPk) const
@@ -109,6 +132,33 @@ std::shared_ptr ChatManager::getConferenceRoom(const ConferenceI
return *it;
}
+GroupMessageDispatcher* ChatManager::getGroupDispatcher(const GroupId& groupId) const
+{
+ auto it = groupMessageDispatchers.find(groupId);
+ if (it == groupMessageDispatchers.end()) {
+ return nullptr;
+ }
+ return it->get();
+}
+
+IChatLog* ChatManager::getGroupChatLog(const GroupId& groupId) const
+{
+ auto it = groupLogs.find(groupId);
+ if (it == groupLogs.end()) {
+ return nullptr;
+ }
+ return it->get();
+}
+
+std::shared_ptr ChatManager::getGroupRoom(const GroupId& groupId) const
+{
+ auto it = groupRooms.find(groupId);
+ if (it == groupRooms.end()) {
+ return nullptr;
+ }
+ return *it;
+}
+
MessageProcessor::SharedParams& ChatManager::getSharedMessageProcessorParams()
{
return *sharedMessageProcessorParams;
@@ -163,6 +213,28 @@ void ChatManager::removeConferenceModel(const ConferenceId& conferenceId)
conferenceRooms.remove(conferenceId);
}
+void ChatManager::removeGroup(const GroupId& groupId)
+{
+ Group* g = groupList.findGroup(groupId);
+ if (g == nullptr) {
+ return;
+ }
+
+ core->quitGroup(g->getId());
+
+ groupMessageDispatchers.remove(groupId);
+ groupLogs.remove(groupId);
+ groupRooms.remove(groupId);
+ settings.removeSavedGroup(groupId.toString());
+}
+
+void ChatManager::removeGroupModel(const GroupId& groupId)
+{
+ groupMessageDispatchers.remove(groupId);
+ groupLogs.remove(groupId);
+ groupRooms.remove(groupId);
+}
+
void ChatManager::onFriendAdded(uint32_t friendId, const ToxPk& friendPk)
{
assert(core != nullptr);
@@ -170,7 +242,8 @@ void ChatManager::onFriendAdded(uint32_t friendId, const ToxPk& friendPk)
Friend* newFriend = friendList.addFriend(friendId, friendPk, settings);
auto chatroom =
- std::make_shared(newFriend, dialogsManager, *core, settings, conferenceList);
+ std::make_shared(newFriend, dialogsManager, *core, settings, conferenceList,
+ groupList);
auto friendMessageDispatcher =
std::make_shared(*newFriend,
MessageProcessor(*sharedMessageProcessorParams),
@@ -179,7 +252,7 @@ void ChatManager::onFriendAdded(uint32_t friendId, const ToxPk& friendPk)
auto* history = profile.getHistory();
auto chatHistory =
std::make_shared(*newFriend, history, *core, settings,
- *friendMessageDispatcher, friendList, conferenceList);
+ *friendMessageDispatcher, friendList, conferenceList, groupList);
friendMessageDispatchers[friendPk] = friendMessageDispatcher;
friendChatLogs[friendPk] = chatHistory;
@@ -306,6 +379,260 @@ void ChatManager::onConferenceTitleChanged(uint32_t conferencenumber, const QStr
c->setTitle(author, title);
}
+void ChatManager::onGroupMessageReceived(uint32_t groupNumber, uint32_t peerId, const QString& message,
+ bool isAction)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g == nullptr) {
+ return;
+ }
+
+ const ToxPk author = core->getGroupPeerPk(groupNumber, peerId);
+
+ groupMessageDispatchers[groupId]->onMessageReceived(author, isAction, message);
+}
+
+void ChatManager::onGroupPrivateMessageReceived(uint32_t groupNumber, uint32_t peerId,
+ const QString& message, bool isAction)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g == nullptr) {
+ return;
+ }
+
+ const ToxPk author = core->getGroupPeerPk(groupNumber, peerId);
+
+ groupMessageDispatchers[groupId]->onPrivateMessageReceived(author, isAction, message);
+}
+
+void ChatManager::onEmptyGroupCreated(uint32_t groupNumber, const GroupId& groupId,
+ const QString& groupName)
+{
+ Group* group = createGroup(groupNumber, groupId, QString());
+ if (group == nullptr) {
+ return;
+ }
+ if (!groupId.isEmpty()) {
+ settings.addSavedGroup(groupId.toString());
+ if (!groupName.isEmpty()) {
+ settings.setGroupName(groupId.toString(), groupName);
+ group->setName(groupName);
+ }
+ }
+ addSelfToGroup(group);
+}
+
+void ChatManager::onGroupJoined(uint32_t groupNumber, const GroupId& groupId)
+{
+ Group* g = groupList.findGroup(groupId);
+ if (g == nullptr) {
+ const QString groupName = core->getGroupTitle(groupNumber);
+ g = createGroup(groupNumber, groupId, groupName);
+ } else {
+ updateGroupNumber(g, groupNumber);
+ }
+ if (g != nullptr) {
+ addSelfToGroup(g);
+ }
+ if (!groupId.isEmpty()) {
+ settings.addSavedGroup(groupId.toString());
+ }
+}
+
+void ChatManager::onGroupPeerJoined(uint32_t groupNumber, uint32_t peerId)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g == nullptr) {
+ return;
+ }
+
+ g->onPeerJoin(peerId);
+}
+
+void ChatManager::onGroupPeerExited(uint32_t groupNumber, uint32_t peerId)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g == nullptr) {
+ return;
+ }
+
+ g->onPeerExit(peerId);
+}
+
+void ChatManager::onGroupPeerNameChanged(uint32_t groupNumber, uint32_t peerId, const QString& newName)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g == nullptr) {
+ return;
+ }
+
+ g->onPeerNameChanged(peerId, newName);
+}
+
+void ChatManager::onGroupPeerStatusChanged(uint32_t groupNumber, uint32_t peerId, Status::Status status)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g == nullptr) {
+ return;
+ }
+
+ g->onPeerStatusChanged(peerId, status);
+}
+
+void ChatManager::onGroupTopicChanged(uint32_t groupNumber, const QString& topic)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g == nullptr) {
+ return;
+ }
+
+ g->setTopic(QString(), topic);
+ settings.setGroupTopic(groupId.toString(), topic);
+}
+
+void ChatManager::onGroupSelfJoined(uint32_t groupNumber)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g == nullptr) {
+ const GroupId persistentId = core->getGroupPersistentId(groupNumber);
+ if (!persistentId.isEmpty()) {
+ g = groupList.findGroup(persistentId);
+ }
+ if (g == nullptr) {
+ const QString groupName = core->getGroupTitle(groupNumber);
+ g = createGroup(groupNumber, persistentId, groupName);
+ }
+ }
+ if (g != nullptr) {
+ updateGroupNumber(g, groupNumber);
+ addSelfToGroup(g);
+ g->updatePeerRoles();
+ const QString groupName = core->getGroupTitle(groupNumber);
+ if (!groupName.isEmpty()) {
+ g->updateName(groupName);
+ }
+ const QString alias = settings.getGroupName(g->getPersistentId().toString());
+ if (!alias.isEmpty() && alias != g->getName()) {
+ g->setName(alias);
+ }
+ const QString nickname = settings.getGroupNickname(g->getPersistentId().toString());
+ if (!nickname.isEmpty()) {
+ g->setGroupNickname(nickname);
+ }
+ const QString groupTopic = core->getGroupTopic(groupNumber);
+ if (!groupTopic.isEmpty()) {
+ g->setTopic(QString(), groupTopic);
+ settings.setGroupTopic(g->getPersistentId().toString(), groupTopic);
+ }
+ }
+}
+
+void ChatManager::addSelfToGroup(Group* g)
+{
+ const uint32_t groupNumber = g->getId();
+ const uint32_t selfPeerId = core->getGroupSelfPeerId(groupNumber);
+ if (selfPeerId == std::numeric_limits::max()) {
+ return;
+ }
+ g->onPeerJoin(selfPeerId);
+}
+
+void ChatManager::updateGroupNumber(Group* g, uint32_t groupNumber)
+{
+ if (g->getId() != groupNumber) {
+ groupList.setToxGroupNum(g->getId(), groupNumber, g->getPersistentId());
+ g->setToxGroupNumber(groupNumber);
+ }
+}
+
+void ChatManager::onGroupSelfDisconnected(uint32_t groupNumber)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g != nullptr) {
+ g->clearPeers();
+ }
+}
+
+void ChatManager::onGroupJoinFailed(uint32_t groupNumber, Tox_Group_Join_Fail failType)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g != nullptr) {
+ if (failType == TOX_GROUP_JOIN_FAIL_INVALID_PASSWORD) {
+ core->quitGroup(g->getId());
+ settings.removeSavedGroup(groupId.toString());
+ // The UI must be torn down before the model, otherwise the GroupForm
+ // keeps a dangling reference to the chat log.
+ emit groupRemoved(groupId);
+ } else {
+ qWarning() << "Group" << groupId.toString() << "join failed temporarily, keeping saved";
+ }
+ }
+}
+
+void ChatManager::onGroupPeerRolesChanged(uint32_t groupNumber)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g != nullptr) {
+ g->updatePeerRoles();
+ }
+}
+
+void ChatManager::onGroupPasswordChanged(uint32_t groupNumber, bool hasPassword)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g != nullptr) {
+ g->setPasswordSet(hasPassword);
+ }
+}
+
+void ChatManager::onGroupPeerLimitChanged(uint32_t groupNumber, uint16_t peerLimit)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g != nullptr) {
+ g->setPeerLimit(peerLimit);
+ }
+}
+
+void ChatManager::onGroupTopicLockChanged(uint32_t groupNumber, GroupTopicLock topicLock)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g != nullptr) {
+ g->setTopicLock(topicLock);
+ }
+}
+
+void ChatManager::onGroupVoiceStateChanged(uint32_t groupNumber, GroupVoiceState voiceState)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g != nullptr) {
+ g->setVoiceState(voiceState);
+ }
+}
+
+void ChatManager::onGroupPrivacyStateChanged(uint32_t groupNumber, GroupPrivacyState privacyState)
+{
+ const GroupId& groupId = groupList.id2Key(groupNumber);
+ Group* g = groupList.findGroup(groupId);
+ if (g != nullptr) {
+ g->setPrivacyState(privacyState);
+ }
+}
+
Conference* ChatManager::createConference(uint32_t conferencenumber, const ConferenceId& conferenceId)
{
assert(core != nullptr);
@@ -340,7 +667,8 @@ Conference* ChatManager::createConference(uint32_t conferencenumber, const Confe
auto* history = profile.getHistory();
auto chatHistory = std::make_shared(*newConference, history, *core, settings,
- *messageDispatcher, friendList, conferenceList);
+ *messageDispatcher, friendList, conferenceList,
+ groupList);
connect(core, &Core::usernameSet, newConference, &Conference::setSelfName);
@@ -352,3 +680,52 @@ Conference* ChatManager::createConference(uint32_t conferencenumber, const Confe
return newConference;
}
+
+Group* ChatManager::createGroup(uint32_t groupNumber, const GroupId& groupId, const QString& groupName)
+{
+ assert(core != nullptr);
+
+ QString name = groupName;
+
+ Group* g = groupList.findGroup(groupId);
+ if (g != nullptr) {
+ qWarning() << "Group already exists";
+ return g;
+ }
+
+ Group* newGroup = groupList.addGroup(*core, groupNumber, groupId, name,
+ core->getUsername(), friendList);
+ assert(newGroup);
+
+ newGroup->setPasswordSet(core->getGroupHasPassword(groupNumber));
+ newGroup->setPeerLimit(core->getGroupPeerLimit(groupNumber));
+ QString topic = core->getGroupTopic(groupNumber);
+ if (topic.isEmpty()) {
+ topic = settings.getGroupTopic(groupId.toString());
+ }
+ newGroup->setTopic(QString(), topic);
+ newGroup->setTopicLock(core->getGroupTopicLock(groupNumber));
+ newGroup->setVoiceState(core->getGroupVoiceState(groupNumber));
+ newGroup->setPrivacyState(core->getGroupPrivacyState(groupNumber));
+
+ auto chatroom = std::make_shared(newGroup, dialogsManager, *core, friendList);
+ auto messageDispatcher =
+ std::make_shared(*newGroup,
+ MessageProcessor(*sharedMessageProcessorParams),
+ *core, *core, settings);
+
+ auto* history = profile.getHistory();
+ auto chatHistory = std::make_shared(*newGroup, history, *core, settings,
+ *messageDispatcher, friendList, conferenceList,
+ groupList);
+
+ connect(core, &Core::usernameSet, newGroup, &Group::setSelfName);
+
+ groupMessageDispatchers[groupId] = messageDispatcher;
+ groupLogs[groupId] = chatHistory;
+ groupRooms[groupId] = chatroom;
+
+ emit groupAdded(newGroup, chatroom, messageDispatcher, chatHistory);
+
+ return newGroup;
+}
diff --git a/src/model/chatmanager.h b/src/model/chatmanager.h
index 5dbfddd08b..33016cdf57 100644
--- a/src/model/chatmanager.h
+++ b/src/model/chatmanager.h
@@ -5,11 +5,16 @@
#pragma once
+#include
+
#include "src/core/conferenceid.h"
+#include "src/core/groupid.h"
+#include "src/core/icoregroupquery.h"
#include "src/core/receiptnum.h"
#include "src/core/toxpk.h"
#include "src/model/conferencemessagedispatcher.h"
#include "src/model/friendmessagedispatcher.h"
+#include "src/model/groupmessagedispatcher.h"
#include "src/model/message.h"
#include
@@ -25,6 +30,9 @@ class Core;
class Friend;
class FriendChatroom;
class FriendList;
+class Group;
+class GroupList;
+class GroupRoom;
class IChatLog;
class IDialogsManager;
class Profile;
@@ -36,8 +44,8 @@ class ChatManager : public QObject
public:
ChatManager(Profile& profile, Settings& settings, FriendList& friendList,
- ConferenceList& conferenceList, IDialogsManager* dialogsManager,
- QObject* parent = nullptr);
+ ConferenceList& conferenceList, GroupList& groupList,
+ IDialogsManager* dialogsManager, QObject* parent = nullptr);
void connectToCore(Core& core);
@@ -47,6 +55,9 @@ class ChatManager : public QObject
ConferenceMessageDispatcher* getConferenceDispatcher(const ConferenceId& id) const;
IChatLog* getConferenceChatLog(const ConferenceId& id) const;
std::shared_ptr getConferenceRoom(const ConferenceId& id) const;
+ GroupMessageDispatcher* getGroupDispatcher(const GroupId& groupId) const;
+ IChatLog* getGroupChatLog(const GroupId& groupId) const;
+ std::shared_ptr getGroupRoom(const GroupId& groupId) const;
MessageProcessor::SharedParams& getSharedMessageProcessorParams();
@@ -54,6 +65,8 @@ class ChatManager : public QObject
void removeConference(const ConferenceId& conferenceId);
void removeFriendModel(const ToxPk& friendPk);
void removeConferenceModel(const ConferenceId& conferenceId);
+ void removeGroup(const GroupId& groupId);
+ void removeGroupModel(const GroupId& groupId);
signals:
void friendAdded(Friend* f, std::shared_ptr chatroom,
@@ -65,6 +78,10 @@ class ChatManager : public QObject
std::shared_ptr chatLog);
void conferenceRemoved(const ConferenceId& conferenceId);
void conferenceNeedsName(const ConferenceId& conferenceId);
+ void groupAdded(Group* g, std::shared_ptr chatroom,
+ std::shared_ptr dispatcher,
+ std::shared_ptr chatLog);
+ void groupRemoved(const GroupId& groupId);
private slots:
void onFriendAdded(uint32_t friendId, const ToxPk& friendPk);
@@ -84,14 +101,39 @@ private slots:
const QString& newName);
void onConferenceTitleChanged(uint32_t conferenceNum, const QString& author, const QString& title);
+ void onGroupMessageReceived(uint32_t groupNumber, uint32_t peerId, const QString& message,
+ bool isAction);
+ void onGroupPrivateMessageReceived(uint32_t groupNumber, uint32_t peerId, const QString& message,
+ bool isAction);
+ void onEmptyGroupCreated(uint32_t groupNumber, const GroupId& groupId, const QString& groupName);
+ void onGroupJoined(uint32_t groupNumber, const GroupId& groupId);
+ void onGroupPeerJoined(uint32_t groupNumber, uint32_t peerId);
+ void onGroupPeerExited(uint32_t groupNumber, uint32_t peerId);
+ void onGroupPeerNameChanged(uint32_t groupNumber, uint32_t peerId, const QString& newName);
+ void onGroupPeerStatusChanged(uint32_t groupNumber, uint32_t peerId, Status::Status status);
+ void onGroupTopicChanged(uint32_t groupNumber, const QString& topic);
+ void onGroupSelfJoined(uint32_t groupNumber);
+ void onGroupSelfDisconnected(uint32_t groupNumber);
+ void onGroupJoinFailed(uint32_t groupNumber, Tox_Group_Join_Fail failType);
+ void onGroupPeerRolesChanged(uint32_t groupNumber);
+ void onGroupPasswordChanged(uint32_t groupNumber, bool hasPassword);
+ void onGroupPeerLimitChanged(uint32_t groupNumber, uint16_t peerLimit);
+ void onGroupTopicLockChanged(uint32_t groupNumber, GroupTopicLock topicLock);
+ void onGroupVoiceStateChanged(uint32_t groupNumber, GroupVoiceState voiceState);
+ void onGroupPrivacyStateChanged(uint32_t groupNumber, GroupPrivacyState privacyState);
+
private:
Conference* createConference(uint32_t conferenceNum, const ConferenceId& conferenceId);
+ Group* createGroup(uint32_t groupNumber, const GroupId& groupId, const QString& groupName);
+ void addSelfToGroup(Group* g);
+ void updateGroupNumber(Group* g, uint32_t groupNumber);
Profile& profile;
Core* core = nullptr;
Settings& settings;
FriendList& friendList;
ConferenceList& conferenceList;
+ GroupList& groupList;
IDialogsManager* dialogsManager;
std::unique_ptr sharedMessageProcessorParams;
@@ -103,4 +145,8 @@ private slots:
QMap> conferenceMessageDispatchers;
QMap> conferenceLogs;
QMap> conferenceRooms;
+
+ QMap> groupMessageDispatchers;
+ QMap> groupLogs;
+ QMap> groupRooms;
};
diff --git a/src/model/chatroom/friendchatroom.cpp b/src/model/chatroom/friendchatroom.cpp
index 6784f5ffc2..5555eca995 100644
--- a/src/model/chatroom/friendchatroom.cpp
+++ b/src/model/chatroom/friendchatroom.cpp
@@ -7,9 +7,11 @@
#include "src/conferencelist.h"
#include "src/core/core.h"
+#include "src/grouplist.h"
#include "src/model/conference.h"
#include "src/model/dialogs/idialogsmanager.h"
#include "src/model/friend.h"
+#include "src/model/group.h"
#include "src/model/status.h"
#include "src/persistence/settings.h"
#include "src/widget/contentdialog.h"
@@ -31,12 +33,14 @@ QString getShortName(const QString& name)
} // namespace
FriendChatroom::FriendChatroom(Friend* frnd_, IDialogsManager* dialogsManager_, Core& core_,
- Settings& settings_, ConferenceList& conferenceList_)
+ Settings& settings_, ConferenceList& conferenceList_,
+ GroupList& groupList_)
: frnd{frnd_}
, dialogsManager{dialogsManager_}
, core{core_}
, settings{settings_}
, conferenceList{conferenceList_}
+ , groupList{groupList_}
{
}
@@ -110,6 +114,21 @@ void FriendChatroom::inviteFriend(const Conference* conference)
core.conferenceInviteFriend(friendId, conferenceId);
}
+void FriendChatroom::inviteToNewGroup()
+{
+ const auto friendId = frnd->getId();
+ const auto groupId = core.createGroup(tr("Group %1").arg(groupList.getAllGroups().size() + 1));
+ if (groupId >= 0) {
+ core.groupInviteFriend(friendId, groupId);
+ }
+}
+
+void FriendChatroom::inviteFriend(const Group* group)
+{
+ const auto friendId = frnd->getId();
+ core.groupInviteFriend(friendId, group->getId());
+}
+
QVector FriendChatroom::getConferences() const
{
QVector conferences;
@@ -122,6 +141,18 @@ QVector FriendChatroom::getConferences() const
return conferences;
}
+QVector FriendChatroom::getGroups() const
+{
+ QVector groups;
+ for (auto* const group : groupList.getAllGroups()) {
+ const auto name = getShortName(group->getDisplayedName());
+ const GroupToDisplay groupToDisplay = {name, group};
+ groups.push_back(groupToDisplay);
+ }
+
+ return groups;
+}
+
/**
* @brief Return sorted list of circles exclude current circle.
*/
diff --git a/src/model/chatroom/friendchatroom.h b/src/model/chatroom/friendchatroom.h
index 3e6894c911..f054cbe07e 100644
--- a/src/model/chatroom/friendchatroom.h
+++ b/src/model/chatroom/friendchatroom.h
@@ -11,8 +11,10 @@
class Chat;
class Core;
-class IDialogsManager;
class Friend;
+class Group;
+class GroupList;
+class IDialogsManager;
class Conference;
class Settings;
class ConferenceList;
@@ -23,6 +25,12 @@ struct ConferenceToDisplay
Conference* conference;
};
+struct GroupToDisplay
+{
+ QString name;
+ Group* group;
+};
+
struct CircleToDisplay
{
QString name;
@@ -34,7 +42,7 @@ class FriendChatroom final : public QObject
Q_OBJECT
public:
FriendChatroom(Friend* frnd_, IDialogsManager* dialogsManager_, Core& core_,
- Settings& settings_, ConferenceList& conferenceList);
+ Settings& settings_, ConferenceList& conferenceList, GroupList& groupList);
Chat* getChat();
@@ -52,12 +60,16 @@ public slots:
void inviteToNewConference();
void inviteFriend(const Conference* conference);
+ void inviteToNewGroup();
+ void inviteFriend(const Group* group);
+
bool autoAcceptEnabled() const;
QString getAutoAcceptDir() const;
void disableAutoAccept();
void setAutoAcceptDir(const QString& dir);
QVector getConferences() const;
+ QVector getGroups() const;
QVector getOtherCircles() const;
void resetEventFlags();
@@ -77,4 +89,5 @@ public slots:
Core& core;
Settings& settings;
ConferenceList& conferenceList;
+ GroupList& groupList;
};
diff --git a/src/model/chatroom/grouproom.cpp b/src/model/chatroom/grouproom.cpp
new file mode 100644
index 0000000000..fb5d234536
--- /dev/null
+++ b/src/model/chatroom/grouproom.cpp
@@ -0,0 +1,82 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#include "grouproom.h"
+
+#include "src/core/core.h"
+#include "src/core/toxpk.h"
+#include "src/friendlist.h"
+#include "src/model/chatroom/friendchatroom.h"
+#include "src/model/dialogs/idialogsmanager.h"
+#include "src/model/friend.h"
+#include "src/model/group.h"
+#include "src/model/status.h"
+
+GroupRoom::GroupRoom(Group* group_, IDialogsManager* dialogsManager_, Core& core_,
+ FriendList& friendList_)
+ : group{group_}
+ , dialogsManager{dialogsManager_}
+ , core{core_}
+ , friendList{friendList_}
+{
+}
+
+Chat* GroupRoom::getChat()
+{
+ return group;
+}
+
+Group* GroupRoom::getGroup()
+{
+ return group;
+}
+
+bool GroupRoom::hasNewMessage() const
+{
+ return group->getEventFlag();
+}
+
+void GroupRoom::resetEventFlags()
+{
+ group->setEventFlag(false);
+ group->setMentionedFlag(false);
+}
+
+bool GroupRoom::friendExists(const ToxPk& pk)
+{
+ return friendList.findFriend(pk) != nullptr;
+}
+
+void GroupRoom::inviteFriend(const ToxPk& pk)
+{
+ const Friend* frnd = friendList.findFriend(pk);
+ const auto friendId = frnd->getId();
+ const auto groupNumber = group->getId();
+ const auto canInvite = Status::isOnline(frnd->getStatus());
+
+ if (canInvite) {
+ core.groupInviteFriend(friendId, groupNumber);
+ }
+}
+
+bool GroupRoom::possibleToOpenInNewWindow() const
+{
+ const auto groupId = group->getPersistentId();
+ auto* const dialogs = dialogsManager->getGroupDialogs(groupId);
+ return (dialogs == nullptr) || dialogs->chatroomCount() > 1;
+}
+
+bool GroupRoom::canBeRemovedFromWindow() const
+{
+ const auto groupId = group->getPersistentId();
+ auto* const dialogs = dialogsManager->getGroupDialogs(groupId);
+ return (dialogs != nullptr) && dialogs->hasChat(groupId);
+}
+
+void GroupRoom::removeGroupFromDialogs()
+{
+ const auto groupId = group->getPersistentId();
+ auto* dialogs = dialogsManager->getGroupDialogs(groupId);
+ dialogs->removeGroup(groupId);
+}
diff --git a/src/model/chatroom/grouproom.h b/src/model/chatroom/grouproom.h
new file mode 100644
index 0000000000..4e7de8f831
--- /dev/null
+++ b/src/model/chatroom/grouproom.h
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#pragma once
+
+#include
+
+class Chat;
+class Core;
+class IDialogsManager;
+class Group;
+class ToxPk;
+class FriendList;
+
+class GroupRoom final : public QObject
+{
+ Q_OBJECT
+public:
+ GroupRoom(Group* group_, IDialogsManager* dialogsManager_, Core& core_, FriendList& friendList);
+
+ Chat* getChat();
+
+ Group* getGroup();
+
+ bool hasNewMessage() const;
+ void resetEventFlags();
+
+ bool friendExists(const ToxPk& pk);
+ void inviteFriend(const ToxPk& pk);
+
+ bool possibleToOpenInNewWindow() const;
+ bool canBeRemovedFromWindow() const;
+ void removeGroupFromDialogs();
+
+private:
+ Group* group{nullptr};
+ IDialogsManager* dialogsManager{nullptr};
+ Core& core;
+ FriendList& friendList;
+};
diff --git a/src/model/dialogs/idialogs.h b/src/model/dialogs/idialogs.h
index 911214747b..cef48d463e 100644
--- a/src/model/dialogs/idialogs.h
+++ b/src/model/dialogs/idialogs.h
@@ -7,6 +7,7 @@
class ChatId;
class ConferenceId;
+class GroupId;
class ToxPk;
class IDialogs
@@ -24,6 +25,7 @@ class IDialogs
virtual void removeFriend(const ToxPk& friendPk) = 0;
virtual void removeConference(const ConferenceId& conferenceId) = 0;
+ virtual void removeGroup(const GroupId& groupId) = 0;
virtual int chatroomCount() const = 0;
};
diff --git a/src/model/dialogs/idialogsmanager.h b/src/model/dialogs/idialogsmanager.h
index 6b25426540..cebebd9c68 100644
--- a/src/model/dialogs/idialogsmanager.h
+++ b/src/model/dialogs/idialogsmanager.h
@@ -8,6 +8,7 @@
#include "idialogs.h"
class ConferenceId;
+class GroupId;
class ToxPk;
class IDialogsManager
@@ -22,4 +23,5 @@ class IDialogsManager
virtual IDialogs* getFriendDialogs(const ToxPk& friendPk) const = 0;
virtual IDialogs* getConferenceDialogs(const ConferenceId& conferenceId) const = 0;
+ virtual IDialogs* getGroupDialogs(const GroupId& groupId) const = 0;
};
diff --git a/src/model/friendlist/friendlistmanager.cpp b/src/model/friendlist/friendlistmanager.cpp
index 6db31c3f0f..da17ab7212 100644
--- a/src/model/friendlist/friendlistmanager.cpp
+++ b/src/model/friendlist/friendlistmanager.cpp
@@ -76,16 +76,18 @@ void FriendListManager::resetParents()
}
void FriendListManager::setFilter(const QString& searchString, bool hideOnline, bool hideOffline,
- bool hideConferences)
+ bool hideConferences, bool hideGroups)
{
if (filterParams.searchString == searchString && filterParams.hideOnline == hideOnline
- && filterParams.hideOffline == hideOffline && filterParams.hideConferences == hideConferences) {
+ && filterParams.hideOffline == hideOffline && filterParams.hideConferences == hideConferences
+ && filterParams.hideGroups == hideGroups) {
return;
}
filterParams.searchString = searchString;
filterParams.hideOnline = hideOnline;
filterParams.hideOffline = hideOffline;
filterParams.hideConferences = hideConferences;
+ filterParams.hideGroups = hideGroups;
setSortRequired();
}
@@ -115,6 +117,10 @@ void FriendListManager::applyFilter()
if (filterParams.hideConferences && itemTmp->isConference()) {
itemTmp->setWidgetVisible(false);
}
+
+ if (filterParams.hideGroups && itemTmp->isGroup()) {
+ itemTmp->setWidgetVisible(false);
+ }
}
hideCircles = filterParams.hideOnline && filterParams.hideOffline;
diff --git a/src/model/friendlist/friendlistmanager.h b/src/model/friendlist/friendlistmanager.h
index 00a8134e00..2ecf7d9ff2 100644
--- a/src/model/friendlist/friendlistmanager.h
+++ b/src/model/friendlist/friendlistmanager.h
@@ -32,7 +32,7 @@ class FriendListManager : public QObject
void sortByActivity();
void resetParents();
void setFilter(const QString& searchString, bool hideOnline, bool hideOffline,
- bool hideConferences);
+ bool hideConferences, bool hideGroups);
void applyFilter();
void updatePositions();
void setSortRequired();
@@ -49,6 +49,7 @@ class FriendListManager : public QObject
bool hideOnline = false;
bool hideOffline = false;
bool hideConferences = false;
+ bool hideGroups = false;
} filterParams;
void removeAll(IFriendListItem* item);
diff --git a/src/model/friendlist/ifriendlistitem.h b/src/model/friendlist/ifriendlistitem.h
index d06da15a81..c364356410 100644
--- a/src/model/friendlist/ifriendlistitem.h
+++ b/src/model/friendlist/ifriendlistitem.h
@@ -21,6 +21,7 @@ class IFriendListItem
virtual bool isFriend() const = 0;
virtual bool isConference() const = 0;
+ virtual bool isGroup() const = 0;
virtual bool isOnline() const = 0;
virtual void startCall() = 0;
virtual void stopCall() = 0;
diff --git a/src/model/group.cpp b/src/model/group.cpp
new file mode 100644
index 0000000000..bb9b98ec11
--- /dev/null
+++ b/src/model/group.cpp
@@ -0,0 +1,458 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#include "group.h"
+
+#include "src/core/chatid.h"
+#include "src/core/groupid.h"
+#include "src/core/toxpk.h"
+#include "src/friendlist.h"
+
+#include
+
+#include
+#include
+
+Group::Group(int groupId_, const GroupId persistentGroupId, QString name, QString selfName_,
+ ICoreGroupQuery& groupQuery_, ICoreIdHandler& idHandler_, FriendList& friendList_)
+ : groupQuery(groupQuery_)
+ , idHandler(idHandler_)
+ , selfName{std::move(selfName_)}
+ , toxcoreName{std::move(name)}
+ , toxGroupNum(groupId_)
+ , groupId{persistentGroupId}
+ , friendList{friendList_}
+{
+ hasNewMessages = false;
+ userWasMentioned = false;
+}
+
+void Group::setName(const QString& newTitle)
+{
+ const QString shortTitle = newTitle.left(TOX_GROUP_MAX_GROUP_NAME_LENGTH);
+ if (groupName != shortTitle) {
+ groupName = shortTitle;
+ emit displayedNameChanged(getDisplayedName());
+ emit titleChangedByUser(groupName);
+ emit titleChanged(selfName, getDisplayedName());
+ }
+}
+
+void Group::updateName(const QString& newTitle)
+{
+ const QString shortTitle = newTitle.left(TOX_GROUP_MAX_GROUP_NAME_LENGTH);
+ if (!shortTitle.isEmpty() && toxcoreName != shortTitle) {
+ toxcoreName = shortTitle;
+ emit displayedNameChanged(getDisplayedName());
+ emit titleChanged(selfName, getDisplayedName());
+ }
+}
+
+QString Group::getName() const
+{
+ return groupName;
+}
+
+QString Group::getDisplayedName() const
+{
+ if (!groupName.isEmpty()) {
+ return groupName;
+ }
+ if (!toxcoreName.isEmpty()) {
+ return toxcoreName;
+ }
+ return tr("Group %1").arg(groupId.toString().left(8));
+}
+
+QString Group::getDisplayedName(const ToxPk& contact) const
+{
+ return resolveToxPk(contact);
+}
+
+uint32_t Group::getId() const
+{
+ return toxGroupNum;
+}
+
+void Group::setToxGroupNumber(uint32_t groupNumber)
+{
+ toxGroupNum = groupNumber;
+}
+
+const GroupId& Group::getPersistentId() const
+{
+ return groupId;
+}
+
+int Group::getPeersCount() const
+{
+ return peerDisplayNames.size();
+}
+
+/**
+ * @brief Gets the PKs and names of all peers
+ * @return PKs and names of all peers, including our own PK and name
+ */
+const QMap& Group::getPeerList() const
+{
+ return peerDisplayNames;
+}
+
+bool Group::peerHasNickname(ToxPk pk)
+{
+ return peerDisplayNames.contains(pk);
+}
+
+void Group::setEventFlag(bool f)
+{
+ hasNewMessages = f;
+}
+
+bool Group::getEventFlag() const
+{
+ return hasNewMessages;
+}
+
+void Group::setMentionedFlag(bool f)
+{
+ userWasMentioned = f;
+}
+
+bool Group::getMentionedFlag() const
+{
+ return userWasMentioned;
+}
+
+QString Group::resolveToxPk(const ToxPk& id) const
+{
+ auto it = peerDisplayNames.find(id);
+
+ if (it != peerDisplayNames.end()) {
+ return *it;
+ }
+
+ return {};
+}
+
+ToxPk Group::getSelfPeerPk() const
+{
+ return groupQuery.getGroupSelfPk(toxGroupNum);
+}
+
+void Group::setSelfName(const QString& name)
+{
+ selfName = name;
+}
+
+QString Group::getSelfName() const
+{
+ return selfName;
+}
+
+void Group::setTopic(const QString& author, const QString& newTopic)
+{
+ const QString shortTopic = newTopic.left(TOX_GROUP_MAX_TOPIC_LENGTH);
+ if (topic != shortTopic) {
+ topic = shortTopic;
+ emit topicChanged(author, topic);
+ }
+}
+
+QString Group::getTopic() const
+{
+ return topic;
+}
+
+void Group::setPasswordSet(bool hasPassword_)
+{
+ if (hasPassword != hasPassword_) {
+ hasPassword = hasPassword_;
+ emit passwordSetChanged(hasPassword);
+ }
+}
+
+bool Group::isPasswordSet() const
+{
+ return hasPassword;
+}
+
+void Group::setPeerLimit(uint16_t peerLimit_)
+{
+ if (peerLimit != peerLimit_) {
+ peerLimit = peerLimit_;
+ emit peerLimitChanged(peerLimit);
+ }
+}
+
+uint16_t Group::getPeerLimit() const
+{
+ return peerLimit;
+}
+
+void Group::setTopicLock(GroupTopicLock topicLock_)
+{
+ if (topicLock != topicLock_) {
+ topicLock = topicLock_;
+ emit topicLockChanged(topicLock);
+ }
+}
+
+GroupTopicLock Group::getTopicLock() const
+{
+ return topicLock;
+}
+
+void Group::setVoiceState(GroupVoiceState voiceState_)
+{
+ if (voiceState != voiceState_) {
+ voiceState = voiceState_;
+ emit voiceStateChanged(voiceState);
+ }
+}
+
+GroupVoiceState Group::getVoiceState() const
+{
+ return voiceState;
+}
+
+void Group::setPrivacyState(GroupPrivacyState privacyState_)
+{
+ if (privacyState != privacyState_) {
+ privacyState = privacyState_;
+ emit privacyStateChanged(privacyState);
+ }
+}
+
+GroupPrivacyState Group::getPrivacyState() const
+{
+ return privacyState;
+}
+
+bool Group::setGroupPassword(const QByteArray& password)
+{
+ return groupQuery.setGroupPassword(toxGroupNum, password);
+}
+
+bool Group::setGroupPeerLimit(uint16_t peerLimit_)
+{
+ return groupQuery.setGroupPeerLimit(toxGroupNum, peerLimit_);
+}
+
+bool Group::setGroupTopicLock(GroupTopicLock topicLock_)
+{
+ return groupQuery.setGroupTopicLock(toxGroupNum, topicLock_);
+}
+
+bool Group::setGroupVoiceState(GroupVoiceState voiceState_)
+{
+ return groupQuery.setGroupVoiceState(toxGroupNum, voiceState_);
+}
+
+bool Group::setGroupPrivacyState(GroupPrivacyState privacyState_)
+{
+ return groupQuery.setGroupPrivacyState(toxGroupNum, privacyState_);
+}
+
+bool Group::setGroupNickname(const QString& nickname_)
+{
+ const QString nameToSet = nickname_.isEmpty() ? idHandler.getUsername() : nickname_;
+ if (groupQuery.setGroupSelfName(toxGroupNum, nameToSet)) {
+ nickname = nickname_;
+ emit nicknameChanged(nickname);
+ const uint32_t selfPeerId = groupQuery.getGroupSelfPeerId(toxGroupNum);
+ if (selfPeerId != std::numeric_limits::max()) {
+ onPeerNameChanged(selfPeerId, nameToSet);
+ }
+ return true;
+ }
+ return false;
+}
+
+QString Group::getGroupNickname() const
+{
+ return nickname;
+}
+
+bool Group::setGroupStatus(Status::Status status)
+{
+ if (groupQuery.setGroupSelfStatus(toxGroupNum, status)) {
+ selfStatus = status;
+ const uint32_t selfPeerId = groupQuery.getGroupSelfPeerId(toxGroupNum);
+ if (selfPeerId != std::numeric_limits::max()) {
+ onPeerStatusChanged(selfPeerId, status);
+ }
+ return true;
+ }
+ return false;
+}
+
+Status::Status Group::getGroupStatus() const
+{
+ return selfStatus;
+}
+
+QString Group::resolvePeerName(uint32_t peerId) const
+{
+ const ToxPk pk = groupQuery.getGroupPeerPk(toxGroupNum, peerId);
+ if (pk == getSelfPeerPk()) {
+ return idHandler.getUsername();
+ }
+
+ const QString peerName = groupQuery.getGroupPeerName(toxGroupNum, peerId);
+ return friendList.decideNickname(pk, peerName);
+}
+
+void Group::onPeerJoin(uint32_t peerId)
+{
+ const ToxPk pk = groupQuery.getGroupPeerPk(toxGroupNum, peerId);
+ peerIdToPk[peerId] = pk;
+ peerRoles[pk] = groupQuery.getGroupPeerRole(toxGroupNum, peerId);
+ peerStatuses[pk] = groupQuery.getGroupPeerStatus(toxGroupNum, peerId);
+ const QString name = resolvePeerName(peerId);
+ if (peerDisplayNames.contains(pk)) {
+ if (peerDisplayNames[pk] != name) {
+ const QString oldName = peerDisplayNames[pk];
+ peerDisplayNames[pk] = name;
+ emit peerNameChanged(pk, oldName, name);
+ }
+ return;
+ }
+
+ peerDisplayNames[pk] = name;
+ emit userJoined(pk, name);
+ emit numPeersChanged(peerDisplayNames.size());
+}
+
+void Group::onPeerExit(uint32_t peerId)
+{
+ const ToxPk pk = resolvePeerPk(peerId);
+ peerIdToPk.remove(peerId);
+ peerStatuses.remove(pk);
+ peerRoles.remove(pk);
+ auto it = peerDisplayNames.find(pk);
+ if (it == peerDisplayNames.end()) {
+ return;
+ }
+
+ const QString name = it.value();
+ peerDisplayNames.erase(it);
+ emit userLeft(pk, name);
+ emit numPeersChanged(peerDisplayNames.size());
+}
+
+void Group::clearPeers()
+{
+ peerIdToPk.clear();
+ peerStatuses.clear();
+ peerRoles.clear();
+ peerDisplayNames.clear();
+ emit numPeersChanged(0);
+}
+
+void Group::onPeerNameChanged(uint32_t peerId, const QString& newName)
+{
+ const ToxPk pk = groupQuery.getGroupPeerPk(toxGroupNum, peerId);
+ peerIdToPk[peerId] = pk;
+
+ const QString displayName = friendList.decideNickname(pk, newName);
+ if (!peerDisplayNames.contains(pk)) {
+ peerDisplayNames[pk] = displayName;
+ if (pk == getSelfPeerPk()) {
+ selfName = displayName;
+ }
+ emit userJoined(pk, displayName);
+ emit numPeersChanged(peerDisplayNames.size());
+ return;
+ }
+
+ if (peerDisplayNames[pk] != displayName) {
+ const auto oldName = peerDisplayNames[pk];
+ peerDisplayNames[pk] = displayName;
+ if (pk == getSelfPeerPk()) {
+ selfName = displayName;
+ }
+ emit peerNameChanged(pk, oldName, displayName);
+ }
+}
+
+void Group::onPeerStatusChanged(uint32_t peerId, Status::Status status)
+{
+ const ToxPk pk = groupQuery.getGroupPeerPk(toxGroupNum, peerId);
+ peerIdToPk[peerId] = pk;
+
+ if (pk == getSelfPeerPk()) {
+ selfStatus = status;
+ }
+
+ if (peerStatuses.value(pk, Status::Status::Online) != status) {
+ peerStatuses[pk] = status;
+ emit peerStatusChanged(pk, status);
+ }
+}
+
+void Group::updatePeerRoles()
+{
+ bool changed = false;
+ for (auto it = peerIdToPk.cbegin(); it != peerIdToPk.cend(); ++it) {
+ const GroupRole role = groupQuery.getGroupPeerRole(toxGroupNum, it.key());
+ if (peerRoles.value(it.value(), GroupRole::Unknown) != role) {
+ peerRoles[it.value()] = role;
+ changed = true;
+ }
+ }
+ if (changed) {
+ emit peerRolesChanged();
+ }
+}
+
+GroupRole Group::getPeerRole(const ToxPk& pk) const
+{
+ return peerRoles.value(pk, GroupRole::User);
+}
+
+Status::Status Group::getPeerStatus(const ToxPk& pk) const
+{
+ return peerStatuses.value(pk, Status::Status::Online);
+}
+
+bool Group::setPeerRole(const ToxPk& pk, GroupRole role)
+{
+ for (auto it = peerIdToPk.cbegin(); it != peerIdToPk.cend(); ++it) {
+ if (it.value() == pk) {
+ return groupQuery.setGroupPeerRole(toxGroupNum, it.key(), role);
+ }
+ }
+ qWarning() << "setPeerRole: unknown peer" << pk.toString();
+ return false;
+}
+
+bool Group::kickPeer(const ToxPk& pk)
+{
+ for (auto it = peerIdToPk.cbegin(); it != peerIdToPk.cend(); ++it) {
+ if (it.value() == pk) {
+ if (groupQuery.kickGroupPeer(toxGroupNum, it.key())) {
+ onPeerExit(it.key());
+ return true;
+ }
+ return false;
+ }
+ }
+ qWarning() << "kickPeer: unknown peer" << pk.toString();
+ return false;
+}
+
+ToxPk Group::resolvePeerPk(uint32_t peerId) const
+{
+ return peerIdToPk.value(peerId, ToxPk{});
+}
+
+uint32_t Group::getPeerId(const ToxPk& pk) const
+{
+ for (auto it = peerIdToPk.cbegin(); it != peerIdToPk.cend(); ++it) {
+ if (it.value() == pk) {
+ return it.key();
+ }
+ }
+ return std::numeric_limits::max();
+}
diff --git a/src/model/group.h b/src/model/group.h
new file mode 100644
index 0000000000..a493e9fcd3
--- /dev/null
+++ b/src/model/group.h
@@ -0,0 +1,132 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#pragma once
+
+#include "chat.h"
+
+#include "src/core/chatid.h"
+#include "src/core/groupid.h"
+#include "src/core/icoregroupquery.h"
+#include "src/core/icoreidhandler.h"
+#include "src/core/toxpk.h"
+
+#include
+#include
+#include
+#include
+#include
+
+class FriendList;
+
+class Group : public Chat
+{
+ Q_OBJECT
+public:
+ Group(int groupId_, GroupId persistentGroupId, QString groupName, QString selfName_,
+ ICoreGroupQuery& groupQuery_, ICoreIdHandler& idHandler_, FriendList& friendList);
+ uint32_t getId() const override;
+ const GroupId& getPersistentId() const override;
+ void setToxGroupNumber(uint32_t groupNumber);
+ int getPeersCount() const;
+ const QMap& getPeerList() const;
+ bool peerHasNickname(ToxPk pk);
+
+ void setEventFlag(bool f) override;
+ bool getEventFlag() const override;
+
+ void setMentionedFlag(bool f);
+ bool getMentionedFlag() const;
+
+ void setName(const QString& newTitle) override;
+ void updateName(const QString& newTitle);
+ QString getName() const;
+ QString getDisplayedName() const override;
+ QString getDisplayedName(const ToxPk& contact) const override;
+ QString resolveToxPk(const ToxPk& id) const;
+ ToxPk getSelfPeerPk() const;
+ void setSelfName(const QString& name);
+ QString getSelfName() const;
+
+ void setTopic(const QString& author, const QString& newTopic);
+ QString getTopic() const;
+
+ void setPasswordSet(bool hasPassword);
+ bool isPasswordSet() const;
+ void setPeerLimit(uint16_t peerLimit);
+ uint16_t getPeerLimit() const;
+ void setTopicLock(GroupTopicLock topicLock);
+ GroupTopicLock getTopicLock() const;
+ void setVoiceState(GroupVoiceState voiceState);
+ GroupVoiceState getVoiceState() const;
+ void setPrivacyState(GroupPrivacyState privacyState);
+ GroupPrivacyState getPrivacyState() const;
+
+ bool setGroupPassword(const QByteArray& password);
+ bool setGroupPeerLimit(uint16_t peerLimit);
+ bool setGroupTopicLock(GroupTopicLock topicLock);
+ bool setGroupVoiceState(GroupVoiceState voiceState);
+ bool setGroupPrivacyState(GroupPrivacyState privacyState);
+ bool setGroupNickname(const QString& nickname);
+ QString getGroupNickname() const;
+ bool setGroupStatus(Status::Status status);
+ Status::Status getGroupStatus() const;
+
+ void onPeerJoin(uint32_t peerId);
+ void onPeerExit(uint32_t peerId);
+ void clearPeers();
+ void onPeerNameChanged(uint32_t peerId, const QString& newName);
+ void onPeerStatusChanged(uint32_t peerId, Status::Status status);
+ void updatePeerRoles();
+ GroupRole getPeerRole(const ToxPk& pk) const;
+ Status::Status getPeerStatus(const ToxPk& pk) const;
+ bool setPeerRole(const ToxPk& pk, GroupRole role);
+ bool kickPeer(const ToxPk& pk);
+ ToxPk resolvePeerPk(uint32_t peerId) const;
+ uint32_t getPeerId(const ToxPk& pk) const;
+
+signals:
+ void titleChanged(const QString& author, const QString& title);
+ void titleChangedByUser(const QString& title);
+ void topicChanged(const QString& author, const QString& topic);
+ void userJoined(const ToxPk& user, const QString& name);
+ void userLeft(const ToxPk& user, const QString& name);
+ void numPeersChanged(int numPeers);
+ void peerNameChanged(const ToxPk& peer, const QString& oldName, const QString& newName);
+ void peerStatusChanged(const ToxPk& peer, Status::Status status);
+ void peerRolesChanged();
+ void passwordSetChanged(bool hasPassword);
+ void peerLimitChanged(uint16_t peerLimit);
+ void topicLockChanged(GroupTopicLock topicLock);
+ void voiceStateChanged(GroupVoiceState voiceState);
+ void privacyStateChanged(GroupPrivacyState privacyState);
+ void nicknameChanged(const QString& nickname);
+
+private:
+ QString resolvePeerName(uint32_t peerId) const;
+
+private:
+ ICoreGroupQuery& groupQuery;
+ ICoreIdHandler& idHandler;
+ QString selfName;
+ QString groupName;
+ QString toxcoreName;
+ QString topic;
+ QString nickname;
+ Status::Status selfStatus = Status::Status::Online;
+ bool hasPassword = false;
+ uint16_t peerLimit = 0;
+ GroupTopicLock topicLock = GroupTopicLock::Unknown;
+ GroupVoiceState voiceState = GroupVoiceState::Unknown;
+ GroupPrivacyState privacyState = GroupPrivacyState::Unknown;
+ QMap peerDisplayNames;
+ QMap peerStatuses;
+ QMap peerIdToPk;
+ QMap peerRoles;
+ bool hasNewMessages;
+ bool userWasMentioned;
+ int toxGroupNum;
+ const GroupId groupId;
+ FriendList& friendList;
+};
diff --git a/src/model/groupinvite.cpp b/src/model/groupinvite.cpp
new file mode 100644
index 0000000000..31db8e6b39
--- /dev/null
+++ b/src/model/groupinvite.cpp
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#include "groupinvite.h"
+
+#include
+
+/**
+ * @class GroupInvite
+ *
+ * @brief This class contains information needed to accept a group invite
+ */
+
+GroupInvite::GroupInvite(uint32_t friendId_, QByteArray inviteData_, QString groupName_)
+ : friendId{friendId_}
+ , inviteData{std::move(inviteData_)}
+ , groupName{std::move(groupName_)}
+ , date{QDateTime::currentDateTime()}
+{
+}
+
+bool GroupInvite::operator==(const GroupInvite& other) const
+{
+ return friendId == other.friendId && inviteData == other.inviteData
+ && groupName == other.groupName && date == other.date;
+}
+
+uint32_t GroupInvite::getFriendId() const
+{
+ return friendId;
+}
+
+QByteArray GroupInvite::getInviteData() const
+{
+ return inviteData;
+}
+
+QString GroupInvite::getGroupName() const
+{
+ return groupName;
+}
+
+QDateTime GroupInvite::getInviteDate() const
+{
+ return date;
+}
diff --git a/src/model/groupinvite.h b/src/model/groupinvite.h
new file mode 100644
index 0000000000..c81a6e352c
--- /dev/null
+++ b/src/model/groupinvite.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+#include
+
+class GroupInvite
+{
+public:
+ GroupInvite() = default;
+ GroupInvite(uint32_t friendId_, QByteArray inviteData_, QString groupName_);
+ bool operator==(const GroupInvite& other) const;
+
+ uint32_t getFriendId() const;
+ QByteArray getInviteData() const;
+ QString getGroupName() const;
+ QDateTime getInviteDate() const;
+
+private:
+ uint32_t friendId{0};
+ QByteArray inviteData;
+ QString groupName;
+ QDateTime date;
+};
diff --git a/src/model/groupmessagedispatcher.cpp b/src/model/groupmessagedispatcher.cpp
new file mode 100644
index 0000000000..b9f7000716
--- /dev/null
+++ b/src/model/groupmessagedispatcher.cpp
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#include "groupmessagedispatcher.h"
+
+#include "src/persistence/settings.h"
+
+#include
+
+GroupMessageDispatcher::GroupMessageDispatcher(Group& g_, MessageProcessor processor_,
+ ICoreIdHandler& idHandler_,
+ ICoreGroupMessageSender& messageSender_,
+ Settings& settings_)
+ : group(g_)
+ , processor(processor_)
+ , idHandler(idHandler_)
+ , messageSender(messageSender_)
+ , settings(settings_)
+{
+ processor.enableMentions();
+}
+
+std::pair
+GroupMessageDispatcher::sendMessage(bool isAction, const QString& content)
+{
+ const auto firstMessageId = nextMessageId;
+ auto lastMessageId = firstMessageId;
+
+ for (const auto& message : processor.processOutgoingMessage(isAction, content)) {
+ auto messageId = nextMessageId++;
+ lastMessageId = messageId;
+ if (message.isAction) {
+ messageSender.sendGroupAction(group.getId(), message.content);
+ } else {
+ messageSender.sendGroupMessage(group.getId(), message.content);
+ }
+
+ // Emit both signals since we do not have receipts for groups
+ emit messageSent(messageId, message);
+ emit messageComplete(messageId);
+ }
+
+ return std::make_pair(firstMessageId, lastMessageId);
+}
+
+std::pair
+GroupMessageDispatcher::sendPrivateMessage(uint32_t peerId, bool isAction, const QString& content)
+{
+ const auto firstMessageId = nextMessageId;
+ auto lastMessageId = firstMessageId;
+ const ToxPk recipientPk = group.resolvePeerPk(peerId);
+ const QString recipientName = group.getDisplayedName(recipientPk);
+
+ for (const auto& message : processor.processOutgoingMessage(isAction, content)) {
+ auto messageId = nextMessageId++;
+ lastMessageId = messageId;
+ const Tox_Message_Type type = isAction ? TOX_MESSAGE_TYPE_ACTION : TOX_MESSAGE_TYPE_NORMAL;
+ messageSender.sendGroupPrivateMessage(group.getId(), peerId, message.content, type);
+
+ Message messageWithRecipient = message;
+ messageWithRecipient.recipient = recipientPk;
+ messageWithRecipient.recipientName = recipientName;
+ emit messageSent(messageId, messageWithRecipient);
+ emit messageComplete(messageId);
+ }
+
+ return std::make_pair(firstMessageId, lastMessageId);
+}
+
+/**
+ * @brief Processes and dispatches received message from toxcore
+ * @param[in] sender
+ * @param[in] isAction True if is action
+ * @param[in] content Message content
+ */
+void GroupMessageDispatcher::onMessageReceived(const ToxPk& sender, bool isAction,
+ const QString& content)
+{
+ const bool isSelf = sender == group.getSelfPeerPk();
+
+ if (isSelf) {
+ return;
+ }
+
+ if (settings.getBlockList().contains(sender.toString())) {
+ qDebug() << "onGroupMessageReceived: Filtered:" << sender.toString();
+ return;
+ }
+
+ emit messageReceived(sender, processor.processIncomingCoreMessage(isAction, content));
+}
+
+void GroupMessageDispatcher::onPrivateMessageReceived(const ToxPk& sender, bool isAction,
+ const QString& content)
+{
+ const bool isSelf = sender == group.getSelfPeerPk();
+
+ if (isSelf) {
+ return;
+ }
+
+ if (settings.getBlockList().contains(sender.toString())) {
+ qDebug() << "onGroupPrivateMessageReceived: Filtered:" << sender.toString();
+ return;
+ }
+
+ Message message = processor.processIncomingCoreMessage(isAction, content);
+ message.recipient = group.getSelfPeerPk();
+ message.recipientName = idHandler.getUsername();
+ emit messageReceived(sender, message);
+}
diff --git a/src/model/groupmessagedispatcher.h b/src/model/groupmessagedispatcher.h
new file mode 100644
index 0000000000..c0184b6da3
--- /dev/null
+++ b/src/model/groupmessagedispatcher.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * Copyright © 2024-2026 The TokTok team.
+ */
+
+#pragma once
+
+#include "src/core/icoregroupmessagesender.h"
+#include "src/core/icoreidhandler.h"
+#include "src/model/group.h"
+#include "src/model/imessagedispatcher.h"
+#include "src/model/message.h"
+
+#include
+#include
+
+class Settings;
+
+class GroupMessageDispatcher : public IMessageDispatcher
+{
+ Q_OBJECT
+public:
+ GroupMessageDispatcher(Group& g_, MessageProcessor processor, ICoreIdHandler& idHandler,
+ ICoreGroupMessageSender& messageSender, Settings& settings);
+
+ std::pair sendMessage(bool isAction,
+ const QString& content) override;
+
+ std::pair sendPrivateMessage(uint32_t peerId,
+ bool isAction,
+ const QString& content);
+
+ void onMessageReceived(const ToxPk& sender, bool isAction, const QString& content);
+ void onPrivateMessageReceived(const ToxPk& sender, bool isAction, const QString& content);
+
+private:
+ Group& group;
+ MessageProcessor processor;
+ ICoreIdHandler& idHandler;
+ ICoreGroupMessageSender& messageSender;
+ Settings& settings;
+ DispatchedMessageId nextMessageId{0};
+};
diff --git a/src/model/message.h b/src/model/message.h
index d035123eb0..815d52d9cd 100644
--- a/src/model/message.h
+++ b/src/model/message.h
@@ -5,6 +5,8 @@
#pragma once
+#include "src/core/toxpk.h"
+
#include
#include