Explorar o código

Merge pull request #4011 from nextcloud/contacts_theming

Contacts theming
Marcel Hibbe hai 8 meses
pai
achega
85b94d679b

+ 10 - 0
app/build.gradle

@@ -158,6 +158,8 @@ ext {
     workVersion = "2.9.1"
     espressoVersion = "3.6.1"
     media3_version = "1.4.0"
+    coroutines_version = "1.3.9"
+    mockitoKotlinVersion = "4.1.0"
 }
 
 configurations.configureEach {
@@ -333,6 +335,14 @@ dependencies {
 
     implementation "com.google.dagger:hilt-android:$hilt_version"
     kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
+
+    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3")
+    testImplementation("junit:junit:4.13.2")
+    androidTestImplementation(platform("androidx.compose:compose-bom:2024.06.00"))
+    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+    testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
+    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
+
 }
 
 tasks.register('installGitHooks', Copy) {

+ 20 - 22
app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityCompose.kt

@@ -13,10 +13,8 @@ import android.content.Context
 import android.content.Intent
 import android.os.Bundle
 import android.util.Log
-import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
@@ -35,6 +33,7 @@ import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.List
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.ExperimentalMaterial3Api
@@ -61,9 +60,8 @@ import androidx.compose.ui.unit.sp
 import androidx.lifecycle.ViewModelProvider
 import autodagger.AutoInjector
 import coil.compose.AsyncImage
-import coil.request.ImageRequest
-import coil.transform.CircleCropTransformation
 import com.nextcloud.talk.R
+import com.nextcloud.talk.activities.BaseActivity
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.chat.ChatActivity
 import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
@@ -72,7 +70,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys
 import javax.inject.Inject
 
 @AutoInjector(NextcloudTalkApplication::class)
-class ContactsActivityCompose : ComponentActivity() {
+class ContactsActivityCompose : BaseActivity() {
 
     @Inject
     lateinit var viewModelFactory: ViewModelProvider.Factory
@@ -83,9 +81,12 @@ class ContactsActivityCompose : ComponentActivity() {
         super.onCreate(savedInstanceState)
         NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
         contactsViewModel = ViewModelProvider(this, viewModelFactory)[ContactsViewModel::class.java]
-
         setContent {
-            MaterialTheme {
+            val colorScheme = viewThemeUtils.getColorScheme(this)
+            val uiState = contactsViewModel.contactsViewState.collectAsState()
+            MaterialTheme(
+                colorScheme = colorScheme
+            ) {
                 val context = LocalContext.current
                 Scaffold(
                     topBar = {
@@ -96,7 +97,6 @@ class ContactsActivityCompose : ComponentActivity() {
                         )
                     },
                     content = {
-                        val uiState = contactsViewModel.contactsViewState.collectAsState()
                         Column(Modifier.padding(it)) {
                             ConversationCreationOptions(context = context)
                             ContactsList(
@@ -170,6 +170,7 @@ fun ContactsItem(contacts: List<AutocompleteUser>, contactsViewModel: ContactsVi
             }
             items(contactsForInitial) { contact ->
                 ContactItemRow(contact = contact, contactsViewModel = contactsViewModel, context = context)
+                Log.d(CompanionClass.TAG, "Contacts:$contact")
             }
         }
     }
@@ -205,15 +206,10 @@ fun ContactItemRow(contact: AutocompleteUser, contactsViewModel: ContactsViewMod
         verticalAlignment = Alignment.CenterVertically
     ) {
         val imageUri = contact.id?.let { contactsViewModel.getImageUri(it, true) }
-        val imageRequest = ImageRequest.Builder(context)
-            .data(imageUri)
-            .transformations(CircleCropTransformation())
-            .error(R.drawable.account_circle_96dp)
-            .placeholder(R.drawable.account_circle_96dp)
-            .build()
-
+        val errorPlaceholderImage: Int = R.drawable.account_circle_96dp
+        val loadedImage = loadImage(imageUri, context, errorPlaceholderImage)
         AsyncImage(
-            model = imageRequest,
+            model = loadedImage,
             contentDescription = stringResource(R.string.user_avatar),
             modifier = Modifier.size(width = 45.dp, height = 45.dp)
         )
@@ -246,8 +242,10 @@ fun ContactItemRow(contact: AutocompleteUser, contactsViewModel: ContactsViewMod
 fun AppBar(title: String, context: Context, contactsViewModel: ContactsViewModel) {
     val searchQuery by contactsViewModel.searchQuery.collectAsState()
     val searchState = contactsViewModel.searchState.collectAsState()
+
     TopAppBar(
         title = { Text(text = title) },
+
         navigationIcon = {
             IconButton(onClick = {
                 (context as? Activity)?.finish()
@@ -282,13 +280,13 @@ fun ConversationCreationOptions(context: Context) {
             modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp),
             verticalAlignment = Alignment.CenterVertically
         ) {
-            Image(
+            Icon(
+                painter = painterResource(id = R.drawable.baseline_chat_bubble_outline_24),
                 modifier = Modifier
                     .width(40.dp)
                     .height(40.dp)
                     .padding(8.dp),
-                painter = painterResource(R.drawable.baseline_chat_bubble_outline_24),
-                contentDescription = stringResource(R.string.new_conversation_creation_icon)
+                contentDescription = null
             )
             Text(
                 modifier = Modifier
@@ -308,13 +306,13 @@ fun ConversationCreationOptions(context: Context) {
                 },
             verticalAlignment = Alignment.CenterVertically
         ) {
-            Image(
+            Icon(
+                Icons.AutoMirrored.Filled.List,
                 modifier = Modifier
                     .width(40.dp)
                     .height(40.dp)
                     .padding(8.dp),
-                painter = painterResource(R.drawable.baseline_format_list_bulleted_24),
-                contentDescription = stringResource(R.string.join_open_conversations_icon)
+                contentDescription = null
             )
             Text(
                 modifier = Modifier

+ 36 - 0
app/src/main/java/com/nextcloud/talk/contacts/ContactsApplication.kt

@@ -0,0 +1,36 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.contacts
+
+import android.app.Application
+import coil.ImageLoader
+import coil.ImageLoaderFactory
+import coil.disk.DiskCache
+import coil.memory.MemoryCache
+import coil.util.DebugLogger
+import com.nextcloud.talk.utils.ContactUtils
+
+class ContactsApplication : Application(), ImageLoaderFactory {
+    override fun newImageLoader(): ImageLoader {
+        val imageLoader = ImageLoader.Builder(this)
+            .memoryCache {
+                MemoryCache.Builder(this)
+                    .maxSizePercent(ContactUtils.CACHE_MEMORY_SIZE_PERCENTAGE)
+                    .build()
+            }
+            .diskCache {
+                DiskCache.Builder()
+                    .maxSizePercent(ContactUtils.CACHE_DISK_SIZE_PERCENTAGE)
+                    .directory(cacheDir)
+                    .build()
+            }
+            .logger(DebugLogger())
+            .build()
+        return imageLoader
+    }
+}

+ 1 - 0
app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt

@@ -13,4 +13,5 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall
 interface ContactsRepository {
     suspend fun getContacts(searchQuery: String?, shareTypes: List<String>): AutocompleteOverall
     suspend fun createRoom(roomType: String, sourceType: String, userId: String, conversationName: String?): RoomOverall
+    fun getImageUri(avatarId: String, requestBigSize: Boolean): String
 }

+ 8 - 0
app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt

@@ -64,4 +64,12 @@ class ContactsRepositoryImpl(
         )
         return response
     }
+
+    override fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
+        return ApiUtils.getUrlForAvatar(
+            _currentUser.baseUrl,
+            avatarId,
+            requestBigSize
+        )
+    }
 }

+ 2 - 13
app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt

@@ -9,27 +9,21 @@ package com.nextcloud.talk.contacts
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
-import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
 import com.nextcloud.talk.models.json.conversations.Conversation
-import com.nextcloud.talk.users.UserManager
-import com.nextcloud.talk.utils.ApiUtils
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 class ContactsViewModel @Inject constructor(
-    private val repository: ContactsRepository,
-    private val userManager: UserManager
+    private val repository: ContactsRepository
 ) : ViewModel() {
 
     private val _contactsViewState = MutableStateFlow<ContactsUiState>(ContactsUiState.None)
     val contactsViewState: StateFlow<ContactsUiState> = _contactsViewState
     private val _roomViewState = MutableStateFlow<RoomUiState>(RoomUiState.None)
     val roomViewState: StateFlow<RoomUiState> = _roomViewState
-    private val _currentUser = userManager.currentUser.blockingGet()
-    val currentUser: User = _currentUser
     private val _searchQuery = MutableStateFlow("")
     val searchQuery: StateFlow<String> = _searchQuery
     private val shareTypes: MutableList<String> = mutableListOf(ShareType.User.shareType)
@@ -86,13 +80,8 @@ class ContactsViewModel @Inject constructor(
             }
         }
     }
-
     fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
-        return ApiUtils.getUrlForAvatar(
-            _currentUser.baseUrl,
-            avatarId,
-            requestBigSize
-        )
+        return repository.getImageUri(avatarId, requestBigSize)
     }
 }
 

+ 24 - 0
app/src/main/java/com/nextcloud/talk/contacts/ImageRequest.kt

@@ -0,0 +1,24 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@email.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.contacts
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import coil.request.ImageRequest
+import coil.transform.CircleCropTransformation
+
+@Composable
+fun loadImage(imageUri: String?, context: Context, errorPlaceholderImage: Int): ImageRequest {
+    val imageRequest = ImageRequest.Builder(context)
+        .data(imageUri)
+        .transformations(CircleCropTransformation())
+        .error(errorPlaceholderImage)
+        .placeholder(errorPlaceholderImage)
+        .build()
+    return imageRequest
+}

+ 46 - 67
app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt

@@ -7,7 +7,6 @@
 
 package com.nextcloud.talk.contacts
 
-import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.text.KeyboardActions
@@ -17,13 +16,10 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.filled.Close
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
-import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextField
-import androidx.compose.material3.TextFieldDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.TextStyle
@@ -34,82 +30,65 @@ import com.nextcloud.talk.R
 
 @Composable
 fun DisplaySearch(text: String, onTextChange: (String) -> Unit, contactsViewModel: ContactsViewModel) {
-    Surface(
+    val keyboardController = LocalSoftwareKeyboardController.current
+    TextField(
         modifier = Modifier
             .fillMaxWidth()
-            .height(60.dp)
-            .background(Color.White)
-    ) {
-        val keyboardController = LocalSoftwareKeyboardController.current
-        TextField(
-            modifier = Modifier
-                .fillMaxWidth(),
-            value = text,
-            onValueChange = { onTextChange(it) },
-            placeholder = {
-                Text(
-                    text = stringResource(R.string.nc_search),
-                    color = Color.DarkGray
+            .height(60.dp),
+        value = text,
+        onValueChange = { onTextChange(it) },
+        placeholder = {
+            Text(
+                text = stringResource(R.string.nc_search)
+            )
+        },
+
+        textStyle = TextStyle(
+            fontSize = 16.sp
+        ),
+        singleLine = true,
+        leadingIcon = {
+            IconButton(
+                onClick = {
+                    onTextChange("")
+                    contactsViewModel.updateSearchState(false)
+                }
+            ) {
+                Icon(
+                    imageVector = Icons.AutoMirrored.Default.ArrowBack,
+                    contentDescription = stringResource(R.string.back_button)
                 )
-            },
+            }
+        },
 
-            textStyle = TextStyle(
-                color = Color.Black,
-                fontSize = 16.sp
-            ),
-            singleLine = true,
-            leadingIcon = {
+        trailingIcon = {
+            if (text.isNotEmpty()) {
                 IconButton(
                     onClick = {
                         onTextChange("")
-                        contactsViewModel.updateSearchState(false)
                     }
                 ) {
                     Icon(
-                        imageVector = Icons.AutoMirrored.Default.ArrowBack,
-                        contentDescription = stringResource(R.string.back_button),
-                        tint = Color.Black
+                        imageVector = Icons.Default.Close,
+                        contentDescription = stringResource(R.string.close_icon)
                     )
                 }
-            },
-
-            trailingIcon = {
-                if (text.isNotEmpty()) {
-                    IconButton(
-                        onClick = {
-                            onTextChange("")
-                        }
-                    ) {
-                        Icon(
-                            imageVector = Icons.Default.Close,
-                            contentDescription = stringResource(R.string.close_icon),
-                            tint = Color.Black
-                        )
-                    }
-                }
-            },
+            }
+        },
 
-            keyboardOptions = KeyboardOptions(
-                imeAction = ImeAction.Search
-            ),
+        keyboardOptions = KeyboardOptions(
+            imeAction = ImeAction.Search
+        ),
 
-            keyboardActions = KeyboardActions(
-                onSearch = {
-                    if (text.trim().isNotEmpty()) {
-                        keyboardController?.hide()
-                    } else {
-                        return@KeyboardActions
-                    }
+        keyboardActions = KeyboardActions(
+            onSearch = {
+                if (text.trim().isNotEmpty()) {
+                    keyboardController?.hide()
+                } else {
+                    return@KeyboardActions
                 }
-            ),
-            maxLines = 1,
-            colors = TextFieldDefaults.colors(
-                focusedContainerColor = Color.White,
-                unfocusedContainerColor = Color.White,
-                disabledContainerColor = Color.White,
-                focusedTextColor = Color.Black,
-                cursorColor = Color.Black
-            )
-        )
-    }
+            }
+        ),
+        maxLines = 1
+    )
 }

+ 2 - 0
app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt

@@ -12,6 +12,8 @@ import android.provider.ContactsContract
 object ContactUtils {
 
     const val MAX_CONTACT_LIMIT = 50
+    const val CACHE_MEMORY_SIZE_PERCENTAGE = 0.1
+    const val CACHE_DISK_SIZE_PERCENTAGE = 0.02
 
     fun getDisplayNameFromDeviceContact(context: Context, id: String?): String? {
         var displayName: String? = null

+ 2 - 2
app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml

@@ -11,8 +11,8 @@
     android:viewportHeight="24"
     android:viewportWidth="24"
     android:width="24dp">
-      
+
     <path android:fillColor="@android:color/white"
         android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v18l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,16L6,16l-2,2L4,4h16v12z"/>
-    
+
 </vector>

+ 126 - 0
app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt

@@ -0,0 +1,126 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.contacts
+
+import com.nextcloud.talk.contacts.apiService.FakeItem
+import com.nextcloud.talk.contacts.repository.FakeRepositoryError
+import com.nextcloud.talk.contacts.repository.FakeRepositorySuccess
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class ContactsViewModelTest {
+    private lateinit var viewModel: ContactsViewModel
+    private val repository: ContactsRepository = FakeRepositorySuccess()
+
+    val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
+
+    @Before
+    fun setup() {
+        Dispatchers.setMain(dispatcher)
+    }
+
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+    }
+
+    @Before
+    fun setUp() {
+        viewModel = ContactsViewModel(repository)
+    }
+
+    @Test
+    fun `fetch contacts`() =
+        runTest {
+            viewModel = ContactsViewModel(repository)
+            viewModel.getContactsFromSearchParams()
+            assert(viewModel.contactsViewState.value is ContactsUiState.Success)
+            val successState = viewModel.contactsViewState.value as ContactsUiState.Success
+            assert(successState.contacts == FakeItem.contacts)
+        }
+
+    @Test
+    fun `test error contacts state`() =
+        runTest {
+            viewModel = ContactsViewModel(FakeRepositoryError())
+            assert(viewModel.contactsViewState.value is ContactsUiState.Error)
+            val errorState = viewModel.contactsViewState.value as ContactsUiState.Error
+            assert(errorState.message == "unable to fetch contacts")
+        }
+
+    @Test
+    fun `update search query`() {
+        viewModel.updateSearchQuery("Ma")
+        assert(viewModel.searchQuery.value == "Ma")
+    }
+
+    @Test
+    fun `initial search query is empty string`() {
+        viewModel.updateSearchQuery("")
+        assert(viewModel.searchQuery.value == "")
+    }
+
+    @Test
+    fun `initial shareType is User`() {
+        assert(viewModel.shareTypeList.contains(ShareType.User.shareType))
+    }
+
+    @Test
+    fun `update shareTypes`() {
+        viewModel.updateShareTypes(ShareType.Group.shareType)
+        assert(viewModel.shareTypeList.contains(ShareType.Group.shareType))
+    }
+
+    @Test
+    fun `initial room state is none`() =
+        runTest {
+            assert(viewModel.roomViewState.value is RoomUiState.None)
+        }
+
+    @Test
+    fun `test success room state`() =
+        runTest {
+            viewModel.createRoom("1", "users", "s@gmail.com", null)
+            assert(viewModel.roomViewState.value is RoomUiState.Success)
+            val successState = viewModel.roomViewState.value as RoomUiState.Success
+            assert(successState.conversation == FakeItem.roomOverall.ocs!!.data)
+        }
+
+    @Test
+    fun `test failure room state`() =
+        runTest {
+            viewModel = ContactsViewModel(FakeRepositoryError())
+            viewModel.createRoom("1", "users", "s@gmail.com", null)
+            assert(viewModel.roomViewState.value is RoomUiState.Error)
+            val errorState = viewModel.roomViewState.value as RoomUiState.Error
+            assert(errorState.message == "unable to create room")
+        }
+
+    @Test
+    fun `test image uri`() {
+        val expectedImageUri = "https://mydomain.com/index.php/avatar/vidya/512"
+        val imageUri = viewModel.getImageUri("vidya", false)
+        assert(imageUri == expectedImageUri)
+    }
+
+    @Test
+    fun `test error image uri`() {
+        val expectedImageUri = "https://mydoman.com/index.php/avatar/vidya/512"
+        val imageUri = viewModel.getImageUri("vidya", false)
+        assert(imageUri != expectedImageUri)
+    }
+}

+ 58 - 0
app/src/test/java/com/nextcloud/talk/contacts/apiService/FakeItem.kt

@@ -0,0 +1,58 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.contacts.apiService
+
+import com.nextcloud.talk.models.json.autocomplete.AutocompleteOCS
+import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
+import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
+import com.nextcloud.talk.models.json.conversations.Conversation
+import com.nextcloud.talk.models.json.conversations.RoomOCS
+import com.nextcloud.talk.models.json.conversations.RoomOverall
+import com.nextcloud.talk.models.json.generic.GenericMeta
+import org.mockito.Mockito.mock
+
+object FakeItem {
+    val contacts: List<AutocompleteUser> =
+        listOf(
+            AutocompleteUser(id = "android", label = "Android", source = "users"),
+            AutocompleteUser(id = "android1", label = "Android 1", source = "users"),
+            AutocompleteUser(id = "android2", label = "Android 2", source = "users"),
+            AutocompleteUser(id = "Benny", label = "Benny J", source = "users"),
+            AutocompleteUser(id = "Benjamin", label = "Benjamin Schmidt", source = "users"),
+            AutocompleteUser(id = "Chris", label = "Christoph Schmidt", source = "users"),
+            AutocompleteUser(id = "Daniel", label = "Daniel H", source = "users"),
+            AutocompleteUser(id = "Dennis", label = "Dennis Richard", source = "users"),
+            AutocompleteUser(id = "Emma", label = "Emma Jackson", source = "users"),
+            AutocompleteUser(id = "Emily", label = "Emily Jackson", source = "users"),
+            AutocompleteUser(id = "Mario", label = "Mario Schmidt", source = "users"),
+            AutocompleteUser(id = "Maria", label = "Maria Schmidt", source = "users"),
+            AutocompleteUser(id = "Samsung", label = "Samsung A52", source = "users"),
+            AutocompleteUser(id = "Tom", label = "Tom Müller", source = "users"),
+            AutocompleteUser(id = "Tony", label = "Tony Baker", source = "users")
+        )
+    val contactsOverall = AutocompleteOverall(
+        ocs = AutocompleteOCS(
+            meta = GenericMeta(
+                status = "ok",
+                statusCode = 200,
+                message = "OK"
+            ),
+            data = contacts
+        )
+    )
+    val roomOverall: RoomOverall = RoomOverall(
+        ocs = RoomOCS(
+            meta = GenericMeta(
+                status = "ok",
+                statusCode = 200,
+                message = "OK"
+            ),
+            data = mock(Conversation::class.java)
+        )
+    )
+}

+ 31 - 0
app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositoryError.kt

@@ -0,0 +1,31 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kota@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.contacts.repository
+
+import com.nextcloud.talk.contacts.ContactsRepository
+import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
+import com.nextcloud.talk.models.json.conversations.RoomOverall
+
+class FakeRepositoryError : ContactsRepository {
+    override suspend fun getContacts(searchQuery: String?, shareTypes: List<String>): AutocompleteOverall {
+        throw Exception("unable to fetch contacts")
+    }
+
+    override suspend fun createRoom(
+        roomType: String,
+        sourceType: String,
+        userId: String,
+        conversationName: String?
+    ): RoomOverall {
+        throw Exception("unable to create room")
+    }
+
+    override fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
+        return "https://mydoman.com/index.php/avatar/$avatarId/512"
+    }
+}

+ 32 - 0
app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt

@@ -0,0 +1,32 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@email.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.contacts.repository
+
+import com.nextcloud.talk.contacts.ContactsRepository
+import com.nextcloud.talk.contacts.apiService.FakeItem
+import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
+import com.nextcloud.talk.models.json.conversations.RoomOverall
+
+class FakeRepositorySuccess : ContactsRepository {
+    override suspend fun getContacts(searchQuery: String?, shareTypes: List<String>): AutocompleteOverall {
+        return FakeItem.contactsOverall
+    }
+
+    override suspend fun createRoom(
+        roomType: String,
+        sourceType: String,
+        userId: String,
+        conversationName: String?
+    ): RoomOverall {
+        return FakeItem.roomOverall
+    }
+
+    override fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
+        return "https://mydomain.com/index.php/avatar/$avatarId/512"
+    }
+}

+ 1 - 3
build.gradle

@@ -11,12 +11,10 @@
 buildscript {
 
     ext {
-      kotlinVersion = '1.9.23'
-      hilt_version = '2.44'
       kotlinVersion = '2.0.0'
+        hilt_version = '2.44'
     }
 
-
     repositories {
         google()
         gradlePluginPortal()

+ 154 - 6
gradle/verification-metadata.xml

@@ -4,13 +4,13 @@
       <verify-metadata>true</verify-metadata>
       <verify-signatures>true</verify-signatures>
       <trusted-artifacts>
-         <trust file="tensorflow-lite-metadata-0.1.0-rc2.pom" reason="differing hash on every CI run - temp global trust"/>
-         <trust group="androidx.fragment"/>
          <trust group="com.android.tools.build" name="aapt2" version="8.4.1-11315950" reason="ships OS specific artifacts (win/linux) - temp global trust"/>
          <trust group="com.github.nextcloud-deps" name="android-talk-webrtc" version="110.5481.0" reason="ships OS specific artifacts (win/linux) - temp global trust"/>
-         <trust group="com.google.dagger"/>
-         <trust group="org.javassist" name="javassist" version="3.26.0-GA" reason="java assist"/>
          <trust file=".*-sources[.]jar" regex="true"/>
+          <trust group="com.google.dagger" />
+          <trust group="org.javassist" name="javassist" version="3.26.0-GA" reason="java assist"/>
+          <trust group="androidx.fragment"/>
+		 <trust file="tensorflow-lite-metadata-0.1.0-rc2.pom" reason="differing hash on every CI run - temp global trust"/>
       </trusted-artifacts>
       <ignored-keys>
          <ignored-key id="0AA3E5C3D232E79B" reason="Key couldn't be downloaded from any key server"/>
@@ -39,7 +39,10 @@
          <trusted-key id="0F07D1201BDDAB67CFB84EB479752DB6C966F0B8" group="com.google.android" name="annotations" version="4.1.1.4"/>
          <trusted-key id="10F3C7A02ECA55E502BADCF3991EFB94DB91127D" group="org.ow2" name="ow2" version="1.5.1"/>
          <trusted-key id="120D6F34E627ED3A772EBBFE55C7E5E701832382" group="org.snakeyaml" name="snakeyaml-engine" version="2.6"/>
-         <trusted-key id="147B691A19097624902F4EA9689CBE64F4BC997F" group="org.mockito"/>
+         <trusted-key id="147B691A19097624902F4EA9689CBE64F4BC997F">
+            <trusting group="org.mockito"/>
+            <trusting group="org.mockito.kotlin" name="mockito-kotlin" version="4.1.0"/>
+         </trusted-key>
          <trusted-key id="1597AB231B7ADD7E14B1D9C43F00DB67AE236E2E" group="org.conscrypt" name="conscrypt-android" version="2.5.2"/>
          <trusted-key id="190D5A957FF22273E601F7A7C92C5FEC70161C62" group="org.apache" name="apache" version="18"/>
          <trusted-key id="19BEAB2D799C020F17C69126B16698A4ADF4D638" group="org.checkerframework"/>
@@ -156,6 +159,7 @@
          <trusted-key id="8756C4F765C9AC3CB6B85D62379CE192D401AB61">
             <trusting group="eu.davidea"/>
             <trusting group="org.jetbrains.intellij.deps"/>
+            <trusting group="org.jetbrains.kotlinx"/>
          </trusted-key>
          <trusted-key id="8E3A02905A1AE67E7B0F9ACD3967D4EDA591B991" group="org.jetbrains.kotlinx" name="kotlinx-html-jvm" version="0.8.1"/>
          <trusted-key id="8F9A3C6D105B9F57844A721D79E193516BE7998F" group="org.dom4j" name="dom4j" version="2.1.4"/>
@@ -232,6 +236,7 @@
          <trusted-key id="E4AC7874F3479A0F1F8ECF9960BB45F36B649F22" group="fr.dudie" name="nominatim-api" version="3.4"/>
          <trusted-key id="E77417AC194160A3FABD04969A259C7EE636C5ED" group="^com[.]google($|([.].*))" regex="true"/>
          <trusted-key id="E7DC75FC24FB3C8DFE8086AD3D5839A2262CBBFB" group="org.jetbrains.kotlinx"/>
+          <trusted-key id="64B9B09F164AA0BF88742EB61188B69F6D6259CA" group="com.google.accompanist"/>
          <trusted-key id="E82D2EAF2E83830CE1F7F6BE571A5291E827E1C7" group="net.java" name="jvnet-parent" version="3"/>
          <trusted-key id="E85AED155021AF8A6C6B7A4A7C7D8456294423BA" group="org.objenesis"/>
          <trusted-key id="EAA526B91DD83BA3E1B9636FA730529CA355A63E" group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2.1"/>
@@ -294,6 +299,14 @@
             <sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="androidx.activity" name="activity-compose" version="1.7.0">
+         <artifact name="activity-compose-1.7.0.aar">
+            <sha256 value="caa72885d1ce7979c1d6c59a8b255c6097b770780d4d4da95d56979a348646cd" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="activity-compose-1.7.0.module">
+            <sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="androidx.annotation" name="annotation" version="1.0.0">
          <artifact name="annotation-1.0.0.jar">
             <sha256 value="0baae9755f7caf52aa80cd04324b91ba93af55d4d1d17dcc9a7b53d99ef7c016" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -326,6 +339,50 @@
             <sha256 value="9516c2ae44284ea0bd3d0eade0ee638879b708cbe31e3af92ba96c300604ebc3" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+       <component group="androidx.exifinterface" name="exifinterface" version="1.3.6">
+           <artifact name="exifinterface-1.3.6.aar">
+               <sha256 value="1804105e9e05fdd8f760413bad5de498c381aa329f4f9d94c851bc891ac654c6" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+           <artifact name="exifinterface-1.3.6.module">
+               <sha256 value="5e9fd84ca3fd3b7706f6856fa4383107de8676bf7c42b7d4b8108949414d6201" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
+       <component group="androidx.core" name="core" version="1.1.0">
+           <artifact name="core-1.1.0.pom">
+               <sha256 value="dae46132cdcd46b798425f7cb78fd65890869b6d26101ccdcd43461a4f51754c" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
+       <component group="androidx.core" name="core" version="1.3.2">
+           <artifact name="core-1.3.2.pom">
+               <sha256 value="afb5ea494dd083ed404cd51f580d218e37362f8ae326e893bee521290ed34920" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
+       <component group="androidx.test.ext" name="junit" version="1.1.5">
+           <artifact name="junit-1.1.5.aar">
+               <sha256 value="4307c0e60f5d701db9c59bcd9115af705113c36a9132fa3dbad58db1294e9bfd" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+           <artifact name="junit-1.1.5.pom">
+               <sha256 value="4cff0df04cae25831e821ef2f9129245783460e98d0fd67d8f6824065a134c4e" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
+       <component group="androidx.core" name="core-ktx" version="1.8.0">
+           <artifact name="core-ktx-1.8.0.module">
+               <sha256 value="a91bc3e02f209f643dd8275345a9e3003ce20d64fc0760eccf479c1709842f72" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
+       <component group="androidx.annotation" name="annotation-experimental" version="1.3.0">
+           <artifact name="annotation-experimental-1.3.0.aar">
+               <sha256 value="abfd29c8556e5bd0325a9f769ab9e9d154ff4a5515c476cdd5a2a8285b1b19dc" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+           <artifact name="annotation-experimental-1.3.0.module">
+               <sha256 value="5eebeaff01d042e06dcf292abf8964ad391e4b0159f0090f16253d6045d38da0" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
+       <component group="androidx.annotation" name="annotation-experimental" version="1.1.0-rc01">
+           <artifact name="annotation-experimental-1.1.0-rc01.module">
+               <sha256 value="d45ac493e84d968aabb2bea2b7744031a98cf5074447c0f3b862d600fc44b55c" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
       <component group="androidx.annotation" name="annotation" version="1.5.0">
          <artifact name="annotation-1.5.0.jar">
             <sha256 value="261fb7c0210858500bab66d34354972a75166ab4182add283780b05513d6ec4a" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -342,6 +399,14 @@
             <sha256 value="fbc64f5c44a7added8b6eab517cf7d70555e25153bf5d44a6ed9b0e5312f7de9" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+       <component group="androidx.exifinterface" name="exifinterface" version="1.3.2">
+           <artifact name="exifinterface-1.3.2.aar">
+               <sha256 value="8770c180103e0b8c04a07eb4c59153af639b09eca25deae9bdcdaf869d1e5b6b" origin="Generated by Gradle"/>
+           </artifact>
+           <artifact name="exifinterface-1.3.2.module">
+               <sha256 value="10ba5b5cbea7f5c8758be4fdaec60a3545e891a1130d830a442b88cf5336a885" origin="Generated by Gradle"/>
+           </artifact>
+       </component>
       <component group="androidx.annotation" name="annotation-experimental" version="1.0.0">
          <artifact name="annotation-experimental-1.0.0.pom">
             <sha256 value="6b73ff6608f4b1d6cbab620b65708a382d0b39901cf4e6b0d16f84a1b04d7732" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -355,6 +420,11 @@
             <sha256 value="0361d1526a4d7501255e19779e09e93cdbd07fee0e2f5c50b7a137432d510119" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+       <component group="androidx.annotation" name="annotation-experimental" version="1.1.0-rc01">
+       <artifact name="annotation-experimental-1.1.0-rc01.module">
+           <sha256 value="d45ac493e84d968aabb2bea2b7744031a98cf5074447c0f3b862d600fc44b55c" origin="Generated by Gradle" reason="Artifact is not signed"/>
+       </artifact>
+       </component>
       <component group="androidx.annotation" name="annotation-experimental" version="1.1.0-rc01">
          <artifact name="annotation-experimental-1.1.0-rc01.module">
             <sha256 value="d45ac493e84d968aabb2bea2b7744031a98cf5074447c0f3b862d600fc44b55c" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -376,6 +446,76 @@
             <sha256 value="9b6974a7dfe26d3c209dd63e16f8ee2461b57a091789160ca1eb492bb1bf3f84" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+       <component group="androidx.annotation" name="annotation-experimental" version="1.3.0">
+           <artifact name="annotation-experimental-1.3.0.aar">
+               <sha256 value="abfd29c8556e5bd0325a9f769ab9e9d154ff4a5515c476cdd5a2a8285b1b19dc" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+           <artifact name="annotation-experimental-1.3.0.module">
+               <sha256 value="5eebeaff01d042e06dcf292abf8964ad391e4b0159f0090f16253d6045d38da0" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
+       <component group="androidx.activity" name="activity-compose" version="1.7.0">
+           <artifact name="activity-compose-1.7.0.aar">
+               <sha256 value="caa72885d1ce7979c1d6c59a8b255c6097b770780d4d4da95d56979a348646cd" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+           <artifact name="activity-compose-1.7.0.module">
+               <sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
+       <component group="androidx.compose.runtime" name="runtime" version="1.0.1">
+           <artifact name="runtime-1.0.1.module">
+               <sha256 value="2543a8c7edc16bde91f140286b4fd3773d7204a283a4ec99f6e5e286aa92c0c3" origin="Generated by Gradle"/>
+           </artifact>
+       </component>
+       <component group="androidx.compose.runtime" name="runtime-saveable" version="1.0.1">
+           <artifact name="runtime-saveable-1.0.1.module">
+               <sha256 value="c0d6f142542d8d74f65481ef6526d2be265f01f812a112948fcde87a458f4fb6" origin="Generated by Gradle"/>
+           </artifact>
+       </component>
+       <component group="androidx.compose.ui" name="ui" version="1.0.1">
+           <artifact name="ui-1.0.1.aar">
+               <sha256 value="1943daa4a3412861b9a2bdc1a7c8c2ff05d9b8191c1d3e56ebb223d2eb4a8526" origin="Generated by Gradle"/>
+           </artifact>
+           <artifact name="ui-1.0.1.module">
+               <sha256 value="57031a6ac9b60e5b56792ebf5cde6e16812ff566ed9190cbd188b00b46c13779" origin="Generated by Gradle"/>
+           </artifact>
+       </component>
+       <component group="androidx.compose" name="compose-bom" version="2024.06.00">
+           <artifact name="compose-bom-2024.06.00.pom">
+               <sha256 value="1b391a969ff81c0bb43b3711e92d977e8bfa72457a11d8a37910a7051bdc3045" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
+       <component group="androidx.activity" name="activity-compose" version="1.7.0">
+           <artifact name="activity-compose-1.7.0.aar">
+               <sha256 value="caa72885d1ce7979c1d6c59a8b255c6097b770780d4d4da95d56979a348646cd" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+           <artifact name="activity-compose-1.7.0.module">
+               <sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
+       <component group="androidx.compose.runtime" name="runtime" version="1.0.1">
+           <artifact name="runtime-1.0.1.module">
+               <sha256 value="2543a8c7edc16bde91f140286b4fd3773d7204a283a4ec99f6e5e286aa92c0c3" origin="Generated by Gradle"/>
+           </artifact>
+       </component>
+       <component group="androidx.compose.runtime" name="runtime-saveable" version="1.0.1">
+           <artifact name="runtime-saveable-1.0.1.module">
+               <sha256 value="c0d6f142542d8d74f65481ef6526d2be265f01f812a112948fcde87a458f4fb6" origin="Generated by Gradle"/>
+           </artifact>
+       </component>
+       <component group="androidx.compose.ui" name="ui" version="1.0.1">
+           <artifact name="ui-1.0.1.aar">
+               <sha256 value="1943daa4a3412861b9a2bdc1a7c8c2ff05d9b8191c1d3e56ebb223d2eb4a8526" origin="Generated by Gradle"/>
+           </artifact>
+           <artifact name="ui-1.0.1.module">
+               <sha256 value="57031a6ac9b60e5b56792ebf5cde6e16812ff566ed9190cbd188b00b46c13779" origin="Generated by Gradle"/>
+           </artifact>
+       </component>
+       <component group="androidx.compose" name="compose-bom" version="2024.06.00">
+           <artifact name="compose-bom-2024.06.00.pom">
+               <sha256 value="1b391a969ff81c0bb43b3711e92d977e8bfa72457a11d8a37910a7051bdc3045" origin="Generated by Gradle" reason="Artifact is not signed"/>
+           </artifact>
+       </component>
       <component group="androidx.appcompat" name="appcompat" version="1.1.0">
          <artifact name="appcompat-1.1.0.pom">
             <sha256 value="340d617121f8ef8e02a6680c8f357aa3e542276d0c8a1cdcb6fd98984b2cb7b9" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -4902,6 +5042,14 @@
             <sha256 value="9a35e48f20f3021c21e469d52fa88b3acc08b20dab77b6c646f72f6fb205ec92" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
          </artifact>
       </component>
+      <component group="com.novoda" name="merlin" version="1.2.1">
+         <artifact name="merlin-1.2.1.aar">
+            <sha256 value="c62d03d0fde57f26fa633feeee24d7dfed3d66cc81097e4d6306b076cc7d70b6" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="merlin-1.2.1.pom">
+            <sha256 value="62dbaffb68b60ca317c05bba83a9fea8d866c3d3e7a2bd928c69591aa2fe4418" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.squareup" name="javapoet" version="1.2.0">
          <artifact name="javapoet-1.2.0.pom">
             <pgp value="9E84765A7AA3E3D3D5598A408E3F0DE7AE354651"/>
@@ -5860,7 +6008,7 @@
             <ignored-keys>
                <ignored-key id="DB0597E3144342256BC81E3EC727D053C4481CF5" reason="PGP verification failed"/>
             </ignored-keys>
-            <sha256 value="8359ad51e0476c8e0df7188a43f16d49733c4a428fb45e99794b783f01b97520" origin="Generated by Gradle" reason="PGP signature verification failed!"/>
+            <sha256 value="9a4f5e5674366c156c90391662f03ed7c5971d6aa63832df74a271da6ff82e96" origin="Generated by Gradle" reason="PGP signature verification failed!"/>
          </artifact>
       </component>
       <component group="org.xerial" name="sqlite-jdbc" version="3.41.2.2">