messenger-android

Android graphical user interfaces for GNUnet Messenger
Log | Files | Refs | README | LICENSE

commit 8faafabb765d052fec69ada363dc132264ac7668
parent 9cad087f70c97c1488075ac9eff99bc11119d4b5
Author: t3sserakt <t3ss@posteo.de>
Date:   Mon, 20 Apr 2026 21:30:20 +0200

group fixed

Diffstat:
MGNUnetMessenger/app/src/main/aidl/org/gnunet/gnunetmessenger/ipc/IGnunetChat.aidl | 4++++
AGNUnetMessenger/app/src/main/aidl/org/gnunet/gnunetmessenger/ipc/IMessageIterateCallback.aidl | 9+++++++++
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt | 61++++++++++++++++++++++++++++++++++++++++++++++---------------
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/GnunetChat.kt | 2++
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/boundimpl/GnunetChatBoundService.kt | 26++++++++++++++++++++++++++
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/mock/GnunetChatMock.kt | 4++++
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/AccountListFragment.kt | 47+++++++++++++++++++++++++++--------------------
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/chat/ChatFragment.kt | 23+++++++++++++++++++++++
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/chat/MemberListFragment.kt | 3+++
9 files changed, 144 insertions(+), 35 deletions(-)

diff --git a/GNUnetMessenger/app/src/main/aidl/org/gnunet/gnunetmessenger/ipc/IGnunetChat.aidl b/GNUnetMessenger/app/src/main/aidl/org/gnunet/gnunetmessenger/ipc/IGnunetChat.aidl @@ -11,6 +11,7 @@ import org.gnunet.gnunetmessenger.ipc.IGroupCallback; import org.gnunet.gnunetmessenger.ipc.IGroupContactCallback; import org.gnunet.gnunetmessenger.ipc.IAttributeCallback; import org.gnunet.gnunetmessenger.ipc.ILobbyCallback; +import org.gnunet.gnunetmessenger.ipc.IMessageIterateCallback; import org.gnunet.gnunetmessenger.ipc.ChatContextDto; import org.gnunet.gnunetmessenger.ipc.ChatMessageDto; @@ -88,4 +89,7 @@ interface IGnunetChat { void getContactAttributes(in ChatContactDto contact, IAttributeCallback cb); void shareAttributes(long handle, in ChatContactDto contact, String key); void unshareAttributes(long handle, in ChatContactDto contact, String key); + + // Message iteration + void iterateContextMessages(in ChatContextDto context, IMessageIterateCallback cb); } diff --git a/GNUnetMessenger/app/src/main/aidl/org/gnunet/gnunetmessenger/ipc/IMessageIterateCallback.aidl b/GNUnetMessenger/app/src/main/aidl/org/gnunet/gnunetmessenger/ipc/IMessageIterateCallback.aidl @@ -0,0 +1,9 @@ +package org.gnunet.gnunetmessenger.ipc; + +import org.gnunet.gnunetmessenger.ipc.ChatMessageDto; + +interface IMessageIterateCallback { + void onMessage(in ChatMessageDto message); + void onDone(); + void onError(int code, String message); +} diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt @@ -278,6 +278,10 @@ class MainActivity : AppCompatActivity() { } MessageKind.INVITATION -> { + // Invitation is auto-accepted at the native layer. + // Reload chats so the newly joined group appears in the overview. + Log.d(TAG, "Received INVITATION — reloading chats to show new group") + loadChats() } MessageKind.TEXT, @@ -322,26 +326,38 @@ class MainActivity : AppCompatActivity() { } } + // Stable identifier for a chat context, independent of the native + // pointer (which can drift between listGroups/listContacts refreshes + // and is blank for newly-built contexts). This is what we key the + // per-chat ViewModels by so that messages survive navigation. + private fun stableChatKey(chatContext: ChatContext): String? { + runCatching { gnunetChat.getGroupFromContext(chatContext) } + .getOrNull() + ?.takeIf { it.name.isNotBlank() } + ?.let { return "group:${it.name}" } + + runCatching { gnunetChat.getContextContact(chatContext) } + .getOrNull() + ?.key + ?.takeIf { it.isNotBlank() } + ?.let { return "contact:$it" } + + return chatContext.userPointer?.takeIf { it.isNotBlank() } + } + fun getChatViewModel(chatContext: ChatContext): ChatViewModel? { - val key = gnunetChat.getUserPointerForContext(chatContext) - var chatViewModel: ChatViewModel? = null - if (key != null) { - chatViewModel = chatViewModels.getOrPut(key) { - ChatViewModel(chatContext) - } - } - return chatViewModel + val key = stableChatKey(chatContext) ?: return null + val isNew = key !in chatViewModels + val vm = chatViewModels.getOrPut(key) { ChatViewModel(chatContext) } + Log.d(TAG, "getChatViewModel key=$key isNew=$isNew msgCount=${vm.messages.value?.size ?: 0}") + return vm } fun getChatMenuViewModel(chatContext: ChatContext): ChatMenuViewModel? { - val key = gnunetChat.getUserPointerForContext(chatContext) - var chatMenuViewModel: ChatMenuViewModel? = null - if (key != null) { - chatMenuViewModel = chatMenuViewModels.getOrPut(key) { - ChatMenuViewModel(chatContext) - } + val key = stableChatKey(chatContext) ?: return null + return chatMenuViewModels.getOrPut(key) { + ChatMenuViewModel(chatContext) } - return chatMenuViewModel } fun loadChats() { @@ -392,6 +408,8 @@ class MainActivity : AppCompatActivity() { } val groupList = gnunetChat.listGroups(handle) + val profileKey = gnunetChat.getProfileKey(handle) + Log.d(TAG, "loadChats: profileKey=${profileKey.take(12)}, found ${groupList.size} groups: ${groupList.map { "${it.name}(ptr=${it.userPointer})" }}") for (group in groupList) { val chatContext = gnunetChat.getGroupContext(group) var uuid = gnunetChat.getUserPointerForContext(chatContext) @@ -409,6 +427,19 @@ class MainActivity : AppCompatActivity() { Log.w(TAG, "listGroupContacts failed for ${group.name}", t) emptyList() } + + // Log membership info for debugging. + // NOTE: We no longer filter groups by membership here because + // invited accounts may not yet appear in listGroupContacts() + // until they send a JOIN message. The monolithic daemon exposes + // all groups to all handles — a proper fix requires native-level + // per-account context filtering. + if (members.isNotEmpty()) { + Log.d(TAG, "Group '${group.name}' members: ${members.map { "${it.name}(${it.key.take(8)})" }}, profileKey=${profileKey.take(8)}") + } else { + Log.d(TAG, "Group '${group.name}' has no members listed yet") + } + val namedMembers = members.filter { it.name.isNotBlank() } val memberPreview = if (namedMembers.isNotEmpty()) { namedMembers.joinToString(", ") { it.name } + " joined the chat" diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/GnunetChat.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/GnunetChat.kt @@ -84,4 +84,6 @@ interface GnunetChat { fun getContactAttributes(contact: ChatContact, callback: (String, String) -> Unit) fun shareAttributes(handle: ChatHandle, contact: ChatContact, key: String) fun unshareAttributes(handle: ChatHandle, contact: ChatContact, key: String) + + suspend fun iterateContextMessages(context: ChatContext): List<ChatMessage> } \ No newline at end of file diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/boundimpl/GnunetChatBoundService.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/boundimpl/GnunetChatBoundService.kt @@ -988,6 +988,32 @@ class GnunetChatBoundService( } } + override suspend fun iterateContextMessages(context: ChatContext): List<ChatMessage> { + val messages = mutableListOf<ChatMessage>() + val done = CompletableDeferred<Unit>() + + withReadyRemote(lastHandle) { remote, _ -> + val cb = object : IMessageIterateCallback.Stub() { + override fun onMessage(message: ChatMessageDto) { + val msg = message.toLocal(context) + messages.add(msg) + } + override fun onDone() { + done.complete(Unit) + } + override fun onError(code: Int, message: String?) { + done.completeExceptionally( + RuntimeException("iterateContextMessages failed: $code $message") + ) + } + } + remote.iterateContextMessages(context.toDto(), cb) + } + + done.await() + return messages + } + companion object { private const val TAG = "GnunetChatBoundService" private const val ACTION_BIND_GNUNET_CHAT = diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/mock/GnunetChatMock.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/mock/GnunetChatMock.kt @@ -460,6 +460,10 @@ class GnunetChatMock : GnunetChat { println("unshare ${key} for contact ${contact.name}") } + override suspend fun iterateContextMessages(context: ChatContext): List<ChatMessage> { + return emptyList() + } + override suspend fun reset() { if (!org.gnunet.gnunetmessenger.BuildConfig.ALLOW_RESET) { println("reset: BLOCKED - Reset not allowed in production builds") diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/AccountListFragment.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/AccountListFragment.kt @@ -79,28 +79,35 @@ class AccountListFragment : Fragment() { val handle = activity.getChatHandle() viewLifecycleOwner.lifecycleScope.launch { - try { - if (activity.currentAccount != null) { - activity.clearChatState() - runCatching { gnunetChat.disconnect(handle) } - .onFailure { - Log.w(TAG, "disconnect before connect failed", it) - } + // If user re-selects the already-connected account, skip the + // disconnect/reconnect cycle so we don't wipe in-memory messages. + val current = activity.currentAccount + val isSameAccount = current != null && current.name == selectedAccount.name + + if (!isSameAccount) { + try { + if (current != null) { + activity.clearChatState() + runCatching { gnunetChat.disconnect(handle) } + .onFailure { + Log.w(TAG, "disconnect before connect failed", it) + } + } + + gnunetChat.connect(handle, selectedAccount) + selectedAccount.key = gnunetChat.getProfileKey(handle) + activity.setCurrentAccount(selectedAccount) + } catch (t: Throwable) { + Log.e(TAG, "Connecting account failed", t) + showError(getString(R.string.account_connect_failed)) + return@launch } - gnunetChat.connect(handle, selectedAccount) - selectedAccount.key = gnunetChat.getProfileKey(handle) - activity.setCurrentAccount(selectedAccount) - } catch (t: Throwable) { - Log.e(TAG, "Connecting account failed", t) - showError(getString(R.string.account_connect_failed)) - return@launch - } - - try { - activity.loadChatsAndWait() - } catch (t: Throwable) { - Log.w(TAG, "Initial chat load failed, LOGIN will retry", t) + try { + activity.loadChatsAndWait() + } catch (t: Throwable) { + Log.w(TAG, "Initial chat load failed, LOGIN will retry", t) + } } val action = diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/chat/ChatFragment.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/chat/ChatFragment.kt @@ -25,6 +25,7 @@ package org.gnunet.gnunetmessenger.ui.chat import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -38,10 +39,12 @@ import androidx.core.view.MenuHost import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.launch import org.gnunet.gnunetmessenger.MainActivity import org.gnunet.gnunetmessenger.R import org.gnunet.gnunetmessenger.model.ChatContact @@ -187,6 +190,26 @@ class ChatFragment : Fragment(R.layout.fragment_chat) { recyclerView.scrollToPosition(messages.size - 1) }) + // If the ViewModel has no messages (e.g., after account switch cleared + // state), reload history from libgnunetchat via native iteration. + if (chatViewModel.messages.value.isNullOrEmpty()) { + viewLifecycleOwner.lifecycleScope.launch { + try { + val profileKey = gnunetChat.getProfileKey(mainActivity.getChatHandle()) + val history = gnunetChat.iterateContextMessages(chatContext) + for (msg in history) { + val isOwn = msg.sender?.key?.isNotBlank() == true && + msg.sender.key == profileKey + val typed = msg.copy(type = if (isOwn) ChatMessageType.OWN else ChatMessageType.OTHER) + chatViewModel.addMessage(typed) + } + Log.d("ChatFragment", "Loaded ${history.size} messages from native") + } catch (t: Throwable) { + Log.w("ChatFragment", "iterateContextMessages failed", t) + } + } + } + view.findViewById<Button>(R.id.sendButton).setOnClickListener { val input = view.findViewById<EditText>(R.id.inputMessage) val text = input.text.toString() diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/chat/MemberListFragment.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/chat/MemberListFragment.kt @@ -55,6 +55,7 @@ class MemberListFragment : Fragment(R.layout.fragment_member_list) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val chatGroup = args.chatGroup + Log.d("MemberListFragment", "Opened for group '${chatGroup.name}' userPointer=${chatGroup.userPointer}") val mainActivity = requireActivity() as MainActivity val gnunetChat = mainActivity.getGnunetChatInstance() @@ -102,7 +103,9 @@ class MemberListFragment : Fragment(R.layout.fragment_member_list) { inviteButton.setOnClickListener { val selectedContacts = adapter.getSelectedContacts() + Log.d("MemberListFragment", "Inviting ${selectedContacts.size} contacts to group '${chatGroup.name}' (groupPtr=${chatGroup.userPointer})") for (contact in selectedContacts) { + Log.d("MemberListFragment", " Inviting contact '${contact.name}' (contactPtr=${contact.userPointer}, key=${contact.key.take(8)})") gnunetChat.inviteContactToGroup(chatGroup, contact) } findNavController().popBackStack()