@ -8,51 +8,98 @@ import com.tailscale.ipn.ui.model.Netmap
import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.model.UserID
import com.tailscale.ipn.ui.service.IpnModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
data class PeerSet ( val user : Tailcfg . UserProfile ? , val peers : List < Tailcfg . Node > )
class PeerCategorizer ( val model : IpnModel ) {
fun groupedAndFilteredPeers ( searchTerm : String = " " ) : List < PeerSet > {
val netmap : Netmap . NetworkMap = model . netmap . value ?: return emptyList ( )
typealias GroupedPeers = MutableMap < UserID , MutableList < Tailcfg . Node > >
class PeerCategorizer ( val model : IpnModel , val scope : CoroutineScope ) {
var peerSets : List < PeerSet > = emptyList ( )
var lastSearchResult : List < PeerSet > = emptyList ( )
var searchTerm : String = " "
// Keep the peer sets current while the model is active
init {
scope . launch {
model . netmap . collect { netmap ->
netmap ?. let {
peerSets = regenerateGroupedPeers ( netmap )
lastSearchResult = peerSets
} ?: run {
peerSets = emptyList ( )
lastSearchResult = emptyList ( )
}
}
}
}
private fun regenerateGroupedPeers ( netmap : Netmap . NetworkMap ) : List < PeerSet > {
val peers : List < Tailcfg . Node > = netmap . Peers ?: return emptyList ( )
val selfNode = netmap . SelfNode
var grouped = mutableMapOf < UserID , MutableList < Tailcfg . Node > > ( )
val grouped = mutableMapOf < UserID , MutableList < Tailcfg . Node > > ( )
for ( peer in ( peers + selfNode ) ) {
// (jonathan) TODO: MDM -> There are a number of MDM settings to hide devices from the user
// (jonathan) TODO: MDM -> currentUser, otherUsers, taggedDevices
val userId = peer . User
if ( searchTerm . isNotEmpty ( ) && ! peer . ComputedName . contains ( searchTerm , ignoreCase = true ) ) {
continue
}
if ( ! grouped . containsKey ( userId ) ) {
grouped [ userId ] = mutableListOf ( )
}
grouped [ userId ] ?. add ( peer )
}
var selfPeers = ( grouped [ selfNode . User ] ?: emptyList ( ) ) . sortedBy { it . ComputedName }
grouped . remove ( selfNode . User )
val currentNode = selfPeers . firstOrNull { it . ID == selfNode . ID }
currentNode ?. let {
selfPeers = selfPeers . filter { it . ID != currentNode . ID }
selfPeers = listOf ( currentNode ) + selfPeers
}
val me = netmap . currentUserProfile ( )
val sorted = grouped . map { ( userId , peers ) ->
val peerSets = grouped . map { ( userId , peers ) ->
val profile = netmap . userProfile ( userId )
PeerSet ( profile , peers )
PeerSet ( profile , peers . sortedBy { it . ComputedName } )
} . sortedBy {
it . user ?. DisplayName ?: " Unknown User "
if ( it . user ?. ID == me ?. ID ) {
" "
} else {
it . user ?. DisplayName ?: " Unknown User "
}
}
val me = netmap . currentUserProfile ( )
return if ( selfPeers . isEmpty ( ) ) {
sorted
} else {
listOf ( PeerSet ( me , selfPeers ) ) + sorted
return peerSets
}
fun groupedAndFilteredPeers ( searchTerm : String = " " ) : List < PeerSet > {
if ( searchTerm . isEmpty ( ) ) {
return peerSets
}
if ( searchTerm == this . searchTerm ) {
return lastSearchResult
}
// We can optimize out typing... If the search term starts with the last search term, we can just search the last result
val setsToSearch = if ( searchTerm . startsWith ( this . searchTerm ) ) lastSearchResult else peerSets
this . searchTerm = searchTerm
val matchingSets = setsToSearch . map { peerSet ->
val user = peerSet . user
val peers = peerSet . peers
val userMatches = user ?. DisplayName ?. contains ( searchTerm , ignoreCase = true ) ?: false
if ( userMatches ) {
return @map peerSet
}
val matchingPeers = peers . filter { it . ComputedName . contains ( searchTerm , ignoreCase = true ) }
if ( matchingPeers . isNotEmpty ( ) ) {
PeerSet ( user , matchingPeers )
} else {
null
}
} . filterNotNull ( )
return matchingSets
}
}