yayan 1 anno fa
parent
commit
3773ff0cd6

+ 236 - 29
app/src/main/java/io/nexilis/alpha/ui/main/Friend.kt

@@ -1,54 +1,261 @@
 package io.nexilis.alpha.ui.main
 
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.Button
-import androidx.compose.material3.OutlinedTextField
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Verified
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SearchBar
 import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateList
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
 import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.navigation.NavHostController
+import coil.compose.AsyncImage
+import coil.request.ImageRequest
+import io.nexilis.service.data.entities.Buddy
 import io.nexilis.service.data.viewmodels.BuddyViewModel
+import org.json.JSONArray
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun Friend(navController: NavHostController) {
     val viewModel: BuddyViewModel = hiltViewModel()
-    val me by viewModel.me.observeAsState()
-    var textUsername by rememberSaveable(stateSaver = TextFieldValue.Saver) {
-        mutableStateOf(TextFieldValue("", TextRange(0, 7)))
-    }
-    Column(
+    var text by rememberSaveable { mutableStateOf("") }
+    var active by rememberSaveable { mutableStateOf(false) }
+    val list = remember { mutableStateListOf<Buddy>() }
+    val listSuggestions = remember { mutableStateListOf<Buddy>() }
+    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+    Scaffold(
         modifier = Modifier
-            .padding(16.dp)
-            .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.End
-    ) {
-        OutlinedTextField(
+            .nestedScroll(scrollBehavior.nestedScrollConnection),
+        topBar = {
+            TopAppBar(
+                title = { Text(text = "Add contact") },
+                navigationIcon = {
+                    NavigationIcon(navController = navController, isHome = false)
+                },
+                scrollBehavior = scrollBehavior
+            )
+        }
+    ) { padding ->
+        Column(
             modifier = Modifier
-                .fillMaxWidth()
-                .imePadding(),
-            value = textUsername,
-            onValueChange = { textUsername = it },
-            label = {
-                Text(text = "pin")
+                .padding(padding)
+                .fillMaxSize()
+        ) {
+            SearchBar(
+                modifier = Modifier.align(Alignment.CenterHorizontally),
+                query = text,
+                onQueryChange = { text = it },
+                onSearch = {
+                    active = false
+                    if (text.isNotEmpty()) {
+                        viewModel.searchFriend(text) { r, d ->
+                            list.clear()
+                            if (r && d.isNotEmpty()) {
+                                val data = JSONArray(d)
+                                for (i in 0 until data.length()) {
+                                    val t = data.getJSONObject(i)
+                                    val pin = t.optString("A00", "")
+                                    if (pin.isNotEmpty()) {
+                                        list.add(
+                                            Buddy(
+                                                f_pin = t.optString("A00", ""),
+                                                first_name = t.optString("A02", ""),
+                                                last_name = t.optString("A03", ""),
+                                                image_id = t.optString("A74", ""),
+                                                official_account = t.optString("oacc", "0")
+                                            )
+                                        )
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                active = active,
+                onActiveChange = {
+                    active = it
+                    if (it) {
+                        viewModel.suggestFriend("0") { r, d ->
+                            list.clear()
+                            if (r && d.isNotEmpty()) {
+                                val data = JSONArray(d)
+                                for (i in 0 until data.length()) {
+                                    val t = data.getJSONObject(i)
+                                    val pin = t.optString("A00", "")
+                                    if (pin.isNotEmpty()) {
+                                        listSuggestions.add(
+                                            Buddy(
+                                                f_pin = t.optString("A00", ""),
+                                                first_name = t.optString("A02", ""),
+                                                last_name = t.optString("A03", ""),
+                                                image_id = t.optString("A74", ""),
+                                                official_account = t.optString("oacc", "0")
+                                            )
+                                        )
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                placeholder = { Text("Search name..") },
+                leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
+                trailingIcon = {
+                    if (active) {
+                        IconButton(onClick = { text = "" }) {
+                            Icon(Icons.Default.Close, contentDescription = null)
+                        }
+                    }
+                },
+            ) {
+                SearchResultList(viewModel = viewModel, list = listSuggestions)
+            }
+            Spacer(modifier = Modifier.size(8.dp))
+            SearchResultList(viewModel = viewModel, list = list)
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SearchResultList(
+    viewModel: BuddyViewModel,
+    list: SnapshotStateList<Buddy>
+) {
+    var selectedItem by remember { mutableIntStateOf(0) }
+    val openAlertDialog = remember { mutableStateOf(false) }
+    LazyColumn(
+        verticalArrangement = Arrangement.spacedBy(8.dp)
+    ) {
+        items(count = list.size) { item ->
+            val buddy = list[item]
+            ListItem(
+                modifier = Modifier.selectable(selected = selectedItem == item, onClick = {
+                    selectedItem = item
+                    openAlertDialog.value = true
+                }),
+                headlineContent = {
+                    Row {
+                        Text(
+                            "${buddy.first_name} ${buddy.last_name}".trim(),
+                            style = MaterialTheme.typography.titleSmall
+                        )
+                        if (buddy.official_account != "0") {
+                            val color: Color = when (buddy.official_account) {
+                                "1", "3" -> {
+                                    Color(0xFF4c87ef)
+                                }
+
+                                "2" -> {
+                                    Color(0xFF0cff00)
+                                }
+
+                                "5" -> {
+                                    Color(0xFFE000FF)
+                                }
+
+                                else -> {
+                                    Color(0xFF4c87ef)
+                                }
+                            }
+                            Icon(
+                                imageVector = Icons.Default.Verified,
+                                contentDescription = "",
+                                tint = color
+                            )
+                        }
+                    }
+                },
+                leadingContent = {
+                    AsyncImage(
+                        model = ImageRequest.Builder(LocalContext.current)
+                            .data("https://digixplatform.com/filepalio/image/${buddy.image_id}")
+                            .addHeader("Cookie", "PHPSESSID=123;MOBILE=123")
+                            .crossfade(true)
+                            .build(),
+                        placeholder = rememberVectorPainter(image = Icons.Default.Person),
+                        contentDescription = "",
+                        contentScale = ContentScale.Crop,
+                        modifier = Modifier
+                            .size(48.dp)
+                            .clip(CircleShape)
+                            .background(Color.LightGray),
+//                            .padding(8.dp),
+                        error = rememberVectorPainter(image = Icons.Default.Person),
+//                        colorFilter = ColorFilter.tint(Color.White)
+                    )
+                }
+            )
+        }
+    }
+    if (openAlertDialog.value) {
+        val buddy = list[selectedItem]
+        AlertDialog(
+            icon = {
+                Icon(Icons.Default.Info, contentDescription = "Example Icon")
+            },
+            title = {
+                Text(text = "Add contact")
             },
-            maxLines = 1
+            text = {
+                Text(text = "Do you want add ${buddy.first_name} to your contact?")
+            },
+            onDismissRequest = {
+                openAlertDialog.value = false
+            },
+            confirmButton = {
+                TextButton(
+                    onClick = {
+                        viewModel.addFriend(buddy.f_pin)
+                    }
+                ) {
+                    Text("Confirm")
+                }
+            },
+            dismissButton = {
+                TextButton(
+                    onClick = {
+                        openAlertDialog.value = false
+                    }
+                ) {
+                    Text("Dismiss")
+                }
+            }
         )
-        Spacer(modifier = Modifier.size(8.dp))
-        Button(onClick = {
-            me?.let { viewModel.addFriend(it.f_pin, textUsername.text) }
-        }) {
-            Text(text = "Add")
-        }
     }
 }

+ 22 - 18
app/src/main/java/io/nexilis/alpha/ui/main/Main.kt

@@ -3,6 +3,7 @@ package io.nexilis.alpha.ui.main
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.filled.*
 import androidx.compose.material3.*
 import androidx.compose.runtime.*
@@ -24,9 +25,10 @@ import io.nexilis.alpha.ui.viewmodel.MainViewModel
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
-fun Main(navController: NavHostController = rememberNavController()) {
+fun Main(navController: NavHostController) {
+    val navHostController = rememberNavController()
     val hostState = remember { SnackbarHostState() }
-    val navBackStackEntry by navController.currentBackStackEntryAsState()
+    val navBackStackEntry by navHostController.currentBackStackEntryAsState()
     val currentDestination = navBackStackEntry?.destination
     var isHome = true
     currentDestination?.route.let {
@@ -43,7 +45,7 @@ fun Main(navController: NavHostController = rememberNavController()) {
             TopAppBar(
                 title = { Text(text = title) },
                 navigationIcon = {
-                    NavigationIcon(navController = navController, isHome = isHome)
+                    NavigationIcon(navController = navHostController, isHome = isHome)
                 },
                 actions = {
                     Actions(navController = navController)
@@ -51,33 +53,38 @@ fun Main(navController: NavHostController = rememberNavController()) {
             )
         },
         bottomBar = {
-            BottomBar(navController = navController, isHome = isHome)
+            BottomBar(navController = navHostController, isHome = isHome)
         },
         snackbarHost = { SnackbarHost(hostState = hostState) },
         floatingActionButton = {
             FloatingActionButton(
-                navController = navController,
+                navController = navHostController,
                 isHome = isHome
             )
         }
     ) { contentPadding ->
+
         NavHost(
-            navController = navController,
+            navController = navHostController,
             startDestination = Screen.Home.route,
             modifier = Modifier.padding(contentPadding),
             route = Graph.home
         ) {
-            composable(route = Screen.Home.route) { Home(mainViewModel = mainViewModel) }
+            composable(route = Screen.Home.route) {
+                Home(mainViewModel = mainViewModel)
+            }
             composable(route = Screen.Chats.route) {
                 Chats(
-                    navController = navController,
+                    navController = navHostController,
                     mainViewModel = mainViewModel
                 )
             }
-            composable(route = Screen.Profile.route) { Profile(mainViewModel = mainViewModel) }
-            navigation(startDestination = Screen.Contact.route, route = Graph.child) {
+            composable(route = Screen.Profile.route) {
+                Profile(mainViewModel = mainViewModel)
+            }
+            navigation(route = Graph.child, startDestination = Screen.Contact.route) {
                 composable(route = Screen.Contact.route) {
-                    Contact(navController = navController, mainViewModel = mainViewModel)
+                    Contact(navController = navHostController, mainViewModel = mainViewModel)
                 }
                 composable(
                     route = Screen.Chat.route + "/{pin}",
@@ -86,19 +93,16 @@ fun Main(navController: NavHostController = rememberNavController()) {
                     it.arguments?.getString("pin")
                         ?.let { pin ->
                             Chat(
-                                navController = navController,
+                                navController = navHostController,
                                 pin = pin,
                                 mainViewModel = mainViewModel
                             )
                         }
                 }
-                composable(route = Screen.Friend.route) {
-                    Friend(navController = navController)
-                }
                 composable(route = Screen.SignUp.route) {
-                    SignUp(navController = navController) {
+                    SignUp(navController = navHostController) {
                         if (it) {
-                            navController.navigate(Graph.home)
+                            navHostController.navigate(Graph.home)
                         }
                     }
                 }
@@ -113,7 +117,7 @@ fun NavigationIcon(navController: NavHostController, isHome: Boolean) {
         navController.navigateUp()
     }) {
         Icon(
-            imageVector = Icons.Default.ArrowBack,
+            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
             contentDescription = ""
         )
     }

+ 1 - 1
app/src/main/java/io/nexilis/alpha/ui/main/Menu.kt

@@ -46,7 +46,7 @@ fun MainMenu(navController: NavHostController) {
             text = { Text(text = "New group") },
             onClick = { /*TODO*/ })
         DropdownMenuItem(
-            text = { Text(text = "Invite friend") },
+            text = { Text(text = "Add contact") },
             onClick = {
                 isOpenMenu = false
                 navController.navigate(Screen.Friend.route) {

+ 5 - 2
app/src/main/java/io/nexilis/alpha/ui/main/Root.kt

@@ -36,7 +36,10 @@ fun Root(navController: NavHostController = rememberNavController()) {
             }
         }
         composable(route = Graph.home) {
-            Main()
+            Main(navController = navController)
+        }
+        composable(route = Screen.Friend.route) {
+            Friend(navController = navController)
         }
     }
 }
@@ -45,5 +48,5 @@ object Graph {
     const val root = "root_graph"
     const val authentication = "auth_graph"
     const val home = "home_graph"
-    const val child = "child_graph"
+    const val child = "home_child_graph"
 }

+ 1 - 1
cpaas-lite/src/main/java/io/nexilis/service/Service.kt

@@ -45,7 +45,7 @@ private val passArray = charArrayOf(
     'g'
 )
 
-private const val DEFAULT_TIMEOUT: Long = 30 * 1000
+private const val DEFAULT_TIMEOUT: Long = 10 * 1000
 
 
 internal val pass = String(passArray).decrypt()

+ 3 - 1
cpaas-lite/src/main/java/io/nexilis/service/core/Incoming.kt

@@ -118,7 +118,9 @@ class Incoming private constructor() {
     private suspend fun pushMySelf(data: Data) {
         try {
             val me = context.getSharedPreferences().getString("pin", "")
-            ApiRoomDatabase.getDatabase(context).buddyDao().insert(
+            val dao = ApiRoomDatabase.getDatabase(context).buddyDao()
+            dao.deleteMe()
+            dao.insert(
                 Buddy(
                     data.bodies["A00"] ?: "",
                     data.bodies["A23"] ?: "",

+ 9 - 0
cpaas-lite/src/main/java/io/nexilis/service/data/daos/BuddyDao.kt

@@ -22,7 +22,16 @@ interface BuddyDao {
     @Insert(onConflict = OnConflictStrategy.REPLACE)
     suspend fun insert(entity: Buddy)
 
+    @Insert()
+    suspend fun insertMe(entity: Buddy)
+
     @Query("delete from Buddy")
     suspend fun deleteAll()
 
+    @Query("delete from Buddy where f_pin = :pin")
+    suspend fun delete(pin: String)
+
+    @Query("delete from Buddy where type = '1'")
+    suspend fun deleteMe()
+
 }

+ 0 - 1
cpaas-lite/src/main/java/io/nexilis/service/data/entities/Buddy.kt

@@ -6,7 +6,6 @@ import androidx.room.Index
 @Entity(
     primaryKeys = ["f_pin"],
     indices = [
-        Index(value = ["type"], unique = true),
         Index(value = ["user_id", "ex_status", "first_name", "email", "msisdn"])
     ]
 )

+ 33 - 2
cpaas-lite/src/main/java/io/nexilis/service/data/repositories/BuddyRepository.kt

@@ -89,12 +89,11 @@ class BuddyRepository @Inject constructor(private val dao: BuddyDao) : Repositor
         }
     }
 
-    fun addFriend(me: String, pin: String) {
+    fun addFriend(pin: String) {
         Service.sendAsync(
             Data(
                 code = "A100",
                 status = System.nanoTime().toString(),
-                f_pin = me,
                 bodies = mutableMapOf(
                     "ffpin" to pin
                 )
@@ -104,4 +103,36 @@ class BuddyRepository @Inject constructor(private val dao: BuddyDao) : Repositor
         }
     }
 
+    suspend fun searchFriend(name: String, completion: (Boolean, String) -> Unit) {
+        Service.sendSync(
+            Data(
+                code = "SSU24",
+                status = System.nanoTime().toString(),
+                bodies = mutableMapOf(
+                    "A92" to name
+                )
+            )
+        )?.let {
+            withContext(Dispatchers.Main) {
+                completion(it.isOk(), it.bodies["A112"] ?: "")
+            }
+        }
+    }
+
+    suspend fun suggestFriend(lastSequence: String, completion: (Boolean, String) -> Unit) {
+        Service.sendSync(
+            Data(
+                code = "SSS24",
+                status = System.nanoTime().toString(),
+                bodies = mutableMapOf(
+                    "BF" to lastSequence
+                )
+            )
+        )?.let {
+            withContext(Dispatchers.Main) {
+                completion(it.isOk(), it.bodies["A112"] ?: "")
+            }
+        }
+    }
+
 }

+ 21 - 6
cpaas-lite/src/main/java/io/nexilis/service/data/viewmodels/BuddyViewModel.kt

@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import io.nexilis.service.data.entities.Buddy
 import io.nexilis.service.data.repositories.BuddyRepository
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import javax.inject.Inject
 
@@ -20,22 +21,36 @@ class BuddyViewModel @Inject constructor(private val repository: BuddyRepository
         return repository.getBuddy(pin)
     }
 
-    fun insert(buddy: Buddy) = viewModelScope.launch {
+    fun insert(buddy: Buddy) = viewModelScope.launch(Dispatchers.IO) {
         repository.insert(buddy)
     }
 
     fun signUp(username: String, password: String, completion: (Boolean, String) -> Unit) =
-        viewModelScope.launch {
+        viewModelScope.launch(Dispatchers.IO) {
             repository.signUp(username, password, completion)
         }
 
-    fun signIn(api: String, username: String, password: String, completion: (Boolean, String) -> Unit) =
-        viewModelScope.launch {
+    fun signIn(
+        api: String,
+        username: String,
+        password: String,
+        completion: (Boolean, String) -> Unit
+    ) =
+        viewModelScope.launch(Dispatchers.IO) {
             repository.signIn(api, username, password, completion)
         }
 
-    fun addFriend(me: String, pin: String) = viewModelScope.launch {
-        repository.addFriend(me, pin)
+    fun addFriend(pin: String) = viewModelScope.launch(Dispatchers.IO) {
+        repository.addFriend(pin)
     }
 
+    fun searchFriend(name: String, completion: (Boolean, String) -> Unit) =
+        viewModelScope.launch(Dispatchers.IO) {
+            repository.searchFriend(name, completion)
+        }
+
+    fun suggestFriend(lastSequence: String, completion: (Boolean, String) -> Unit) =
+        viewModelScope.launch(Dispatchers.IO) {
+            repository.suggestFriend(lastSequence, completion)
+        }
 }

+ 3 - 2
cpaas-lite/src/main/java/io/nexilis/service/data/viewmodels/MessageViewModel.kt

@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import io.nexilis.service.data.entities.Message
 import io.nexilis.service.data.repositories.MessageRepository
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import javax.inject.Inject
 
@@ -23,11 +24,11 @@ class MessageViewModel @Inject constructor(private val repository: MessageReposi
         return repository.getOpposite(pin)
     }
 
-    fun insert(entity: Message) = viewModelScope.launch {
+    fun insert(entity: Message) = viewModelScope.launch(Dispatchers.IO) {
         repository.insert(entity)
     }
 
-    fun send(entity: Message) = viewModelScope.launch {
+    fun send(entity: Message) = viewModelScope.launch(Dispatchers.IO) {
         repository.send(entity)
     }