Add distributed user profiles and username directory

This commit is contained in:
dom4k
2026-03-17 02:25:07 +00:00
parent 1cfdb42e04
commit b4df94200e
19 changed files with 749 additions and 118 deletions

View File

@@ -25,12 +25,12 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import pro.nnnteam.nnnet.data.ChatSummary
import pro.nnnteam.nnnet.data.MeshDatabase
import pro.nnnteam.nnnet.data.MeshRepository
import pro.nnnteam.nnnet.mesh.MeshForegroundService
import pro.nnnteam.nnnet.mesh.MeshServiceContract
import pro.nnnteam.nnnet.ui.ChatListAdapter
import pro.nnnteam.nnnet.ui.ChatListItem
import pro.nnnteam.nnnet.update.UpdateInfo
import pro.nnnteam.nnnet.update.UpdateManager
import java.util.Locale
@@ -44,7 +44,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var chatListView: ListView
private val peers = linkedSetOf<String>()
private val chatSummaries = mutableListOf<ChatSummary>()
private val chatItems = mutableListOf<ChatListItem>()
private lateinit var chatAdapter: ChatListAdapter
private var receiverRegistered = false
@@ -63,7 +63,8 @@ class MainActivity : AppCompatActivity() {
when (eventType) {
MeshServiceContract.EVENT_STATUS -> updateMeshStatus(value)
MeshServiceContract.EVENT_PEER -> addPeer(value)
MeshServiceContract.EVENT_MESSAGES_CHANGED -> refreshChats()
MeshServiceContract.EVENT_MESSAGES_CHANGED,
MeshServiceContract.EVENT_PROFILES_CHANGED -> refreshChats()
}
}
}
@@ -71,8 +72,7 @@ class MainActivity : AppCompatActivity() {
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
val allGranted = result.values.all { it }
if (allGranted) {
if (result.values.all { it }) {
ensureBluetoothEnabledAndContinue()
} else {
Toast.makeText(this, R.string.permissions_denied, Toast.LENGTH_SHORT).show()
@@ -94,7 +94,7 @@ class MainActivity : AppCompatActivity() {
setContentView(R.layout.activity_main)
val database = MeshDatabase.getInstance(applicationContext)
repository = MeshRepository(database.messageDao(), database.outboundQueueDao())
repository = MeshRepository(database.messageDao(), database.outboundQueueDao(), database.profileDao())
deviceCountText = findViewById(R.id.deviceCountText)
statusBadge = findViewById(R.id.statusBadge)
@@ -102,10 +102,10 @@ class MainActivity : AppCompatActivity() {
emptyStateText = findViewById(R.id.emptyStateText)
chatListView = findViewById(R.id.chatListView)
chatAdapter = ChatListAdapter(this, chatSummaries)
chatAdapter = ChatListAdapter(this, chatItems)
chatListView.adapter = chatAdapter
chatListView.setOnItemClickListener { _, _, position, _ ->
openChat(chatSummaries[position].peerId)
openChat(chatItems[position].peerId)
}
statusBadge.setOnClickListener { toggleMesh() }
@@ -164,7 +164,7 @@ class MainActivity : AppCompatActivity() {
private fun showNewChatDialog() {
val input = EditText(this).apply {
hint = getString(R.string.hint_peer_id)
hint = getString(R.string.hint_chat_target)
setSingleLine()
setPadding(48, 32, 48, 32)
}
@@ -172,18 +172,44 @@ class MainActivity : AppCompatActivity() {
AlertDialog.Builder(this)
.setTitle(R.string.new_chat_title)
.setView(input)
.setPositiveButton(R.string.open_chat) { _, _ ->
val peerId = input.text.toString().trim()
if (peerId.isNotEmpty()) {
openChat(peerId)
} else {
Toast.makeText(this, R.string.peer_id_required, Toast.LENGTH_SHORT).show()
.setPositiveButton(R.string.open_chat, null)
.setNegativeButton(R.string.cancel, null)
.create()
.also { dialog ->
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val value = input.text.toString().trim()
if (value.isEmpty()) {
Toast.makeText(this, R.string.peer_id_required, Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
lifecycleScope.launch {
val resolvedPeerId = resolvePeerId(value)
if (resolvedPeerId != null) {
dialog.dismiss()
openChat(resolvedPeerId)
} else {
Toast.makeText(
this@MainActivity,
R.string.profile_not_found_locally,
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private suspend fun resolvePeerId(value: String): String? {
val normalized = value.trim().removePrefix("@").lowercase(Locale.getDefault())
return when {
':' in value -> value.trim()
else -> repository.profileByUsername(normalized)?.peerId?.takeIf { it.isNotBlank() }
}
}
private fun openChat(peerId: String) {
startActivity(Intent(this, ChatActivity::class.java).putExtra(ChatActivity.EXTRA_PEER_ID, peerId))
}
@@ -238,10 +264,22 @@ class MainActivity : AppCompatActivity() {
private fun refreshChats() {
lifecycleScope.launch {
val chats = repository.chatSummaries()
chatSummaries.clear()
chatSummaries.addAll(chats)
val mappedItems = chats.map { chat ->
val profile = repository.profileByPeerId(chat.peerId)
val title = profile?.displayName() ?: chat.peerId
val subtitlePrefix = profile?.let { "@${it.username} · " }.orEmpty()
ChatListItem(
peerId = chat.peerId,
title = title,
subtitle = subtitlePrefix + chat.lastBody,
lastStatus = chat.lastStatus,
lastTimestamp = chat.lastTimestamp
)
}
chatItems.clear()
chatItems.addAll(mappedItems)
chatAdapter.notifyDataSetChanged()
emptyStateText.visibility = if (chats.isEmpty()) View.VISIBLE else View.GONE
emptyStateText.visibility = if (mappedItems.isEmpty()) View.VISIBLE else View.GONE
}
}
@@ -253,14 +291,14 @@ class MainActivity : AppCompatActivity() {
private fun updateMeshStatus(status: String) {
val normalized = status.lowercase(Locale.getDefault())
if (normalized.contains("останов")) {
if (normalized.contains("останов") || normalized.contains("оффлайн")) {
meshEnabled = false
peers.clear()
} else if (
normalized.contains("актив") ||
normalized.contains("запуска") ||
normalized.contains("в сети") ||
normalized.contains("присутствие") ||
normalized.contains("устройство") ||
normalized.contains("сообщение")
) {
meshEnabled = true