commit 29dc79f8f0d0135646c43cbb8e010136c17fdd2f
parent 618db56d5b4ed6b1915f23d543e9f629cbc6291a
Author: t3sserakt <t3sserakt@posteo.de>
Date: Tue, 17 Mar 2026 19:31:50 +0100
UI waiting for startChat.
Diffstat:
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>