messenger-android

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

commit 29dc79f8f0d0135646c43cbb8e010136c17fdd2f
parent 618db56d5b4ed6b1915f23d543e9f629cbc6291a
Author: t3sserakt <t3sserakt@posteo.de>
Date:   Tue, 17 Mar 2026 19:31:50 +0100

UI waiting for startChat.

Diffstat:
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt | 186+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/AccountListFragment.kt | 104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
MGNUnetMessenger/app/src/main/res/layout/fragment_account_list.xml | 28++++++++++++++++++++++++----
MGNUnetMessenger/app/src/main/res/values/strings.xml | 17++++++++++++-----
4 files changed, 242 insertions(+), 93 deletions(-)

diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt @@ -25,20 +25,23 @@ package org.gnunet.gnunetmessenger import android.os.Bundle -import android.view.Menu -import android.view.MenuItem +import android.util.Log import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.NavigationUI -import androidx.appcompat.widget.Toolbar -import androidx.navigation.fragment.NavHostFragment +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import org.gnunet.gnunetmessenger.model.ChatAccount import org.gnunet.gnunetmessenger.model.ChatContact import org.gnunet.gnunetmessenger.model.ChatContext import org.gnunet.gnunetmessenger.model.ChatHandle -import org.gnunet.gnunetmessenger.viewmodel.ChatMenuViewModel import org.gnunet.gnunetmessenger.model.ChatMessage import org.gnunet.gnunetmessenger.model.ChatMessageType import org.gnunet.gnunetmessenger.model.ChatSummary @@ -46,6 +49,7 @@ import org.gnunet.gnunetmessenger.model.MessageKind import org.gnunet.gnunetmessenger.model.MessengerApp import org.gnunet.gnunetmessenger.service.GnunetChat import org.gnunet.gnunetmessenger.service.ServiceFactory +import org.gnunet.gnunetmessenger.viewmodel.ChatMenuViewModel import org.gnunet.gnunetmessenger.viewmodel.ChatOverviewViewModel import org.gnunet.gnunetmessenger.viewmodel.ChatViewModel import org.gnunet.gnunetmessenger.viewmodel.ContactListViewModel @@ -56,153 +60,195 @@ class MainActivity : AppCompatActivity() { private lateinit var navController: NavController private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var handle: ChatHandle + + private val chatReady = CompletableDeferred<ChatHandle>() + private val chatOverviewViewModel: ChatOverviewViewModel by viewModels() val contactListViewModel: ContactListViewModel by viewModels() + private val chatMenuViewModels = mutableMapOf<String, ChatMenuViewModel>() private val chatViewModels = mutableMapOf<String, ChatViewModel>() private val chats = mutableMapOf<String, ChatContext>() + var currentAccount: ChatAccount? = null private set + companion object { + private const val TAG = "MainActivity" + private const val CHAT_READY_POLL_MS = 50L + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - // Initialize GnunetChat (keep this line!) gnunetChat = ServiceFactory.create(this, useMock = false) val app = MessengerApp() handle = gnunetChat.startChat(app) { chatContext, chatMessage -> processChatMessage(chatContext, chatMessage) } - // Initialize the NavController - val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + + lifecycleScope.launch { + try { + awaitHandlePointerReady() + if (!chatReady.isCompleted) { + chatReady.complete(handle) + } + Log.d(TAG, "Chat is ready with handle=${handle.pointer}") + } catch (t: Throwable) { + Log.e(TAG, "Chat initialization failed", t) + if (!chatReady.isCompleted) { + chatReady.completeExceptionally(t) + } + } + } + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment navController = navHostFragment.navController - //navController = findNavController(R.id.nav_host_fragment) - // Set up the Toolbar with the NavController val toolbar = findViewById<Toolbar>(R.id.nav_bar) setSupportActionBar(toolbar) - // Define the AppBarConfiguration (Drawer layout can be added later if needed) appBarConfiguration = AppBarConfiguration( - setOf(R.id.accountListFragment), // Define start destination fragment - null // No drawer layout for now + setOf(R.id.accountListFragment), + null ) - // Set up ActionBar with NavController NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration) } - private fun processChatMessage(chatContext: ChatContext, chatMessage: ChatMessage){ - requireNotNull(chatMessage) + private suspend fun awaitHandlePointerReady(): ChatHandle { + while (lifecycleScope.coroutineContext.isActive && handle.pointer == 0L) { + delay(CHAT_READY_POLL_MS) + } + check(handle.pointer != 0L) { "Chat handle was not initialized" } + return handle + } - /*if (chatMessage.isDeleted()) { - app.callMessageEvent(EventType.DELETE_MESSAGE, chatContext, chatMessage) - return true - }*/ + suspend fun awaitChatReady(): ChatHandle { + return chatReady.await() + } + + fun isChatReady(): Boolean { + return chatReady.isCompleted && handle.pointer != 0L + } + + private fun processChatMessage(chatContext: ChatContext, chatMessage: ChatMessage) { + requireNotNull(chatMessage) when (chatMessage.kind) { MessageKind.WARNING -> { - //app.callMessageEvent(EventType.HANDLE_WARNING, chatContext, chatMessage) } + MessageKind.REFRESH -> { - //app.callEvent(EventType.REFRESH_ACCOUNTS) } + MessageKind.LOGIN -> { loadChats() } + MessageKind.LOGOUT -> { - println("logout") + Log.d(TAG, "Received LOGOUT") contactListViewModel.clearModel() chatOverviewViewModel.clearModel() contactListViewModel.clearModel() chatViewModels.values.forEach { it.clearModel() } chats.clear() } + MessageKind.CREATED_ACCOUNT, MessageKind.UPDATE_ACCOUNT -> { - //app.callMessageEvent(EventType.SELECT_PROFILE, chatContext, chatMessage) } + MessageKind.UPDATE_CONTEXT -> { - //app.callMessageEvent(EventType.UPDATE_CHATS, chatContext, chatMessage) } + MessageKind.JOIN, MessageKind.LEAVE -> { var uuid = gnunetChat.getUserPointerForContext(chatContext) val chatContact = gnunetChat.getSenderFromMessage(chatMessage) - val lastMessagePreview = if (chatMessage.kind == MessageKind.JOIN) "${chatContact.name} joined the chat" else "${chatContact.name} left the chat" + val lastMessagePreview = if (chatMessage.kind == MessageKind.JOIN) { + "${chatContact.name} joined the chat" + } else { + "${chatContact.name} left the chat" + } val viewModel = getChatViewModel(chatContext) chatMessage.text = lastMessagePreview chatMessage.type = ChatMessageType.SYSTEM - - if (null == uuid) { - assert(chatMessage.kind == MessageKind.JOIN) + if (uuid == null) { uuid = gnunetChat.randomUUID() - chats.put(uuid, chatContext) - chatOverviewViewModel.addOrUpdateChat(ChatSummary(chatContext,chatContact.name,lastMessagePreview)) + chats[uuid] = chatContext + chatOverviewViewModel.addOrUpdateChat( + ChatSummary(chatContext, chatContact.name, lastMessagePreview) + ) } else { val localChatContext = chats[uuid] val group = gnunetChat.getGroupFromContext(chatContext) - if (null != localChatContext && null != group) { + if (localChatContext != null && group != null) { if (MessageKind.JOIN == chatMessage.kind) { chatOverviewViewModel.addOrUpdateChat( ChatSummary( localChatContext, group.name, - chatContact.name+" "+lastMessagePreview + "${chatContact.name} $lastMessagePreview" ) ) } } } viewModel?.addMessage(chatMessage) - } + MessageKind.CONTACT, MessageKind.SHARED_ATTRIBUTES -> { - //app.callMessageEvent(EventType.UPDATE_CONTACTS, chatContext, chatMessage) } + MessageKind.INVITATION -> { - //app.callMessageEvent(EventType.INVITATION, chatContext, chatMessage) } + MessageKind.TEXT, MessageKind.FILE -> { val viewModel = getChatViewModel(chatContext) val uuid = gnunetChat.getUserPointerForContext(chatContext) val localChatContext = chats[uuid] - chatMessage.type = if (gnunetChat.getContactKey(chatMessage.sender!!) == gnunetChat.getProfileKey(handle)) - ChatMessageType.OWN - else ChatMessageType.OTHER + chatMessage.type = + if (gnunetChat.getContactKey(chatMessage.sender!!) == gnunetChat.getProfileKey( + handle + ) + ) { + ChatMessageType.OWN + } else { + ChatMessageType.OTHER + } if (localChatContext != null) { viewModel?.addMessage(chatMessage) chatOverviewViewModel.updateChatMessage(localChatContext, chatMessage) } } + MessageKind.DELETION -> { - /*val target = chatMessage.getTarget() - if (target != null) { - //app.callMessageEvent(EventType.DELETE_MESSAGE, contchatContextext, target) - }*/ } + MessageKind.TAG -> { - //app.callMessageEvent(EventType.TAG_MESSAGE, chatContext, chatMessage) } + MessageKind.ATTRIBUTES -> { - //app.callEvent(EventType.UPDATE_ATTRIBUTES) } + MessageKind.DISCOURSE -> { - //app.callMessageEvent(EventType.DISCOURSE, chatContext, chatMessage) } + MessageKind.DATA -> { - //app.callMessageEvent(EventType.DISCOURSE_DATA, chatContext, chatMessage) } + else -> { - assert(false) + error("Unexpected message kind: ${chatMessage.kind}") } } } @@ -210,26 +256,27 @@ class MainActivity : AppCompatActivity() { fun getChatViewModel(chatContext: ChatContext): ChatViewModel? { val key = gnunetChat.getUserPointerForContext(chatContext) var chatViewModel: ChatViewModel? = null - if (null != key) + if (key != null) { chatViewModel = chatViewModels.getOrPut(key) { ChatViewModel(chatContext) } + } return chatViewModel } fun getChatMenuViewModel(chatContext: ChatContext): ChatMenuViewModel? { val key = gnunetChat.getUserPointerForContext(chatContext) var chatMenuViewModel: ChatMenuViewModel? = null - if (null != key) + if (key != null) { chatMenuViewModel = chatMenuViewModels.getOrPut(key) { ChatMenuViewModel(chatContext) } + } return chatMenuViewModel } private fun loadChats() { val summaries = mutableListOf<ChatSummary>() - val contacts = mutableListOf<ChatContact>() gnunetChat.iterateContacts(handle) { contact -> @@ -238,7 +285,7 @@ class MainActivity : AppCompatActivity() { contacts.add(contact) gnunetChat.setUserPointerForContext(chatContext, uuid) - chats.put(uuid, chatContext) + chats[uuid] = chatContext contact.blocked = gnunetChat.isContactBlocked(contact) summaries.add( ChatSummary( @@ -256,7 +303,7 @@ class MainActivity : AppCompatActivity() { val uuid = gnunetChat.randomUUID() gnunetChat.setUserPointerForContext(chatContext, uuid) - chats.put(uuid, chatContext) + chats[uuid] = chatContext summaries.add( ChatSummary( chatContext = group.chatContext, @@ -270,68 +317,75 @@ class MainActivity : AppCompatActivity() { chatOverviewViewModel.setChats(summaries) } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun onCreateOptionsMenu(menu: android.view.Menu?): Boolean { menuInflater.inflate(R.menu.main_menu, menu) val current = currentAccount - menu?.findItem(R.id.menu_current_account)?.title = "Account: ${current?.name ?: "No active account!"}" + menu?.findItem(R.id.menu_current_account)?.title = + "Account: ${current?.name ?: "No active account!"}" return true } - override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + override fun onPrepareOptionsMenu(menu: android.view.Menu?): Boolean { val current = currentAccount - menu?.findItem(R.id.menu_current_account)?.title = "Account: ${current?.name ?: "No active account!"}" + menu?.findItem(R.id.menu_current_account)?.title = + "Account: ${current?.name ?: "No active account!"}" return super.onPrepareOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { + override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean { return when (item.itemId) { R.id.menu_current_account -> { - if (null != currentAccount) { + if (currentAccount != null) { val action = NavGraphDirections.actionGlobalAccountDetailsFragment() navController.navigate(action) } true } + R.id.menu_create_lobby -> { val action = NavGraphDirections.actionGlobalLobbyCreateFragment() navController.navigate(action) true } + R.id.menu_join_lobby -> { val action = NavGraphDirections.actionGlobalLobbyJoinFragment() navController.navigate(action) true } + R.id.menu_new_group -> { val action = NavGraphDirections.actionGlobalCreateGroupOrPlatformFragment(isGroup = true) navController.navigate(action) true } + R.id.menu_new_platform -> { val action = NavGraphDirections.actionGlobalCreateGroupOrPlatformFragment(isGroup = false) navController.navigate(action) true } + R.id.menu_contact_list -> { val action = NavGraphDirections.actionGlobalContactListFragment() navController.navigate(action) true } + R.id.menu_settings -> { val action = NavGraphDirections.actionGlobalSettingsFragment() navController.navigate(action) true } - R.id.menu_about -> { - true - } + + R.id.menu_about -> true else -> super.onOptionsItemSelected(item) } } override fun onSupportNavigateUp(): Boolean { - return NavigationUI.navigateUp(navController, appBarConfiguration) - || super.onSupportNavigateUp() + return NavigationUI.navigateUp(navController, appBarConfiguration) || + super.onSupportNavigateUp() } fun getGnunetChatInstance(): GnunetChat { @@ -344,5 +398,6 @@ class MainActivity : AppCompatActivity() { fun setCurrentAccount(account: ChatAccount) { currentAccount = account + invalidateOptionsMenu() } -} +} +\ No newline at end of file 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 @@ -30,6 +30,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.ProgressBar +import android.widget.TextView +import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController @@ -45,58 +49,119 @@ class AccountListFragment : Fragment() { private lateinit var recycler: RecyclerView private lateinit var createButton: Button + private lateinit var loadingIndicator: ProgressBar + private lateinit var statusText: TextView private lateinit var adapter: AccountAdapter + private val accounts = mutableListOf<ChatAccount>() + companion object { private const val TAG = "AccountListFragment" } override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle? ): View { - val list = mutableListOf<ChatAccount>() val view = inflater.inflate(R.layout.fragment_account_list, container, false) - val activity = activity as MainActivity + val activity = requireActivity() as MainActivity val gnunetChat = activity.getGnunetChatInstance() - val handle = activity.getChatHandle() recycler = view.findViewById(R.id.account_recycler) createButton = view.findViewById(R.id.btn_create_account) + loadingIndicator = view.findViewById(R.id.account_loading_indicator) + statusText = view.findViewById(R.id.account_status_text) adapter = AccountAdapter { selectedAccount -> + val handle = activity.getChatHandle() viewLifecycleOwner.lifecycleScope.launch { try { - if (null != activity.currentAccount) + if (activity.currentAccount != null) { runCatching { gnunetChat.disconnect(handle) } - .onFailure { /* loggen/toast, aber trotzdem weiter */ } + .onFailure { + Log.w(TAG, "disconnect before connect failed", it) + } + } gnunetChat.connect(handle, selectedAccount) + selectedAccount.key = gnunetChat.getProfileKey(handle) + activity.setCurrentAccount(selectedAccount) + + val action = + AccountListFragmentDirections.actionAccountListFragmentToAccountOverviewFragment( + account = selectedAccount + ) + findNavController().navigate(action) } catch (t: Throwable) { - // optional: Fehlermeldung / Toast + Log.e(TAG, "Connecting account failed", t) + showError(getString(R.string.account_connect_failed)) } } - - selectedAccount.key = gnunetChat.getProfileKey(handle) - val action = AccountListFragmentDirections.actionAccountListFragmentToAccountOverviewFragment(account = selectedAccount) - findNavController().navigate(action) } recycler.layoutManager = LinearLayoutManager(context) recycler.adapter = adapter createButton.setOnClickListener { - val action = AccountListFragmentDirections.actionAccountListFragmentToCreateAccountFragment() + val action = + AccountListFragmentDirections.actionAccountListFragmentToCreateAccountFragment() findNavController().navigate(action) } - Log.d(TAG, "iterateAccounts(): handle=${handle.pointer}") - (activity as MainActivity).getGnunetChatInstance().iterateAccounts((activity as MainActivity).getChatHandle()) { account -> - account.key = gnunetChat.getProfileKey(handle) - list.add(account) - } - adapter.submitList(list) + showLoading(getString(R.string.connecting_to_gnunet)) return view } -} + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val activity = requireActivity() as MainActivity + val gnunetChat = activity.getGnunetChatInstance() + + viewLifecycleOwner.lifecycleScope.launch { + try { + val handle = activity.awaitChatReady() + Log.d(TAG, "Chat ready, loading accounts for handle=${handle.pointer}") + + showLoading(getString(R.string.loading_accounts)) + createButton.isEnabled = true + recycler.isVisible = true + loadingIndicator.isGone = true + + gnunetChat.iterateAccounts(handle) { account -> + account.key = gnunetChat.getProfileKey(handle) + accounts.add(account) + adapter.submitList(accounts.toList()) + statusText.isGone = true + recycler.isVisible = true + } + + if (accounts.isEmpty()) { + statusText.text = getString(R.string.no_accounts_available) + statusText.isVisible = true + } + } catch (t: Throwable) { + Log.e(TAG, "Failed to initialize account list", t) + showError(getString(R.string.gnunet_connection_failed)) + } + } + } + + private fun showLoading(message: String) { + loadingIndicator.isVisible = true + statusText.isVisible = true + statusText.text = message + recycler.isGone = true + createButton.isEnabled = false + } + + private fun showError(message: String) { + loadingIndicator.isGone = true + statusText.isVisible = true + statusText.text = message + recycler.isGone = true + createButton.isEnabled = false + } +} +\ No newline at end of file diff --git a/GNUnetMessenger/app/src/main/res/layout/fragment_account_list.xml b/GNUnetMessenger/app/src/main/res/layout/fragment_account_list.xml @@ -33,17 +33,37 @@ android:id="@+id/btn_create_account" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/create_account" - android:layout_marginBottom="16dp" /> + android:layout_marginBottom="16dp" + android:enabled="false" + android:text="@string/create_account" /> + + <ProgressBar + android:id="@+id/account_loading_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_marginTop="24dp" + android:layout_marginBottom="12dp" + android:contentDescription="@string/loading_accounts" /> + + <TextView + android:id="@+id/account_status_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:paddingBottom="16dp" + android:text="@string/connecting_to_gnunet" + android:textAppearance="?attr/textAppearanceBodyMedium" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/account_recycler" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" - tools:listitem="@layout/item_account" android:contentDescription="@string/account_list_description" + android:fadeScrollbars="false" android:scrollbars="vertical" - android:fadeScrollbars="false"/> + android:visibility="gone" + tools:listitem="@layout/item_account" /> </LinearLayout> \ No newline at end of file diff --git a/GNUnetMessenger/app/src/main/res/values/strings.xml b/GNUnetMessenger/app/src/main/res/values/strings.xml @@ -9,12 +9,12 @@ <string name="about">About</string> <string name="create_lobby">Create Lobby</string> <string name="join_lobby">Join Lobby</string> - <string name="lobby_warning">Please notice that everyone with access to the lobby\'s code can enter it</string> - <string name="attribute_list_description">Liste der Attribute</string> - <string name="account_list_description">Account List</string> - <string name="chat_list_description">Chat List</string> + <string name="lobby_warning">Please note that anyone with access to the lobby code can enter it</string> + <string name="attribute_list_description">List of attributes</string> + <string name="account_list_description">Account list</string> + <string name="chat_list_description">Chat list</string> <string name="join">Join Lobby</string> - <string name="chat_message_description">Nachrichtenliste</string> + <string name="chat_message_description">Message list</string> <string name="share_identity">Share Identity</string> <string name="block_contact">Block Contact</string> <string name="leave_chat">Leave Chat</string> @@ -27,6 +27,13 @@ <string name="global_options">--- Global Options ---</string> <string name="account_overview">Chat Overview</string> <string name="placeholder_label_chat">Chat</string> + + <string name="connecting_to_gnunet">Connecting to GNUnet…</string> + <string name="loading_accounts">Loading accounts…</string> + <string name="no_accounts_available">No accounts available yet.</string> + <string name="gnunet_connection_failed">Could not connect to GNUnet.</string> + <string name="account_connect_failed">Could not activate the selected account.</string> + <string-array name="lobby_lifetimes"> <item>Off</item> <item>4 weeks</item>