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

@@ -1,33 +1,78 @@
package pro.nnnteam.nnnet
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.EditText
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.material.button.MaterialButton
import com.google.android.material.switchmaterial.SwitchMaterial
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import pro.nnnteam.nnnet.data.MeshDatabase
import pro.nnnteam.nnnet.data.MeshRepository
import pro.nnnteam.nnnet.data.ProfileEntity
import pro.nnnteam.nnnet.mesh.MeshServiceContract
import pro.nnnteam.nnnet.update.UpdateInfo
import pro.nnnteam.nnnet.update.UpdateManager
import android.content.BroadcastReceiver
import android.content.Intent
import android.content.IntentFilter
class SettingsActivity : AppCompatActivity() {
private lateinit var repository: MeshRepository
private lateinit var firstNameInput: EditText
private lateinit var lastNameInput: EditText
private lateinit var usernameInput: EditText
private lateinit var descriptionInput: EditText
private lateinit var searchInput: EditText
private lateinit var profileResultCard: android.view.View
private lateinit var resultNameText: TextView
private lateinit var resultUsernameText: TextView
private lateinit var resultDescriptionText: TextView
private lateinit var resultPeerIdText: TextView
private var receiverRegistered = false
private val prefs by lazy {
getSharedPreferences(UpdateManager.PREFS_NAME, MODE_PRIVATE)
}
private val meshEventReceiver = object : BroadcastReceiver() {
override fun onReceive(context: android.content.Context?, intent: Intent?) {
if (intent?.action != MeshServiceContract.ACTION_EVENT) return
val eventType = intent.getStringExtra(MeshServiceContract.EXTRA_EVENT_TYPE) ?: return
if (eventType == MeshServiceContract.EVENT_PROFILES_CHANGED) {
val query = searchInput.text.toString().trim()
if (query.isNotEmpty()) {
lookupProfile(query)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
val database = MeshDatabase.getInstance(applicationContext)
repository = MeshRepository(database.messageDao(), database.outboundQueueDao(), database.profileDao())
findViewById<ImageButton>(R.id.backButton).setOnClickListener { finish() }
firstNameInput = findViewById(R.id.firstNameInput)
lastNameInput = findViewById(R.id.lastNameInput)
usernameInput = findViewById(R.id.usernameInput)
descriptionInput = findViewById(R.id.descriptionInput)
searchInput = findViewById(R.id.searchInput)
profileResultCard = findViewById(R.id.profileResultCard)
resultNameText = findViewById(R.id.resultNameText)
resultUsernameText = findViewById(R.id.resultUsernameText)
resultDescriptionText = findViewById(R.id.resultDescriptionText)
resultPeerIdText = findViewById(R.id.resultPeerIdText)
val autoUpdateSwitch = findViewById<SwitchMaterial>(R.id.autoUpdateSwitch)
val versionText = findViewById<TextView>(R.id.versionText)
autoUpdateSwitch.isChecked = prefs.getBoolean(UpdateManager.KEY_AUTO_UPDATE, false)
@@ -41,14 +86,108 @@ class SettingsActivity : AppCompatActivity() {
currentVersionCode()
)
findViewById<android.view.View>(R.id.checkUpdatesButton).setOnClickListener {
checkForUpdates()
findViewById<MaterialButton>(R.id.saveProfileButton).setOnClickListener { saveProfile() }
findViewById<MaterialButton>(R.id.searchButton).setOnClickListener {
val query = searchInput.text.toString().trim()
if (query.isEmpty()) {
Toast.makeText(this, R.string.enter_username_to_search, Toast.LENGTH_SHORT).show()
} else {
lookupProfile(query)
}
}
findViewById<MaterialButton>(R.id.checkUpdatesButton).setOnClickListener { checkForUpdates() }
loadLocalProfile()
}
override fun onStart() {
super.onStart()
registerMeshReceiver()
}
override fun onStop() {
if (receiverRegistered) {
unregisterReceiver(meshEventReceiver)
receiverRegistered = false
}
super.onStop()
}
private fun registerMeshReceiver() {
if (receiverRegistered) return
val filter = IntentFilter(MeshServiceContract.ACTION_EVENT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(meshEventReceiver, filter, RECEIVER_NOT_EXPORTED)
} else {
@Suppress("DEPRECATION")
registerReceiver(meshEventReceiver, filter)
}
receiverRegistered = true
}
private fun loadLocalProfile() {
lifecycleScope.launch {
val localProfile = repository.localProfile()
if (localProfile != null) {
firstNameInput.setText(localProfile.firstName)
lastNameInput.setText(localProfile.lastName)
usernameInput.setText(localProfile.username)
descriptionInput.setText(localProfile.description)
}
}
}
private fun saveProfile() {
val firstName = firstNameInput.text.toString().trim()
val lastName = lastNameInput.text.toString().trim()
val username = usernameInput.text.toString().trim().removePrefix("@")
val description = descriptionInput.text.toString().trim()
if (username.isBlank()) {
Toast.makeText(this, R.string.username_required, Toast.LENGTH_SHORT).show()
return
}
lifecycleScope.launch {
repository.saveLocalProfile(
firstName = firstName,
lastName = lastName,
username = username,
description = description
)
Toast.makeText(this@SettingsActivity, R.string.profile_saved, Toast.LENGTH_SHORT).show()
}
}
private fun lookupProfile(username: String) {
lifecycleScope.launch {
val profile = repository.profileByUsername(username.removePrefix("@"))
renderSearchResult(profile)
if (profile == null) {
Toast.makeText(this@SettingsActivity, R.string.profile_not_found_locally, Toast.LENGTH_SHORT).show()
}
}
}
private fun renderSearchResult(profile: ProfileEntity?) {
if (profile == null) {
profileResultCard.visibility = android.view.View.GONE
return
}
profileResultCard.visibility = android.view.View.VISIBLE
resultNameText.text = profile.displayName()
resultUsernameText.text = "@${profile.username}"
resultDescriptionText.text = profile.description.ifBlank { getString(R.string.no_profile_description) }
resultPeerIdText.text = if (profile.peerId.isBlank()) {
getString(R.string.peer_id_unknown)
} else {
getString(R.string.peer_id_value, profile.peerId)
}
}
private fun checkForUpdates() {
lifecycleScope.launch {
val updateInfo = withContext(Dispatchers.IO) { UpdateManager.fetchUpdateInfo() }
val updateInfo = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { UpdateManager.fetchUpdateInfo() }
if (updateInfo == null) {
Toast.makeText(this@SettingsActivity, R.string.update_check_failed, Toast.LENGTH_SHORT).show()
return@launch
@@ -63,10 +202,10 @@ class SettingsActivity : AppCompatActivity() {
private fun showUpdateDialog(updateInfo: UpdateInfo) {
lifecycleScope.launch {
val releaseNotes = withContext(Dispatchers.IO) {
val releaseNotes = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
UpdateManager.fetchReleaseNotes(updateInfo.releaseNotesPath)
}
AlertDialog.Builder(this@SettingsActivity)
androidx.appcompat.app.AlertDialog.Builder(this@SettingsActivity)
.setTitle(updateInfo.releaseNotesTitle)
.setMessage(
buildString {
@@ -79,7 +218,7 @@ class SettingsActivity : AppCompatActivity() {
)
.setPositiveButton(R.string.download_update) { _, _ ->
val url = UpdateManager.buildDownloadUrl(updateInfo.apkPath)
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
startActivity(Intent(Intent.ACTION_VIEW, android.net.Uri.parse(url)))
}
.setNegativeButton(R.string.later, null)
.show()