chore: refactor the internal structure (#33)
Reviewed-on: #33
This commit was merged in pull request #33.
This commit is contained in:
@@ -25,8 +25,8 @@ kotlin {
|
||||
implementation(libs.jetbrains.lifecycle.viewmodelNavigation3)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation("su.reya:nostr-sdk-kmp:0.3.2")
|
||||
implementation("io.coil-kt.coil3:coil-compose:3.4.0")
|
||||
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
|
||||
implementation("io.coil-kt.coil3:coil-compose:3.5.0")
|
||||
implementation("io.coil-kt.coil3:coil-network-okhttp:3.5.0")
|
||||
implementation("io.github.kalinjul.easyqrscan:scanner:0.7.0")
|
||||
implementation("io.github.alexzhirkevich:qrose:1.1.2")
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import su.reya.coop.repository.ErrorRepository
|
||||
import su.reya.coop.screens.ChatScreen
|
||||
import su.reya.coop.screens.ContactListScreen
|
||||
import su.reya.coop.screens.HomeScreen
|
||||
@@ -47,30 +48,20 @@ import su.reya.coop.screens.RelayScreen
|
||||
import su.reya.coop.screens.RequestListScreen
|
||||
import su.reya.coop.screens.ScanScreen
|
||||
import su.reya.coop.screens.UpdateProfileScreen
|
||||
import su.reya.coop.viewmodels.AccountViewModel
|
||||
import su.reya.coop.viewmodels.AppViewModel
|
||||
import su.reya.coop.viewmodels.ChatViewModel
|
||||
import su.reya.coop.viewmodels.ProfileViewModel
|
||||
import su.reya.coop.viewmodels.RelayViewModel
|
||||
import su.reya.coop.viewmodel.AuthViewModel
|
||||
import su.reya.coop.viewmodel.ChatViewModel
|
||||
import su.reya.coop.viewmodel.NostrViewModel
|
||||
|
||||
val LocalAppViewModel = staticCompositionLocalOf<AppViewModel> {
|
||||
error("No AppViewModel provided")
|
||||
}
|
||||
|
||||
val LocalAccountViewModel = staticCompositionLocalOf<AccountViewModel> {
|
||||
error("No AccountViewModel provided")
|
||||
val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
|
||||
error("No NostrViewModel provided")
|
||||
}
|
||||
|
||||
val LocalChatViewModel = staticCompositionLocalOf<ChatViewModel> {
|
||||
error("No ChatViewModel provided")
|
||||
}
|
||||
|
||||
val LocalProfileViewModel = staticCompositionLocalOf<ProfileViewModel> {
|
||||
error("No ProfileViewModel provided")
|
||||
}
|
||||
|
||||
val LocalRelayViewModel = staticCompositionLocalOf<RelayViewModel> {
|
||||
error("No RelayViewModel provided")
|
||||
val LocalAuthViewModel = staticCompositionLocalOf<AuthViewModel> {
|
||||
error("No AuthViewModel provided")
|
||||
}
|
||||
|
||||
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
|
||||
@@ -88,11 +79,9 @@ val LocalScanResult = staticCompositionLocalOf<QrScanResult> {
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun App(
|
||||
appViewModel: AppViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nostrViewModel: NostrViewModel,
|
||||
chatViewModel: ChatViewModel,
|
||||
profileViewModel: ProfileViewModel,
|
||||
relayViewModel: RelayViewModel,
|
||||
authViewModel: AuthViewModel,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as? ComponentActivity
|
||||
@@ -101,7 +90,8 @@ fun App(
|
||||
val qrScanResult = remember { QrScanResult() }
|
||||
|
||||
// Get the signer required state
|
||||
val signerRequired by accountViewModel.signerRequired.collectAsStateWithLifecycle()
|
||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
||||
val signerRequired = authState.signerRequired
|
||||
|
||||
// Snackbar
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
@@ -128,7 +118,7 @@ fun App(
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
appViewModel.errorEvents.collect { message ->
|
||||
ErrorRepository.errors.collect { message ->
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
@@ -171,15 +161,14 @@ fun App(
|
||||
motionScheme = MotionScheme.expressive(),
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalAppViewModel provides appViewModel,
|
||||
LocalAccountViewModel provides accountViewModel,
|
||||
LocalNostrViewModel provides nostrViewModel,
|
||||
LocalChatViewModel provides chatViewModel,
|
||||
LocalProfileViewModel provides profileViewModel,
|
||||
LocalRelayViewModel provides relayViewModel,
|
||||
LocalAuthViewModel provides authViewModel,
|
||||
LocalSnackbarHostState provides snackbarHostState,
|
||||
LocalNavigator provides navigator,
|
||||
LocalScanResult provides qrScanResult,
|
||||
) {
|
||||
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = {
|
||||
|
||||
@@ -10,23 +10,13 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import su.reya.coop.coop.storage.SecretStore
|
||||
import su.reya.coop.nostr.NostrManager
|
||||
import su.reya.coop.viewmodels.AccountViewModel
|
||||
import su.reya.coop.viewmodels.AppViewModel
|
||||
import su.reya.coop.viewmodels.ChatViewModel
|
||||
import su.reya.coop.viewmodels.ProfileViewModel
|
||||
import su.reya.coop.viewmodels.RelayViewModel
|
||||
import su.reya.coop.viewmodel.AuthViewModel
|
||||
import su.reya.coop.viewmodel.ChatViewModel
|
||||
import su.reya.coop.viewmodel.NostrViewModel
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@@ -36,34 +26,30 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private val factory by lazy {
|
||||
object : ViewModelProvider.Factory {
|
||||
private val secretStore = SecretStore(this@MainActivity)
|
||||
private val androidSigner =
|
||||
AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
|
||||
private val appVM = AppViewModel(NostrManager.instance, secretStore)
|
||||
private val profileVM = ProfileViewModel(NostrManager.instance, appVM)
|
||||
private val chatVM = ChatViewModel(NostrManager.instance, appVM)
|
||||
private val accountVM =
|
||||
AccountViewModel(NostrManager.instance, secretStore, appVM, androidSigner)
|
||||
private val relayVM = RelayViewModel(NostrManager.instance, appVM, chatVM, profileVM)
|
||||
private val secretStore = SecretStore(this@MainActivity)
|
||||
private val nostrViewModel =
|
||||
NostrViewModel(NostrManager.instance)
|
||||
private val chatViewModel =
|
||||
ChatViewModel(NostrManager.instance)
|
||||
private val authViewModel =
|
||||
AuthViewModel(NostrManager.instance, secretStore, androidSigner)
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return when {
|
||||
modelClass.isAssignableFrom(AppViewModel::class.java) -> appVM
|
||||
modelClass.isAssignableFrom(AccountViewModel::class.java) -> accountVM
|
||||
modelClass.isAssignableFrom(ChatViewModel::class.java) -> chatVM
|
||||
modelClass.isAssignableFrom(ProfileViewModel::class.java) -> profileVM
|
||||
modelClass.isAssignableFrom(RelayViewModel::class.java) -> relayVM
|
||||
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrViewModel
|
||||
modelClass.isAssignableFrom(ChatViewModel::class.java) -> chatViewModel
|
||||
modelClass.isAssignableFrom(AuthViewModel::class.java) -> authViewModel
|
||||
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
||||
} as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val appViewModel: AppViewModel by viewModels { factory }
|
||||
private val accountViewModel: AccountViewModel by viewModels { factory }
|
||||
private val nostrViewModel: NostrViewModel by viewModels { factory }
|
||||
private val chatViewModel: ChatViewModel by viewModels { factory }
|
||||
private val profileViewModel: ProfileViewModel by viewModels { factory }
|
||||
private val relayViewModel: RelayViewModel by viewModels { factory }
|
||||
private val authViewModel: AuthViewModel by viewModels { factory }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
@@ -102,36 +88,19 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
// Keep the splash screen visible until the signer check is complete
|
||||
splashScreen.setKeepOnScreenCondition {
|
||||
accountViewModel.signerRequired.value == null
|
||||
authViewModel.state.value.signerRequired == null
|
||||
}
|
||||
|
||||
// Bind the lifecycle of the ViewModels
|
||||
val lifecycle = ProcessLifecycleOwner.get().lifecycle
|
||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
appViewModel.viewModelScope.launch {
|
||||
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
coroutineScope {
|
||||
launch { chatViewModel.bindObservers() }
|
||||
launch { profileViewModel.bindObservers() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
relayViewModel.observeSignerAndCheckRelays()
|
||||
|
||||
setContent {
|
||||
App(
|
||||
appViewModel = appViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nostrViewModel = nostrViewModel,
|
||||
chatViewModel = chatViewModel,
|
||||
profileViewModel = profileViewModel,
|
||||
relayViewModel = relayViewModel,
|
||||
authViewModel = authViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
|
||||
@@ -21,7 +21,6 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import su.reya.coop.nostr.NostrManager
|
||||
import su.reya.coop.nostr.roomId
|
||||
import java.io.File
|
||||
|
||||
private const val GROUP_KEY_MESSAGES = "su.reya.coop.MESSAGES"
|
||||
|
||||
@@ -70,27 +70,24 @@ import coop.composeapp.generated.resources.ic_cancel
|
||||
import coop.composeapp.generated.resources.ic_check_circle
|
||||
import coop.composeapp.generated.resources.ic_send
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import rust.nostr.sdk.Timestamp
|
||||
import rust.nostr.sdk.UnsignedEvent
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalChatViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalProfileViewModel
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.Room
|
||||
import su.reya.coop.Screen
|
||||
import su.reya.coop.nostr.Room
|
||||
import su.reya.coop.nostr.formatAsGroupHeader
|
||||
import su.reya.coop.nostr.humanReadable
|
||||
import su.reya.coop.nostr.roomId
|
||||
import su.reya.coop.formatAsGroupHeader
|
||||
import su.reya.coop.humanReadable
|
||||
import su.reya.coop.rememberUiState
|
||||
import su.reya.coop.roomId
|
||||
import su.reya.coop.shared.Avatar
|
||||
import su.reya.coop.shared.getExpressiveFontFamily
|
||||
import su.reya.coop.shared.nameFlow
|
||||
import su.reya.coop.shared.pictureFlow
|
||||
import su.reya.coop.short
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@@ -99,11 +96,14 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
||||
val context = LocalContext.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val navigator = LocalNavigator.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val chatViewModel = LocalChatViewModel.current
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
// Get current user
|
||||
val currentUser by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
|
||||
// Get chat room by ID
|
||||
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
|
||||
val room by remember(id) { derivedStateOf { chatRooms.firstOrNull { it.id == id } } }
|
||||
@@ -123,15 +123,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val displayName by remember(room) {
|
||||
room?.nameFlow(profileViewModel) ?: flowOf("Loading...")
|
||||
}.collectAsStateWithLifecycle("Loading...")
|
||||
|
||||
val picture by remember(room) {
|
||||
room?.pictureFlow(profileViewModel) ?: flowOf(null)
|
||||
}.collectAsStateWithLifecycle(null)
|
||||
|
||||
val roomState by (room as Room).rememberUiState(nostrViewModel, currentUser?.publicKey)
|
||||
var text by remember { mutableStateOf("") }
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
var newOtherMessages by remember { mutableIntStateOf(0) }
|
||||
@@ -215,14 +207,14 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
||||
LoadingIndicator(modifier = Modifier.size(32.dp))
|
||||
} else {
|
||||
Avatar(
|
||||
picture = picture,
|
||||
description = displayName,
|
||||
picture = roomState.picture,
|
||||
description = roomState.name,
|
||||
size = 32.dp,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
text = displayName,
|
||||
text = roomState.name,
|
||||
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||
)
|
||||
}
|
||||
@@ -282,7 +274,10 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
||||
items = messagesInGroup,
|
||||
key = { it.id()?.toBech32() ?: it.hashCode() }
|
||||
) {
|
||||
ChatMessage(it)
|
||||
ChatMessage(
|
||||
rumor = it,
|
||||
isMine = currentUser?.publicKey == it.author()
|
||||
)
|
||||
}
|
||||
item {
|
||||
DateSeparator(dateHeader)
|
||||
@@ -377,28 +372,24 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
||||
fun ScreenerCard(room: Room) {
|
||||
val pubkey = room.members.firstOrNull() ?: return
|
||||
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var isContact by remember { mutableStateOf(false) }
|
||||
var mutualContacts by remember { mutableStateOf<Set<PublicKey>>(emptySet()) }
|
||||
var lastActivity by remember { mutableStateOf<Timestamp?>(null) }
|
||||
|
||||
val metadataFlow = remember(pubkey) { profileViewModel.getMetadata(pubkey) }
|
||||
val metadata by metadataFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val profile = metadata?.asRecord()
|
||||
val displayName = profile?.displayName ?: profile?.name ?: "No name"
|
||||
val picture = profile?.picture
|
||||
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
|
||||
val profile by profileFlow.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(pubkey) {
|
||||
scope.launch {
|
||||
// Check contact
|
||||
profileViewModel.verifyContact(pubkey).let { isContact = it }
|
||||
nostrViewModel.verifyContact(pubkey).let { isContact = it }
|
||||
// Get mutual contacts
|
||||
profileViewModel.mutualContacts(pubkey).let { mutualContacts = it }
|
||||
nostrViewModel.mutualContacts(pubkey).let { mutualContacts = it }
|
||||
// Get the last activity
|
||||
profileViewModel.verifyActivity(pubkey)?.let { lastActivity = it }
|
||||
nostrViewModel.verifyActivity(pubkey)?.let { lastActivity = it }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,7 +406,7 @@ fun ScreenerCard(room: Room) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Avatar(
|
||||
picture = picture,
|
||||
picture = profile?.picture,
|
||||
description = "Profile picture",
|
||||
modifier = Modifier.size(120.dp),
|
||||
shape = MaterialShapes.Cookie12Sided.toShape(),
|
||||
@@ -425,7 +416,7 @@ fun ScreenerCard(room: Room) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = displayName,
|
||||
text = profile?.name ?: "No name",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.titleLargeEmphasized.copy(
|
||||
fontFamily = getExpressiveFontFamily()
|
||||
@@ -514,13 +505,9 @@ fun DateSeparator(date: String) {
|
||||
|
||||
@Composable
|
||||
fun ChatMessage(
|
||||
rumor: UnsignedEvent
|
||||
rumor: UnsignedEvent,
|
||||
isMine: Boolean = false,
|
||||
) {
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val chatViewModel = LocalChatViewModel.current
|
||||
val currentUser = accountViewModel.nostr.signer.currentUser
|
||||
val isMine = rumor.author() == currentUser
|
||||
|
||||
val bubbleShape = if (isMine) {
|
||||
RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp, bottomStart = 20.dp, bottomEnd = 4.dp)
|
||||
} else {
|
||||
@@ -546,17 +533,7 @@ fun ChatMessage(
|
||||
color = containerColor,
|
||||
contentColor = contentColor,
|
||||
shape = bubbleShape,
|
||||
modifier = Modifier
|
||||
.widthIn(max = 280.dp)
|
||||
.clickable(
|
||||
onClick = {
|
||||
val id = rumor.id()
|
||||
if (id != null) {
|
||||
val sent = chatViewModel.isMessageSent(id)
|
||||
println("Sent: $sent")
|
||||
}
|
||||
}
|
||||
)
|
||||
modifier = Modifier.widthIn(max = 280.dp)
|
||||
) {
|
||||
Text(
|
||||
text = rumor.content(),
|
||||
|
||||
@@ -55,7 +55,7 @@ import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.Nip05Address
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalProfileViewModel
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.Screen
|
||||
import su.reya.coop.shared.Avatar
|
||||
@@ -66,9 +66,9 @@ import su.reya.coop.short
|
||||
fun ContactListScreen() {
|
||||
val navigator = LocalNavigator.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
|
||||
val contactList by profileViewModel.contactList.collectAsStateWithLifecycle()
|
||||
val contactList by nostrViewModel.contactList.collectAsStateWithLifecycle()
|
||||
var openAddContactDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
@@ -180,7 +180,7 @@ fun ContactListScreen() {
|
||||
@Composable
|
||||
fun AddContactDialog(onDismissRequest: () -> Unit) {
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var contact by remember { mutableStateOf("") }
|
||||
var isError by remember { mutableStateOf(false) }
|
||||
@@ -225,7 +225,7 @@ fun AddContactDialog(onDismissRequest: () -> Unit) {
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
val success = profileViewModel.addContact(contact)
|
||||
val success = nostrViewModel.addContact(contact)
|
||||
if (success) onDismissRequest()
|
||||
}
|
||||
}) {
|
||||
@@ -283,32 +283,28 @@ fun ContactListItem(
|
||||
total: Int = 0,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val metadataFlow = remember(pubkey) { profileViewModel.getMetadata(pubkey) }
|
||||
val metadata by metadataFlow.collectAsState(initial = null)
|
||||
|
||||
val profile = metadata?.asRecord()
|
||||
val displayName = profile?.name ?: profile?.displayName ?: pubkey.short()
|
||||
val picture = profile?.picture
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
|
||||
val profile by profileFlow.collectAsState(initial = null)
|
||||
|
||||
SegmentedListItem(
|
||||
onClick = onClick,
|
||||
onLongClick = { profileViewModel.removeContact(pubkey) },
|
||||
onLongClick = { nostrViewModel.removeContact(pubkey) },
|
||||
shapes = ListItemDefaults.segmentedShapes(
|
||||
index = index,
|
||||
count = total
|
||||
),
|
||||
leadingContent = {
|
||||
Avatar(
|
||||
picture = picture,
|
||||
description = displayName,
|
||||
picture = profile?.picture,
|
||||
description = profile?.name,
|
||||
size = 36.dp
|
||||
)
|
||||
},
|
||||
supportingContent = { Text(text = pubkey.short()) },
|
||||
content = {
|
||||
Text(
|
||||
text = displayName,
|
||||
text = profile?.name ?: "No name",
|
||||
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.material3.toShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -87,27 +86,22 @@ import coop.composeapp.generated.resources.ic_new_chat
|
||||
import coop.composeapp.generated.resources.ic_qr
|
||||
import coop.composeapp.generated.resources.ic_request
|
||||
import coop.composeapp.generated.resources.ic_scanner
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalAppViewModel
|
||||
import su.reya.coop.LocalAuthViewModel
|
||||
import su.reya.coop.LocalChatViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalProfileViewModel
|
||||
import su.reya.coop.LocalRelayViewModel
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalScanResult
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.Room
|
||||
import su.reya.coop.RoomKind
|
||||
import su.reya.coop.Screen
|
||||
import su.reya.coop.nostr.Room
|
||||
import su.reya.coop.nostr.RoomKind
|
||||
import su.reya.coop.nostr.ago
|
||||
import su.reya.coop.ago
|
||||
import su.reya.coop.rememberUiState
|
||||
import su.reya.coop.shared.Avatar
|
||||
import su.reya.coop.shared.getExpressiveFontFamily
|
||||
import su.reya.coop.shared.nameFlow
|
||||
import su.reya.coop.shared.pictureFlow
|
||||
import su.reya.coop.short
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -117,27 +111,24 @@ fun HomeScreen() {
|
||||
val qrScanResult = LocalScanResult.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val clipboardManager = LocalClipboard.current
|
||||
val appViewModel = LocalAppViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val chatViewModel = LocalChatViewModel.current
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val relayViewModel = LocalRelayViewModel.current
|
||||
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val sheetState = rememberModalBottomSheetState(true)
|
||||
val listState = rememberLazyListState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
val currentUser = accountViewModel.nostr.signer.currentUser ?: return
|
||||
val currentUserProfile = profileViewModel.getMetadata(currentUser)
|
||||
|
||||
val userProfile by currentUserProfile.collectAsStateWithLifecycle()
|
||||
val userProfile by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
|
||||
val isRelayListEmpty by relayViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
|
||||
|
||||
val isRelayListEmpty by nostrViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
|
||||
val isSyncing by chatViewModel.isSyncing.collectAsStateWithLifecycle()
|
||||
val isPartialProcessedGiftWrap by chatViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
|
||||
val isBannerDismissed by appViewModel.isNotificationBannerDismissed.collectAsState()
|
||||
|
||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
||||
val isBannerDismissed = authState.isNotificationBannerDismissed
|
||||
|
||||
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
@@ -165,7 +156,7 @@ fun HomeScreen() {
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
chatViewModel.getChatRooms()
|
||||
chatViewModel.refreshChatRooms()
|
||||
}
|
||||
|
||||
LaunchedEffect(qrScanResult.content) {
|
||||
@@ -218,8 +209,8 @@ fun HomeScreen() {
|
||||
// User
|
||||
IconButton(onClick = { showBottomSheet = true }) {
|
||||
Avatar(
|
||||
picture = userProfile?.asRecord()?.picture,
|
||||
description = userProfile?.asRecord()?.displayName,
|
||||
picture = userProfile?.picture,
|
||||
description = userProfile?.name ?: "No name",
|
||||
size = 32.dp,
|
||||
)
|
||||
}
|
||||
@@ -289,7 +280,7 @@ fun HomeScreen() {
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
TextButton(
|
||||
onClick = { appViewModel.dismissNotificationBanner() },
|
||||
onClick = { authViewModel.dismissNotificationBanner() },
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(text = "Maybe later")
|
||||
@@ -400,14 +391,6 @@ fun HomeScreen() {
|
||||
onDismissRequest = { showBottomSheet = false },
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
val pubkey = accountViewModel.nostr.signer.currentUser
|
||||
val shortPubkey = pubkey?.short() ?: "Not available"
|
||||
|
||||
val userName =
|
||||
userProfile?.asRecord()?.displayName
|
||||
?: userProfile?.asRecord()?.name
|
||||
?: "No name"
|
||||
|
||||
val dismissAndRun: (suspend () -> Unit) -> Unit = { action ->
|
||||
scope.launch {
|
||||
sheetState.hide()
|
||||
@@ -432,8 +415,8 @@ fun HomeScreen() {
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Avatar(
|
||||
picture = userProfile?.asRecord()?.picture,
|
||||
description = userProfile?.asRecord()?.displayName,
|
||||
picture = userProfile?.picture,
|
||||
description = userProfile?.name ?: "No name",
|
||||
shape = MaterialShapes.Cookie9Sided.toShape(),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
@@ -443,7 +426,7 @@ fun HomeScreen() {
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = userName,
|
||||
text = userProfile?.name ?: "No name",
|
||||
style = MaterialTheme.typography.titleLargeEmphasized,
|
||||
)
|
||||
}
|
||||
@@ -454,16 +437,15 @@ fun HomeScreen() {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pubkey?.let {
|
||||
userProfile?.publicKey?.let {
|
||||
val bech32 = it.toBech32()
|
||||
val data =
|
||||
ClipData.newPlainText(bech32, bech32)
|
||||
val data = ClipData.newPlainText(bech32, bech32)
|
||||
clipboardManager.setClipEntry(ClipEntry(data))
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(text = shortPubkey)
|
||||
Text(text = userProfile?.shortPublicKey ?: "Unknown")
|
||||
}
|
||||
FilledIconButton(
|
||||
onClick = {
|
||||
@@ -487,7 +469,7 @@ fun HomeScreen() {
|
||||
// Show the relay setup dialog if the msg relay list is empty
|
||||
if (isRelayListEmpty) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { relayViewModel.dismissRelayWarning() },
|
||||
onDismissRequest = { nostrViewModel.dismissRelayWarning() },
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
) {
|
||||
@@ -594,7 +576,7 @@ fun HomeScreen() {
|
||||
scope.launch {
|
||||
isBusy = true
|
||||
try {
|
||||
relayViewModel.refetchMsgRelays(currentUser)
|
||||
nostrViewModel.refetchMsgRelays()
|
||||
} catch (e: Exception) {
|
||||
snackbarHostState.showSnackbar("Failed to refresh metadata: ${e.message}")
|
||||
}
|
||||
@@ -614,7 +596,7 @@ fun HomeScreen() {
|
||||
enabled = !isBusy,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
relayViewModel.useDefaultMsgRelayList()
|
||||
nostrViewModel.useDefaultMsgRelayList()
|
||||
sheetState.hide()
|
||||
}
|
||||
},
|
||||
@@ -637,34 +619,29 @@ fun HomeScreen() {
|
||||
@Composable
|
||||
fun NewRequests(requests: List<Room>) {
|
||||
val navigator = LocalNavigator.current
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
|
||||
val total = requests.size
|
||||
val firstRoom = requests.getOrNull(0)
|
||||
val secondRoom = requests.getOrNull(1)
|
||||
|
||||
val firstName by remember(firstRoom) {
|
||||
firstRoom?.nameFlow(profileViewModel) ?: flowOf("")
|
||||
}.collectAsStateWithLifecycle("Loading...")
|
||||
|
||||
val secondName by remember(secondRoom) {
|
||||
secondRoom?.nameFlow(profileViewModel) ?: flowOf("")
|
||||
}.collectAsStateWithLifecycle("")
|
||||
val firstRoomState by (firstRoom as Room).rememberUiState(nostrViewModel)
|
||||
val secondRoomState by (secondRoom as Room).rememberUiState(nostrViewModel)
|
||||
|
||||
val supportingText = when {
|
||||
total == 1 && firstRoom != null -> {
|
||||
total == 1 -> {
|
||||
val message = firstRoom.lastMessage ?: ""
|
||||
"$firstName: $message"
|
||||
"${firstRoomState.name}: $message"
|
||||
}
|
||||
|
||||
total == 2 -> {
|
||||
"$firstName and $secondName"
|
||||
"${firstRoomState.name} and ${secondRoomState.name}"
|
||||
}
|
||||
|
||||
total > 2 -> {
|
||||
val othersCount = total - 2
|
||||
val othersText = if (othersCount == 1) "1 other" else "$othersCount others"
|
||||
"$firstName, $secondName and $othersText"
|
||||
"${firstRoomState.name}, ${secondRoomState.name} and $othersText"
|
||||
}
|
||||
|
||||
else -> ""
|
||||
@@ -720,14 +697,13 @@ fun NewRequests(requests: List<Room>) {
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val displayName by remember(room) { room.nameFlow(profileViewModel) }.collectAsStateWithLifecycle("Loading...")
|
||||
val picture by remember(room) { room.pictureFlow(profileViewModel) }.collectAsStateWithLifecycle(null)
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val roomState by room.rememberUiState(nostrViewModel)
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(onClick = onClick),
|
||||
leadingContent = {
|
||||
Avatar(picture = picture, description = displayName)
|
||||
Avatar(picture = roomState.picture, description = roomState.picture)
|
||||
},
|
||||
headlineContent = {
|
||||
Row(
|
||||
@@ -735,7 +711,7 @@ fun ChatRoom(room: Room, onClick: () -> Unit) {
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = displayName,
|
||||
text = roomState.name,
|
||||
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
@@ -768,7 +744,9 @@ fun BottomMenuList(
|
||||
onDismiss: (suspend () -> Unit) -> Unit
|
||||
) {
|
||||
val navigator = LocalNavigator.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val chatViewModel = LocalChatViewModel.current
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
|
||||
val defaultMenuList = listOf(
|
||||
"Update Profile" to { navigator.navigate(Screen.UpdateProfile) },
|
||||
@@ -798,7 +776,14 @@ fun BottomMenuList(
|
||||
}
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
FilledTonalButton(
|
||||
onClick = { onDismiss { accountViewModel.logout() } },
|
||||
onClick = {
|
||||
onDismiss {
|
||||
authViewModel.logout(onLogout = {
|
||||
nostrViewModel.resetInternalState()
|
||||
chatViewModel.resetInternalState()
|
||||
})
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError
|
||||
|
||||
@@ -58,16 +58,14 @@ import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.Keys
|
||||
import rust.nostr.sdk.NostrConnectUri
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalAppViewModel
|
||||
import su.reya.coop.LocalAuthViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalProfileViewModel
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalScanResult
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.Screen
|
||||
import su.reya.coop.shared.Avatar
|
||||
import su.reya.coop.shared.getExpressiveFontFamily
|
||||
import su.reya.coop.short
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
@@ -76,25 +74,20 @@ fun ImportScreen() {
|
||||
val navigator = LocalNavigator.current
|
||||
val qrScanResult = LocalScanResult.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val appViewModel = LocalAppViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val isBusy by appViewModel.isBusy.collectAsStateWithLifecycle(false)
|
||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
||||
val isBusy = authState.isBusy
|
||||
var secret by remember { mutableStateOf("") }
|
||||
var pubkey by remember { mutableStateOf<PublicKey?>(null) }
|
||||
|
||||
// Get metadata when pubkey changes
|
||||
val metadata by remember(pubkey) {
|
||||
pubkey?.let(profileViewModel::getMetadata) ?: flowOf(null)
|
||||
val profile by remember(pubkey) {
|
||||
pubkey?.let(nostrViewModel::getMetadata) ?: flowOf(null)
|
||||
}.collectAsStateWithLifecycle(null)
|
||||
|
||||
val profile = metadata?.asRecord()
|
||||
val displayName = profile?.displayName ?: profile?.name ?: pubkey?.short() ?: "Unknown"
|
||||
val picture = profile?.picture
|
||||
|
||||
LaunchedEffect(qrScanResult.content) {
|
||||
qrScanResult.content?.let { result ->
|
||||
runCatching {
|
||||
@@ -168,7 +161,7 @@ fun ImportScreen() {
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Avatar(
|
||||
picture = picture,
|
||||
picture = profile?.picture,
|
||||
description = "Profile picture",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
shape = MaterialShapes.Cookie9Sided.toShape(),
|
||||
@@ -176,7 +169,7 @@ fun ImportScreen() {
|
||||
}
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
text = displayName,
|
||||
text = profile?.name ?: "",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.titleLargeEmphasized.copy(
|
||||
fontFamily = getExpressiveFontFamily()
|
||||
@@ -250,10 +243,10 @@ fun ImportScreen() {
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (pubkey == null) {
|
||||
accountViewModel.verifyIdentity(secret).let { pubkey = it }
|
||||
authViewModel.verifyIdentity(secret).let { pubkey = it }
|
||||
} else {
|
||||
// Import the identity
|
||||
accountViewModel.importIdentity(secret)
|
||||
authViewModel.importIdentity(secret)
|
||||
// Navigate to the home screen
|
||||
navigator.navigate(Screen.Home)
|
||||
}
|
||||
|
||||
@@ -13,22 +13,24 @@ import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coop.composeapp.generated.resources.Res
|
||||
import coop.composeapp.generated.resources.ic_arrow_back
|
||||
import io.github.alexzhirkevich.qrose.rememberQrCodePainter
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
|
||||
@Composable
|
||||
fun MyQrScreen() {
|
||||
val navigator = LocalNavigator.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val currentUser = accountViewModel.nostr.signer.currentUser ?: return
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val currentUser by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
@@ -59,7 +61,7 @@ fun MyQrScreen() {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
painter = rememberQrCodePainter(currentUser.toBech32()),
|
||||
painter = rememberQrCodePainter(currentUser?.publicKey?.toBech32() ?: ""),
|
||||
contentDescription = "My QR"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import su.reya.coop.LocalChatViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalProfileViewModel
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalScanResult
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.Screen
|
||||
@@ -71,10 +71,10 @@ fun NewChatScreen() {
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val navigator = LocalNavigator.current
|
||||
val qrScanResult = LocalScanResult.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val chatViewModel = LocalChatViewModel.current
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
|
||||
val contactList by profileViewModel.contactList.collectAsStateWithLifecycle()
|
||||
val contactList by nostrViewModel.contactList.collectAsStateWithLifecycle()
|
||||
var query by remember { mutableStateOf("") }
|
||||
|
||||
val createGroup = remember { mutableStateOf(false) }
|
||||
@@ -96,12 +96,12 @@ fun NewChatScreen() {
|
||||
selectedReceivers.add(pubkey)
|
||||
}
|
||||
} else if (query.contains("@")) {
|
||||
val pubkey = profileViewModel.searchByAddress(query)
|
||||
val pubkey = nostrViewModel.searchByAddress(query)
|
||||
if (pubkey != null) {
|
||||
selectedReceivers.add(pubkey)
|
||||
}
|
||||
} else {
|
||||
val results = profileViewModel.searchByNostr(query)
|
||||
val results = nostrViewModel.searchByNostr(query)
|
||||
searchResults.clear()
|
||||
searchResults.addAll(results)
|
||||
}
|
||||
@@ -290,13 +290,9 @@ fun ReceiverChip(
|
||||
pubkey: PublicKey,
|
||||
onRemove: () -> Unit
|
||||
) {
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val metadataFlow = remember(pubkey) { profileViewModel.getMetadata(pubkey) }
|
||||
val metadata by metadataFlow.collectAsState(initial = null)
|
||||
|
||||
val profile = metadata?.asRecord()
|
||||
val displayName = profile?.name ?: profile?.displayName ?: pubkey.short()
|
||||
val picture = profile?.picture
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
|
||||
val profile by profileFlow.collectAsState(initial = null)
|
||||
|
||||
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
|
||||
InputChip(
|
||||
@@ -304,7 +300,7 @@ fun ReceiverChip(
|
||||
onClick = onRemove,
|
||||
label = {
|
||||
Text(
|
||||
text = displayName,
|
||||
text = profile?.name ?: "No name",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
@@ -312,8 +308,8 @@ fun ReceiverChip(
|
||||
},
|
||||
avatar = {
|
||||
Avatar(
|
||||
picture = picture,
|
||||
description = displayName,
|
||||
picture = profile?.picture,
|
||||
description = profile?.name ?: "No name",
|
||||
size = 24.dp
|
||||
)
|
||||
},
|
||||
@@ -376,13 +372,9 @@ fun ContactListItem(
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit
|
||||
) {
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val metadataFlow = remember(pubkey) { profileViewModel.getMetadata(pubkey) }
|
||||
val metadata by metadataFlow.collectAsState(initial = null)
|
||||
|
||||
val profile = metadata?.asRecord()
|
||||
val displayName = profile?.name ?: profile?.displayName ?: pubkey.short()
|
||||
val picture = profile?.picture
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
|
||||
val profile by profileFlow.collectAsState(initial = null)
|
||||
|
||||
SegmentedListItem(
|
||||
selected = isSelected,
|
||||
@@ -394,15 +386,15 @@ fun ContactListItem(
|
||||
),
|
||||
leadingContent = {
|
||||
Avatar(
|
||||
picture = picture,
|
||||
description = displayName,
|
||||
picture = profile?.picture,
|
||||
description = profile?.name ?: "",
|
||||
size = 36.dp
|
||||
)
|
||||
},
|
||||
supportingContent = { Text(text = pubkey.short()) },
|
||||
content = {
|
||||
Text(
|
||||
text = displayName,
|
||||
text = profile?.name ?: "",
|
||||
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,21 +5,19 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalAppViewModel
|
||||
import su.reya.coop.LocalAuthViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalProfileViewModel
|
||||
import su.reya.coop.Screen
|
||||
import su.reya.coop.shared.ProfileEditor
|
||||
|
||||
@Composable
|
||||
fun NewIdentityScreen() {
|
||||
val appViewModel = LocalAppViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
val navigator = LocalNavigator.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val isBusy by appViewModel.isBusy.collectAsStateWithLifecycle(false)
|
||||
|
||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
||||
val isBusy = authState.isBusy
|
||||
|
||||
ProfileEditor(
|
||||
title = "Create a new identity",
|
||||
@@ -28,7 +26,7 @@ fun NewIdentityScreen() {
|
||||
onBack = { navigator.goBack() },
|
||||
onConfirm = { name, bio, bytes, type ->
|
||||
scope.launch {
|
||||
accountViewModel.createIdentity(name, bio, bytes, type)
|
||||
authViewModel.createIdentity(name, bio, bytes, type)
|
||||
navigator.navigate(Screen.Home)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import coop.composeapp.generated.resources.Res
|
||||
import coop.composeapp.generated.resources.coop
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalAuthViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.Screen
|
||||
@@ -57,12 +57,12 @@ fun OnboardingScreen() {
|
||||
val context = LocalContext.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val navigator = LocalNavigator.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val logoPainter = painterResource(Res.drawable.coop)
|
||||
val expressiveFont = getExpressiveFontFamily()
|
||||
|
||||
|
||||
val annotatedText = buildAnnotatedString {
|
||||
append("By using Coop, you agree to accept\nour ")
|
||||
// Push "Terms of Use" link
|
||||
@@ -158,9 +158,9 @@ fun OnboardingScreen() {
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (accountViewModel.isExternalSignerAvailable()) {
|
||||
if (authViewModel.isExternalSignerAvailable()) {
|
||||
try {
|
||||
accountViewModel.connectExternalSigner()
|
||||
authViewModel.connectExternalSigner()
|
||||
navigator.navigate(Screen.Home)
|
||||
} catch (e: Exception) {
|
||||
e.message?.let { snackbarHostState.showSnackbar(it) }
|
||||
|
||||
@@ -46,7 +46,7 @@ import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import su.reya.coop.LocalChatViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalProfileViewModel
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.Screen
|
||||
import su.reya.coop.shared.Avatar
|
||||
@@ -56,28 +56,27 @@ import su.reya.coop.short
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun ProfileScreen(pubkey: String) {
|
||||
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
|
||||
|
||||
val context = LocalContext.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val navigator = LocalNavigator.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val chatViewModel = LocalChatViewModel.current
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
|
||||
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
|
||||
val profile by profileFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val metadataFlow = remember(pubkey) { profileViewModel.getMetadata(pubkey) }
|
||||
val metadata by metadataFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val profile = metadata?.asRecord()
|
||||
val displayName = profile?.displayName ?: profile?.name ?: "No name"
|
||||
val nip05 = profile?.nip05 ?: pubkey.short()
|
||||
val picture = profile?.picture
|
||||
val metadata = profile?.metadata?.asRecord()
|
||||
val nip05 = metadata?.nip05 ?: pubkey.short()
|
||||
val picture = metadata?.picture
|
||||
|
||||
val details = remember(profile) {
|
||||
listOf(
|
||||
"Username:" to (profile?.name ?: "None"),
|
||||
"Website:" to (profile?.website ?: "None"),
|
||||
"₿ Lightning Address:" to (profile?.lud16 ?: "None"),
|
||||
"Username:" to (metadata?.name ?: "None"),
|
||||
"Website:" to (metadata?.website ?: "None"),
|
||||
"₿ Lightning Address:" to (metadata?.lud16 ?: "None"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -135,7 +134,7 @@ fun ProfileScreen(pubkey: String) {
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = displayName,
|
||||
text = profile?.name ?: "No name",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.titleLargeEmphasized.copy(
|
||||
fontFamily = getExpressiveFontFamily()
|
||||
@@ -161,7 +160,8 @@ fun ProfileScreen(pubkey: String) {
|
||||
onClick = {
|
||||
scope.launch {
|
||||
try {
|
||||
val roomId = chatViewModel.createChatRoom(listOf(pubkey))
|
||||
val roomId =
|
||||
chatViewModel.createChatRoom(listOf(pubkey))
|
||||
navigator.navigate(Screen.Chat(roomId))
|
||||
} catch (e: Exception) {
|
||||
e.message?.let { snackbarHostState.showSnackbar(it) }
|
||||
@@ -240,4 +240,4 @@ fun ProfileScreen(pubkey: String) {
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.RelayMetadata
|
||||
import rust.nostr.sdk.RelayUrl
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalRelayViewModel
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@@ -74,7 +74,7 @@ import su.reya.coop.LocalSnackbarHostState
|
||||
fun RelayScreen() {
|
||||
val navigator = LocalNavigator.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val relayViewModel = LocalRelayViewModel.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val msgRelayList = remember { mutableStateListOf<RelayUrl>() }
|
||||
@@ -96,8 +96,8 @@ fun RelayScreen() {
|
||||
var relayToDelete by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
relayList.putAll(relayViewModel.currentUserRelayList())
|
||||
msgRelayList.addAll(relayViewModel.currentUserMsgRelayList())
|
||||
relayList.putAll(nostrViewModel.currentUserRelayList())
|
||||
msgRelayList.addAll(nostrViewModel.currentUserMsgRelayList())
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -321,7 +321,7 @@ fun RelayScreen() {
|
||||
return@launch
|
||||
}
|
||||
try {
|
||||
relayViewModel.removeMsgRelay(relayToDelete!!)
|
||||
nostrViewModel.removeMsgRelay(relayToDelete!!)
|
||||
msgRelayList.removeIf { it.toString() == relayToDelete }
|
||||
relayToDelete = null
|
||||
} catch (e: Exception) {
|
||||
@@ -349,7 +349,7 @@ fun AddRelayDialog(
|
||||
onMsgRelayAdded: (newRelay: String) -> Unit,
|
||||
onRelayAdded: (newRelay: String, metadata: RelayMetadata?) -> Unit,
|
||||
) {
|
||||
val relayViewModel = LocalRelayViewModel.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -401,17 +401,17 @@ fun AddRelayDialog(
|
||||
if (!isError) {
|
||||
when (selected) {
|
||||
"Messaging" -> {
|
||||
relayViewModel.addMsgRelay(relayAddress)
|
||||
nostrViewModel.addMsgRelay(relayAddress)
|
||||
onMsgRelayAdded(relayAddress)
|
||||
}
|
||||
|
||||
"Inbox" -> {
|
||||
relayViewModel.addInboxRelay(relayAddress)
|
||||
nostrViewModel.addInboxRelay(relayAddress)
|
||||
onRelayAdded(relayAddress, RelayMetadata.WRITE)
|
||||
}
|
||||
|
||||
"Outbox" -> {
|
||||
relayViewModel.addOutboxRelay(relayAddress)
|
||||
nostrViewModel.addOutboxRelay(relayAddress)
|
||||
onRelayAdded(relayAddress, RelayMetadata.READ)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ import kotlinx.coroutines.launch
|
||||
import su.reya.coop.LocalChatViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.RoomKind
|
||||
import su.reya.coop.Screen
|
||||
import su.reya.coop.nostr.RoomKind
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -155,4 +155,4 @@ fun RequestListScreen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,25 +5,19 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalAppViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalProfileViewModel
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.shared.ProfileEditor
|
||||
|
||||
@Composable
|
||||
fun UpdateProfileScreen() {
|
||||
val appViewModel = LocalAppViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val profileViewModel = LocalProfileViewModel.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val navigator = LocalNavigator.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val currentUser = accountViewModel.nostr.signer.currentUser ?: return
|
||||
val metadata by profileViewModel.getMetadata(currentUser).collectAsStateWithLifecycle()
|
||||
val isBusy by appViewModel.isBusy.collectAsStateWithLifecycle(false)
|
||||
|
||||
val profile = metadata?.asRecord()
|
||||
val isBusy by nostrViewModel.isBusy.collectAsStateWithLifecycle(false)
|
||||
val currentUser by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
val profile = currentUser?.metadata?.asRecord()
|
||||
|
||||
ProfileEditor(
|
||||
title = "Update profile",
|
||||
@@ -35,9 +29,9 @@ fun UpdateProfileScreen() {
|
||||
onBack = { navigator.goBack() },
|
||||
onConfirm = { name, bio, bytes, type ->
|
||||
scope.launch {
|
||||
profileViewModel.updateProfile(name, bio, bytes, type)
|
||||
nostrViewModel.updateProfile(name, bio, bytes, type)
|
||||
navigator.goBack()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package su.reya.coop.shared
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import su.reya.coop.nostr.Room
|
||||
import su.reya.coop.short
|
||||
import su.reya.coop.viewmodels.ProfileViewModel
|
||||
|
||||
fun Room.nameFlow(
|
||||
profileViewModel: ProfileViewModel,
|
||||
currentUser: PublicKey? = null
|
||||
): Flow<String> {
|
||||
// Return early if there's a custom subject/room name
|
||||
subject?.takeIf { it.isNotBlank() }?.let { return flowOf(it) }
|
||||
|
||||
val displayMembers = if (isGroup()) members.take(2) else members.take(1)
|
||||
if (displayMembers.isEmpty()) return flowOf("Unknown")
|
||||
|
||||
return combine(displayMembers.map { profileViewModel.getMetadata(it) }) { metadataArray ->
|
||||
val names = metadataArray.mapIndexed { i, metadata ->
|
||||
val profile = metadata?.asRecord()
|
||||
profile?.displayName?.takeIf { it.isNotBlank() }
|
||||
?: profile?.name?.takeIf { it.isNotBlank() }
|
||||
?: displayMembers[i].short()
|
||||
}
|
||||
|
||||
if (isGroup()) {
|
||||
val combined = names.joinToString(", ")
|
||||
val extraCount = members.size - names.size
|
||||
if (extraCount > 0) "$combined, +$extraCount" else combined
|
||||
} else {
|
||||
val name = names.first()
|
||||
if (displayMembers.first() == currentUser) "$name (you)" else name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Room.pictureFlow(profileViewModel: ProfileViewModel): Flow<String?> {
|
||||
val firstMember = members.firstOrNull() ?: return flowOf(null)
|
||||
return profileViewModel.getMetadata(firstMember).map { it?.asRecord()?.picture }
|
||||
}
|
||||
Reference in New Issue
Block a user