ConversationCreationActivity.kt 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. /*
  2. * Nextcloud Talk - Android Client
  3. *
  4. * SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
  5. * SPDX-License-Identifier: GPL-3.0-or-later
  6. */
  7. @file:Suppress("DEPRECATION")
  8. package com.nextcloud.talk.conversationcreation
  9. import android.annotation.SuppressLint
  10. import android.app.Activity
  11. import android.content.Context
  12. import android.content.Intent
  13. import android.net.Uri
  14. import android.os.Bundle
  15. import androidx.activity.compose.ManagedActivityResultLauncher
  16. import androidx.activity.compose.rememberLauncherForActivityResult
  17. import androidx.activity.compose.setContent
  18. import androidx.activity.result.ActivityResult
  19. import androidx.activity.result.contract.ActivityResultContracts
  20. import androidx.compose.foundation.clickable
  21. import androidx.compose.foundation.isSystemInDarkTheme
  22. import androidx.compose.foundation.layout.Arrangement
  23. import androidx.compose.foundation.layout.Box
  24. import androidx.compose.foundation.layout.Column
  25. import androidx.compose.foundation.layout.Row
  26. import androidx.compose.foundation.layout.Spacer
  27. import androidx.compose.foundation.layout.fillMaxHeight
  28. import androidx.compose.foundation.layout.fillMaxWidth
  29. import androidx.compose.foundation.layout.padding
  30. import androidx.compose.foundation.layout.size
  31. import androidx.compose.foundation.layout.width
  32. import androidx.compose.foundation.rememberScrollState
  33. import androidx.compose.foundation.shape.CircleShape
  34. import androidx.compose.foundation.verticalScroll
  35. import androidx.compose.material.icons.Icons
  36. import androidx.compose.material.icons.automirrored.filled.ArrowBack
  37. import androidx.compose.material3.AlertDialog
  38. import androidx.compose.material3.Button
  39. import androidx.compose.material3.ExperimentalMaterial3Api
  40. import androidx.compose.material3.HorizontalDivider
  41. import androidx.compose.material3.Icon
  42. import androidx.compose.material3.IconButton
  43. import androidx.compose.material3.MaterialTheme
  44. import androidx.compose.material3.OutlinedTextField
  45. import androidx.compose.material3.Scaffold
  46. import androidx.compose.material3.Switch
  47. import androidx.compose.material3.Text
  48. import androidx.compose.material3.TextField
  49. import androidx.compose.material3.TopAppBar
  50. import androidx.compose.runtime.Composable
  51. import androidx.compose.runtime.DisposableEffect
  52. import androidx.compose.runtime.collectAsState
  53. import androidx.compose.runtime.getValue
  54. import androidx.compose.runtime.mutableStateOf
  55. import androidx.compose.runtime.remember
  56. import androidx.compose.runtime.setValue
  57. import androidx.compose.ui.Alignment
  58. import androidx.compose.ui.Modifier
  59. import androidx.compose.ui.draw.clip
  60. import androidx.compose.ui.graphics.Color
  61. import androidx.compose.ui.layout.ContentScale
  62. import androidx.compose.ui.platform.LocalContext
  63. import androidx.compose.ui.platform.LocalView
  64. import androidx.compose.ui.res.colorResource
  65. import androidx.compose.ui.res.painterResource
  66. import androidx.compose.ui.res.stringResource
  67. import androidx.compose.ui.text.style.TextAlign
  68. import androidx.compose.ui.unit.dp
  69. import androidx.compose.ui.unit.sp
  70. import androidx.core.view.WindowCompat
  71. import androidx.lifecycle.ViewModelProvider
  72. import autodagger.AutoInjector
  73. import coil.compose.AsyncImage
  74. import com.nextcloud.talk.R
  75. import com.nextcloud.talk.activities.BaseActivity
  76. import com.nextcloud.talk.application.NextcloudTalkApplication
  77. import com.nextcloud.talk.chat.ChatActivity
  78. import com.nextcloud.talk.contacts.ContactsActivityCompose
  79. import com.nextcloud.talk.contacts.loadImage
  80. import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
  81. import com.nextcloud.talk.utils.PickImage
  82. import com.nextcloud.talk.utils.bundle.BundleKeys
  83. import javax.inject.Inject
  84. @AutoInjector(NextcloudTalkApplication::class)
  85. class ConversationCreationActivity : BaseActivity() {
  86. @Inject
  87. lateinit var viewModelFactory: ViewModelProvider.Factory
  88. private lateinit var pickImage: PickImage
  89. override fun onCreate(savedInstanceState: Bundle?) {
  90. super.onCreate(savedInstanceState)
  91. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  92. val conversationCreationViewModel = ViewModelProvider(
  93. this,
  94. viewModelFactory
  95. )[ConversationCreationViewModel::class.java]
  96. val conversationUser = conversationCreationViewModel.currentUser
  97. pickImage = PickImage(this, conversationUser)
  98. setContent {
  99. val colorScheme = viewThemeUtils.getColorScheme(this)
  100. val context = LocalContext.current
  101. MaterialTheme(
  102. colorScheme = colorScheme
  103. ) {
  104. ConversationCreationScreen(conversationCreationViewModel, context, pickImage)
  105. }
  106. SetStatusBarColor()
  107. }
  108. }
  109. }
  110. @Composable
  111. private fun SetStatusBarColor() {
  112. val view = LocalView.current
  113. val isDarkMod = isSystemInDarkTheme()
  114. DisposableEffect(isDarkMod) {
  115. val activity = view.context as Activity
  116. activity.window.statusBarColor = activity.getColor(R.color.bg_default)
  117. WindowCompat.getInsetsController(activity.window, activity.window.decorView).apply {
  118. isAppearanceLightStatusBars = !isDarkMod
  119. }
  120. onDispose { }
  121. }
  122. }
  123. @OptIn(ExperimentalMaterial3Api::class)
  124. @Composable
  125. fun ConversationCreationScreen(
  126. conversationCreationViewModel: ConversationCreationViewModel,
  127. context: Context,
  128. pickImage: PickImage
  129. ) {
  130. val selectedImageUri = conversationCreationViewModel.selectedImageUri.collectAsState().value
  131. val imagePickerLauncher = rememberLauncherForActivityResult(
  132. contract = ActivityResultContracts.StartActivityForResult()
  133. ) { result ->
  134. if (result.resultCode == Activity.RESULT_OK) {
  135. pickImage.onImagePickerResult(result.data) { uri ->
  136. conversationCreationViewModel.updateSelectedImageUri(uri)
  137. }
  138. }
  139. }
  140. val remoteFilePickerLauncher = rememberLauncherForActivityResult(
  141. contract = ActivityResultContracts.StartActivityForResult()
  142. ) { result ->
  143. if (result.resultCode == Activity.RESULT_OK) {
  144. pickImage.onSelectRemoteFilesResult(imagePickerLauncher, result.data)
  145. }
  146. }
  147. val cameraLauncher = rememberLauncherForActivityResult(
  148. contract = ActivityResultContracts.StartActivityForResult()
  149. ) { result ->
  150. if (result.resultCode == Activity.RESULT_OK) {
  151. pickImage.onTakePictureResult(imagePickerLauncher, result.data)
  152. }
  153. }
  154. val launcher = rememberLauncherForActivityResult(
  155. contract = ActivityResultContracts.StartActivityForResult(),
  156. onResult = { result ->
  157. if (result.resultCode == Activity.RESULT_OK) {
  158. val data = result.data
  159. val selectedParticipants =
  160. data?.getParcelableArrayListExtra<AutocompleteUser>("selectedParticipants")
  161. ?: emptyList()
  162. val participants = selectedParticipants.toMutableList()
  163. conversationCreationViewModel.updateSelectedParticipants(participants)
  164. }
  165. }
  166. )
  167. Scaffold(
  168. topBar = {
  169. TopAppBar(
  170. title = { Text(text = stringResource(id = R.string.nc_new_conversation)) },
  171. navigationIcon = {
  172. IconButton(onClick = {
  173. (context as? Activity)?.finish()
  174. }) {
  175. Icon(
  176. Icons.AutoMirrored.Filled.ArrowBack,
  177. contentDescription = stringResource(id = R.string.back_button)
  178. )
  179. }
  180. }
  181. )
  182. },
  183. content = { paddingValues ->
  184. Column(
  185. modifier = Modifier
  186. .padding(paddingValues)
  187. .verticalScroll(rememberScrollState())
  188. ) {
  189. DefaultUserAvatar(selectedImageUri)
  190. UploadAvatar(
  191. pickImage = pickImage,
  192. onImageSelected = { uri -> conversationCreationViewModel.updateSelectedImageUri(uri) },
  193. imagePickerLauncher = imagePickerLauncher,
  194. remoteFilePickerLauncher = remoteFilePickerLauncher,
  195. cameraLauncher = cameraLauncher,
  196. onDeleteImage = { conversationCreationViewModel.updateSelectedImageUri(null) },
  197. selectedImageUri = selectedImageUri
  198. )
  199. ConversationNameAndDescription(conversationCreationViewModel)
  200. AddParticipants(launcher, context, conversationCreationViewModel)
  201. RoomCreationOptions(conversationCreationViewModel)
  202. CreateConversation(conversationCreationViewModel, context)
  203. }
  204. }
  205. )
  206. }
  207. @Composable
  208. fun DefaultUserAvatar(selectedImageUri: Uri?) {
  209. Box(
  210. modifier = Modifier.fillMaxWidth(),
  211. contentAlignment = Alignment.Center
  212. ) {
  213. if (selectedImageUri != null) {
  214. AsyncImage(
  215. model = selectedImageUri,
  216. contentDescription = stringResource(id = R.string.user_avatar),
  217. contentScale = ContentScale.Crop,
  218. modifier = Modifier
  219. .size(84.dp)
  220. .padding(top = 8.dp)
  221. .clip(CircleShape)
  222. )
  223. } else {
  224. AsyncImage(
  225. model = R.drawable.ic_circular_group,
  226. contentDescription = stringResource(id = R.string.user_avatar),
  227. contentScale = ContentScale.Crop,
  228. modifier = Modifier
  229. .size(84.dp)
  230. .padding(top = 8.dp)
  231. .clip(CircleShape)
  232. )
  233. }
  234. }
  235. }
  236. @Composable
  237. fun UploadAvatar(
  238. pickImage: PickImage,
  239. onImageSelected: (Uri) -> Unit,
  240. imagePickerLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
  241. remoteFilePickerLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
  242. cameraLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
  243. onDeleteImage: () -> Unit,
  244. selectedImageUri: Uri?
  245. ) {
  246. Row(
  247. modifier = Modifier
  248. .fillMaxWidth()
  249. .padding(16.dp),
  250. horizontalArrangement = Arrangement.Center
  251. ) {
  252. IconButton(
  253. onClick = {
  254. pickImage.takePicture(cameraLauncher)
  255. }
  256. ) {
  257. Icon(
  258. painter = painterResource(id = R.drawable.ic_baseline_photo_camera_24),
  259. contentDescription = null,
  260. modifier = Modifier.size(24.dp)
  261. )
  262. }
  263. IconButton(onClick = {
  264. pickImage.selectLocal(imagePickerLauncher)
  265. }) {
  266. Icon(
  267. painter = painterResource(id = R.drawable.upload),
  268. contentDescription = null,
  269. modifier = Modifier.size(24.dp)
  270. )
  271. }
  272. IconButton(
  273. onClick = {
  274. pickImage.selectRemote(remoteFilePickerLauncher)
  275. }
  276. ) {
  277. Icon(
  278. painter = painterResource(id = R.drawable.ic_mimetype_folder),
  279. contentDescription = null,
  280. modifier = Modifier.size(24.dp)
  281. )
  282. }
  283. if (selectedImageUri != null) {
  284. IconButton(onClick = {
  285. onDeleteImage()
  286. }) {
  287. Icon(
  288. painter = painterResource(id = R.drawable.ic_delete_grey600_24dp),
  289. contentDescription = null,
  290. modifier = Modifier.size(24.dp)
  291. )
  292. }
  293. }
  294. }
  295. }
  296. @Composable
  297. fun ConversationNameAndDescription(conversationCreationViewModel: ConversationCreationViewModel) {
  298. val conversationRoomName = conversationCreationViewModel.roomName.collectAsState()
  299. val conversationDescription = conversationCreationViewModel.conversationDescription.collectAsState()
  300. OutlinedTextField(
  301. value = conversationRoomName.value,
  302. onValueChange = {
  303. conversationCreationViewModel.updateRoomName(it)
  304. },
  305. label = { Text(text = stringResource(id = R.string.nc_call_name)) },
  306. modifier = Modifier
  307. .padding(start = 16.dp, end = 16.dp)
  308. .fillMaxWidth(),
  309. singleLine = true
  310. )
  311. OutlinedTextField(
  312. value = conversationDescription.value,
  313. onValueChange = {
  314. conversationCreationViewModel.updateConversationDescription(it)
  315. },
  316. label = { Text(text = stringResource(id = R.string.nc_conversation_description)) },
  317. modifier = Modifier
  318. .padding(top = 8.dp, start = 16.dp, end = 16.dp)
  319. .fillMaxWidth()
  320. )
  321. }
  322. @SuppressLint("SuspiciousIndentation")
  323. @Composable
  324. fun AddParticipants(
  325. launcher: ManagedActivityResultLauncher<Intent, ActivityResult>,
  326. context: Context,
  327. conversationCreationViewModel: ConversationCreationViewModel
  328. ) {
  329. val participants = conversationCreationViewModel.selectedParticipants.collectAsState().value
  330. Column(
  331. modifier = Modifier
  332. .fillMaxHeight()
  333. .padding(start = 16.dp, end = 16.dp, top = 16.dp)
  334. ) {
  335. Row {
  336. Text(
  337. text = stringResource(id = R.string.nc_participants).uppercase(),
  338. fontSize = 14.sp,
  339. modifier = Modifier.padding(start = 0.dp, bottom = 16.dp)
  340. )
  341. Spacer(modifier = Modifier.weight(1f))
  342. if (participants.isNotEmpty()) {
  343. Text(
  344. text = stringResource(id = R.string.nc_edit),
  345. fontSize = 12.sp,
  346. modifier = Modifier
  347. .padding(start = 16.dp, bottom = 16.dp)
  348. .clickable {
  349. val intent = Intent(context, ContactsActivityCompose::class.java)
  350. intent.putParcelableArrayListExtra(
  351. "selectedParticipants",
  352. participants as ArrayList<AutocompleteUser>
  353. )
  354. intent.putExtra("isAddParticipants", true)
  355. intent.putExtra("isAddParticipantsEdit", true)
  356. launcher.launch(intent)
  357. },
  358. textAlign = TextAlign.Right
  359. )
  360. }
  361. }
  362. participants.toSet().forEach { participant ->
  363. Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
  364. val imageUri = participant.id?.let { conversationCreationViewModel.getImageUri(it, true) }
  365. val errorPlaceholderImage: Int = R.drawable.account_circle_96dp
  366. val loadedImage = loadImage(imageUri, context, errorPlaceholderImage)
  367. AsyncImage(
  368. model = loadedImage,
  369. contentDescription = stringResource(id = R.string.user_avatar),
  370. modifier = Modifier.size(width = 32.dp, height = 32.dp)
  371. )
  372. participant.label?.let {
  373. Text(
  374. text = it,
  375. modifier = Modifier.padding(all = 16.dp),
  376. fontSize = 15.sp
  377. )
  378. }
  379. }
  380. HorizontalDivider(thickness = 0.1.dp, color = Color.Black)
  381. }
  382. Row(
  383. modifier = Modifier
  384. .fillMaxWidth()
  385. .clickable {
  386. val intent = Intent(context, ContactsActivityCompose::class.java)
  387. intent.putExtra("isAddParticipants", true)
  388. launcher.launch(intent)
  389. },
  390. verticalAlignment = Alignment.CenterVertically
  391. ) {
  392. if (participants.isEmpty()) {
  393. Icon(
  394. painter = painterResource(id = R.drawable.ic_account_plus),
  395. contentDescription = null,
  396. modifier = Modifier.size(24.dp)
  397. )
  398. Text(
  399. text = stringResource(id = R.string.nc_add_participants),
  400. modifier = Modifier.padding(start = 16.dp)
  401. )
  402. }
  403. }
  404. }
  405. }
  406. @Composable
  407. fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewModel) {
  408. val isGuestsAllowed = conversationCreationViewModel.isGuestsAllowed.value
  409. val isConversationAvailableForRegisteredUsers = conversationCreationViewModel
  410. .isConversationAvailableForRegisteredUsers.value
  411. val isOpenForGuestAppUsers = conversationCreationViewModel.openForGuestAppUsers.value
  412. Text(
  413. text = stringResource(id = R.string.nc_new_conversation_visibility).uppercase(),
  414. fontSize = 14.sp,
  415. modifier = Modifier.padding(top = 24.dp, start = 16.dp, end = 16.dp)
  416. )
  417. ConversationOptions(
  418. icon = R.drawable.ic_avatar_link,
  419. text = R.string.nc_guest_access_allow_title,
  420. switch = {
  421. Switch(
  422. checked = isGuestsAllowed,
  423. onCheckedChange = {
  424. conversationCreationViewModel.isGuestsAllowed.value = it
  425. }
  426. )
  427. },
  428. showDialog = false,
  429. conversationCreationViewModel = conversationCreationViewModel
  430. )
  431. if (isGuestsAllowed) {
  432. ConversationOptions(
  433. icon = R.drawable.ic_lock_grey600_24px,
  434. text = R.string.nc_set_password,
  435. showDialog = true,
  436. conversationCreationViewModel = conversationCreationViewModel
  437. )
  438. }
  439. ConversationOptions(
  440. icon = R.drawable.baseline_format_list_bulleted_24,
  441. text = R.string.nc_open_conversation_to_registered_users,
  442. switch = {
  443. Switch(
  444. checked = isConversationAvailableForRegisteredUsers,
  445. onCheckedChange = {
  446. conversationCreationViewModel.isConversationAvailableForRegisteredUsers.value = it
  447. }
  448. )
  449. },
  450. showDialog = false,
  451. conversationCreationViewModel = conversationCreationViewModel
  452. )
  453. if (isConversationAvailableForRegisteredUsers) {
  454. ConversationOptions(
  455. text = R.string.nc_open_to_guest_app_users,
  456. switch = {
  457. Switch(
  458. checked = isOpenForGuestAppUsers,
  459. onCheckedChange = {
  460. conversationCreationViewModel.openForGuestAppUsers.value = it
  461. }
  462. )
  463. },
  464. showDialog = false,
  465. conversationCreationViewModel = conversationCreationViewModel
  466. )
  467. }
  468. }
  469. @Composable
  470. fun ConversationOptions(
  471. icon: Int? = null,
  472. text: Int,
  473. switch: @Composable (() -> Unit)? = null,
  474. showDialog: Boolean,
  475. conversationCreationViewModel: ConversationCreationViewModel
  476. ) {
  477. var showPasswordDialog by remember { mutableStateOf(false) }
  478. Row(
  479. modifier = Modifier
  480. .fillMaxWidth()
  481. .padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
  482. .then(
  483. if (showDialog) {
  484. Modifier.clickable {
  485. showPasswordDialog = true
  486. }
  487. } else {
  488. Modifier
  489. }
  490. ),
  491. horizontalArrangement = Arrangement.SpaceBetween,
  492. verticalAlignment = Alignment.CenterVertically
  493. ) {
  494. if (icon != null) {
  495. Icon(
  496. painter = painterResource(id = icon),
  497. contentDescription = null,
  498. modifier = Modifier.size(24.dp)
  499. )
  500. Spacer(modifier = Modifier.width(16.dp))
  501. } else {
  502. Spacer(modifier = Modifier.width(40.dp))
  503. }
  504. Text(
  505. text = stringResource(id = text),
  506. modifier = Modifier.weight(1f)
  507. )
  508. if (switch != null) {
  509. switch()
  510. }
  511. if (showPasswordDialog) {
  512. ShowPasswordDialog(
  513. onDismiss = { showPasswordDialog = false },
  514. conversationCreationViewModel = conversationCreationViewModel
  515. )
  516. }
  517. }
  518. }
  519. @Composable
  520. fun ShowPasswordDialog(onDismiss: () -> Unit, conversationCreationViewModel: ConversationCreationViewModel) {
  521. var password by remember { mutableStateOf("") }
  522. AlertDialog(
  523. containerColor = colorResource(id = R.color.dialog_background),
  524. onDismissRequest = onDismiss,
  525. confirmButton = {
  526. Button(onClick = {
  527. conversationCreationViewModel.updatePassword(password)
  528. onDismiss()
  529. }) {
  530. Text(text = stringResource(id = R.string.save))
  531. }
  532. },
  533. title = { Text(text = stringResource(id = R.string.nc_set_password)) },
  534. text = {
  535. TextField(
  536. value = password,
  537. onValueChange = {
  538. password = it
  539. },
  540. label = { Text(text = stringResource(id = R.string.nc_guest_access_password_dialog_hint)) }
  541. )
  542. },
  543. dismissButton = {
  544. Button(onClick = { onDismiss() }) {
  545. Text(text = stringResource(id = R.string.nc_cancel))
  546. }
  547. }
  548. )
  549. }
  550. @Composable
  551. fun CreateConversation(conversationCreationViewModel: ConversationCreationViewModel, context: Context) {
  552. val selectedParticipants by conversationCreationViewModel.selectedParticipants.collectAsState()
  553. Box(
  554. modifier = Modifier
  555. .fillMaxWidth()
  556. .padding(all = 16.dp),
  557. contentAlignment = Alignment.Center
  558. ) {
  559. Button(
  560. onClick = {
  561. conversationCreationViewModel.createRoomAndAddParticipants(
  562. roomType = CompanionClass.ROOM_TYPE_GROUP,
  563. conversationName = conversationCreationViewModel.roomName.value,
  564. participants = selectedParticipants.toSet()
  565. ) { roomToken ->
  566. val bundle = Bundle()
  567. bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
  568. val chatIntent = Intent(context, ChatActivity::class.java)
  569. chatIntent.putExtras(bundle)
  570. chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
  571. context.startActivity(chatIntent)
  572. }
  573. }
  574. ) {
  575. Text(text = stringResource(id = R.string.create_conversation))
  576. }
  577. }
  578. }
  579. class CompanionClass {
  580. companion object {
  581. internal val TAG = ConversationCreationActivity::class.simpleName
  582. internal const val ROOM_TYPE_GROUP = "2"
  583. }
  584. }