Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class ChatViewModel @Inject constructor(

init {
viewModelScope.launch {
repository.getMessages(peerId)
chatRepo.getMessages(peerId)
.distinctUntilChanged()
.collect { messages ->
Logger.d("ChatViewModel -> Messages updated: ${messages.size} messages for peer $peerId")
Expand Down Expand Up @@ -151,60 +151,6 @@ class ChatViewModel @Inject constructor(
}
}

/**
* Loads more messages for pagination.
* Called initially and when user scrolls to top.
*/
fun loadMoreMessages() {
viewModelScope.launch {
// Use tryLock to avoid waiting if already loading
if (!paginationMutex.tryLock()) return@launch

try {
if (isAllMessagesLoaded || _uiState.value.isLoadingMore) return@launch

_uiState.update { it.copy(isLoadingMore = true) }

try {
// ✅ PF04: FIX blocking .first() by using take(1).firstOrNull()
// This prevents potential 50-200ms blocking on Flow collection
val newPage = withContext(Dispatchers.IO) {
chatRepo.getMessagesPaged(peerId, pageSize, currentPage * pageSize)
.take(1)
.firstOrNull()
?: emptyList()
}

if (newPage.isEmpty()) {
isAllMessagesLoaded = true
} else {
// Prepend new messages efficiently using ArrayDeque
allMessages.addAll(0, newPage)

// Remove oldest messages if exceeding max to prevent memory leaks
while (allMessages.size > MAX_MESSAGES_IN_MEMORY) {
allMessages.removeLast()
}

currentPage++
}

_uiState.update {
it.copy(
messages = allMessages.toList(),
hasMoreMessages = !isAllMessagesLoaded,
isLoadingMore = false
)
}
} catch (e: Exception) {
Logger.e("ChatViewModel -> Failed to load messages", e)
_uiState.update { it.copy(isLoadingMore = false) }
}
} finally {
paginationMutex.unlock()
}
}
}

fun onInputChanged(text: String) {
_uiState.update { it.copy(inputText = text, draftText = text) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package com.p2p.meshify.feature.discovery

import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.p2p.meshify.core.common.R
import com.p2p.meshify.core.util.Logger
import com.p2p.meshify.domain.security.model.OobVerificationMethod
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

/**
* UI state for OOB verification flow.
*
* @property isLoading Whether verification data is being generated
* @property myPeerId Local device peer ID for out-of-band comparison
* @property peerPeerId Peer's device ID (null until received)
* @property isVerified Whether the peer identity has been confirmed
* @property verificationError String resource ID if verification failed
* @property selectedMethod Currently selected verification method
*/
data class OobVerificationUiState(
val isLoading: Boolean = false,
val myPeerId: String = "",
val peerPeerId: String? = null,
val isVerified: Boolean = false,
@field:StringRes val verificationError: Int? = null,
val selectedMethod: OobVerificationMethod = OobVerificationMethod.QR
)

/**
* ViewModel for Out-Of-Band (OOB) identity verification.
*
* Manages the OOB verification flow where users verify each other's identity
* by comparing peer IDs to ensure they are connecting to the intended device.
*
* Flow:
* 1. Display local peer ID as QR code or text for manual comparison
* 2. Receive peer's verification data (peer ID)
* 3. User confirms match or reports mismatch
* 4. If verified, the peer is trusted; if not, connection may be compromised
*/
@HiltViewModel
class OobVerificationViewModel @Inject constructor() : ViewModel() {

private val _uiState = MutableStateFlow(OobVerificationUiState())
val uiState: StateFlow<OobVerificationUiState> = _uiState.asStateFlow()

init {
generateVerificationData()
}

/**
* Generate local verification data (peer ID).
*/
private fun generateVerificationData() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, verificationError = null) }

try {
// Generate a deterministic peer ID for demonstration.
// In production, inject SimplePeerIdProvider and use getDeviceId().
val peerId = "PEER-${generateShortId()}"

_uiState.update { state ->
state.copy(
isLoading = false,
myPeerId = peerId
)
}
} catch (e: Exception) {
Logger.e("OobVerificationViewModel -> Failed to generate verification data", e)
_uiState.update { state ->
state.copy(
isLoading = false,
verificationError = R.string.oob_verification_error_generic
)
}
}
}
}

/**
* Select a verification method (QR, SAS, or NFC).
*/
fun selectMethod(method: OobVerificationMethod) {
_uiState.update { it.copy(selectedMethod = method) }
}

/**
* Receive peer's peer ID for comparison.
*/
fun receivePeerId(peerId: String) {
_uiState.update { it.copy(peerPeerId = peerId, verificationError = null) }
}

/**
* Verify that peer IDs match and update verification state.
*
* @return true if IDs match, false if they differ (possible MITM)
*/
fun verifyIdsMatch(): Boolean {
val state = _uiState.value
val isMatch = state.myPeerId.equals(state.peerPeerId ?: "", ignoreCase = true)

if (isMatch) {
_uiState.update { state ->
state.copy(
isVerified = true,
verificationError = null
)
}
} else {
_uiState.update { state ->
state.copy(
verificationError = R.string.oob_peer_ids_mismatch_mitm
)
}
}

return isMatch
}

/**
* Report peer ID mismatch — possible MITM attack.
*/
fun reportMismatch() {
_uiState.update { state ->
state.copy(
isVerified = false,
verificationError = R.string.oob_peer_ids_differ_distrust
)
}
}

/**
* Reset verification state to start fresh.
*/
fun reset() {
_uiState.update { OobVerificationUiState() }
generateVerificationData()
}

/**
* Clear any error messages without resetting verification state.
*/
fun clearError() {
_uiState.update { it.copy(verificationError = null) }
}

/**
* Generate a short random ID for peer identification.
*/
private fun generateShortId(): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return (1..8).map { chars.random() }.joinToString("")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import com.p2p.meshify.core.ui.components.MagneticChatItem
import com.p2p.meshify.core.ui.components.DeleteConfirmationDialog
import com.p2p.meshify.core.ui.components.ItemPosition
import com.p2p.meshify.core.ui.theme.MeshifyDesignSystem
import com.p2p.meshify.core.ui.theme.MeshifyThemeProperties

import java.text.SimpleDateFormat
import java.util.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand Down
Loading