Skip to content

Commit 8f45853

Browse files
Merge pull request #5931 from nextcloud/style/noid/userInfoDetailItemComposables
⚒️🎨 Migrate user info details to Composables
2 parents 3bbb451 + f5464c8 commit 8f45853

10 files changed

Lines changed: 542 additions & 168 deletions

File tree

app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt

Lines changed: 62 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ package com.nextcloud.talk.profile
1212
import android.app.Activity
1313
import android.content.pm.PackageManager
1414
import android.os.Bundle
15-
import android.text.Editable
1615
import android.text.TextUtils
17-
import android.text.TextWatcher
1816
import android.util.Log
19-
import android.view.LayoutInflater
2017
import android.view.Menu
2118
import android.view.MenuItem
2219
import android.view.View
2320
import android.view.ViewGroup
2421
import androidx.activity.result.ActivityResult
2522
import androidx.activity.result.contract.ActivityResultContracts
2623
import androidx.annotation.DrawableRes
24+
import androidx.compose.material3.MaterialTheme
25+
import androidx.compose.ui.platform.ComposeView
26+
import androidx.compose.ui.platform.ViewCompositionStrategy
2727
import androidx.core.content.ContextCompat
2828
import androidx.core.graphics.drawable.toDrawable
2929
import androidx.core.net.toFile
@@ -34,14 +34,12 @@ import autodagger.AutoInjector
3434
import com.github.dhaval2404.imagepicker.ImagePicker
3535
import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError
3636
import com.google.android.material.snackbar.Snackbar
37-
import com.nextcloud.android.common.ui.theme.utils.ColorRole
3837
import com.nextcloud.talk.R
3938
import com.nextcloud.talk.activities.BaseActivity
4039
import com.nextcloud.talk.api.NcApi
4140
import com.nextcloud.talk.application.NextcloudTalkApplication
4241
import com.nextcloud.talk.data.user.model.User
4342
import com.nextcloud.talk.databinding.ActivityProfileBinding
44-
import com.nextcloud.talk.databinding.UserInfoDetailsTableItemBinding
4543
import com.nextcloud.talk.models.json.generic.GenericOverall
4644
import com.nextcloud.talk.models.json.userprofile.Scope
4745
import com.nextcloud.talk.models.json.userprofile.UserProfileData
@@ -433,7 +431,7 @@ class ProfileActivity : BaseActivity() {
433431
result.add(
434432
UserInfoDetailsItem(
435433
R.drawable.ic_web,
436-
DisplayUtils.beautifyURL(userInfo.website),
434+
userInfo.website,
437435
resources!!.getString(R.string.user_info_website),
438436
Field.WEBSITE,
439437
userInfo.websiteScope
@@ -442,7 +440,7 @@ class ProfileActivity : BaseActivity() {
442440
result.add(
443441
UserInfoDetailsItem(
444442
R.drawable.ic_twitter,
445-
DisplayUtils.beautifyTwitterHandle(userInfo.twitter),
443+
userInfo.twitter,
446444
resources!!.getString(R.string.user_info_twitter),
447445
Field.TWITTER,
448446
userInfo.twitterScope
@@ -587,7 +585,7 @@ class ProfileActivity : BaseActivity() {
587585
credentials,
588586
ApiUtils.getUrlForUserData(currentUser!!.baseUrl!!, currentUser!!.userId!!),
589587
item.field.scopeName,
590-
item.scope!!.name
588+
item.scope!!.id
591589
)
592590
.retry(DEFAULT_RETRIES)
593591
.subscribeOn(Schedulers.io())
@@ -598,12 +596,12 @@ class ProfileActivity : BaseActivity() {
598596
}
599597

600598
override fun onNext(userProfileOverall: GenericOverall) {
601-
Log.d(TAG, "Successfully saved: " + item.scope + " as " + item.field)
599+
Log.d(TAG, "Successfully saved: " + item.scope!!.id + " as " + item.field.scopeName)
602600
}
603601

604602
override fun onError(e: Throwable) {
605603
item.scope = userInfo?.getScopeByField(item.field)
606-
Log.e(TAG, "Failed to saved: " + item.scope + " as " + item.field, e)
604+
Log.e(TAG, "Failed to saved: " + item.scope!!.id + " as " + item.field.scopeName, e)
607605
}
608606

609607
override fun onComplete() {
@@ -629,7 +627,7 @@ class ProfileActivity : BaseActivity() {
629627
var displayList: List<UserInfoDetailsItem>?
630628
var filteredDisplayList: MutableList<UserInfoDetailsItem> = LinkedList()
631629

632-
class ViewHolder(val binding: UserInfoDetailsTableItemBinding) : RecyclerView.ViewHolder(binding.root)
630+
class ViewHolder(val composeView: ComposeView) : RecyclerView.ViewHolder(composeView)
633631

634632
init {
635633
this.displayList = displayList ?: LinkedList()
@@ -653,9 +651,12 @@ class ProfileActivity : BaseActivity() {
653651
}
654652

655653
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
656-
val itemBinding =
657-
UserInfoDetailsTableItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
658-
return ViewHolder(itemBinding)
654+
val composeView = ComposeView(parent.context).apply {
655+
layoutParams =
656+
ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
657+
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool)
658+
}
659+
return ViewHolder(composeView)
659660
}
660661

661662
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
@@ -664,87 +665,60 @@ class ProfileActivity : BaseActivity() {
664665
} else {
665666
filteredDisplayList[position]
666667
}
667-
668-
initScopeElements(item, holder)
669-
670-
holder.binding.icon.setImageResource(item.icon)
671-
initUserInfoEditText(holder, item)
672-
673-
holder.binding.icon.contentDescription = item.hint
674-
viewThemeUtils.platform.colorImageView(holder.binding.icon, ColorRole.PRIMARY)
675-
if (!TextUtils.isEmpty(item.text) || profileActivity.edit) {
676-
holder.binding.userInfoDetailContainer.visibility = View.VISIBLE
677-
profileActivity.viewThemeUtils.material.colorTextInputLayout(holder.binding.userInfoInputLayout)
678-
if (profileActivity.edit &&
679-
profileActivity.editableFields.contains(item.field.toString().lowercase())
680-
) {
681-
holder.binding.userInfoEditTextEdit.isEnabled = true
682-
holder.binding.userInfoEditTextEdit.isFocusableInTouchMode = true
683-
holder.binding.userInfoEditTextEdit.isEnabled = true
684-
holder.binding.userInfoEditTextEdit.isCursorVisible = true
685-
holder.binding.scope.setOnClickListener {
686-
ScopeDialog(
687-
holder.binding.scope.context,
688-
this,
689-
item.field,
690-
holder.adapterPosition
691-
).show()
692-
}
693-
holder.binding.scope.alpha = HIGH_EMPHASIS_ALPHA
694-
} else {
695-
holder.binding.userInfoEditTextEdit.isEnabled = false
696-
holder.binding.userInfoEditTextEdit.isFocusableInTouchMode = false
697-
holder.binding.userInfoEditTextEdit.isEnabled = false
698-
holder.binding.userInfoEditTextEdit.isCursorVisible = false
699-
holder.binding.scope.setOnClickListener(null)
700-
holder.binding.scope.alpha = MEDIUM_EMPHASIS_ALPHA
701-
}
702-
} else {
703-
holder.binding.userInfoDetailContainer.visibility = View.GONE
668+
val colorScheme = viewThemeUtils.getColorScheme(profileActivity)
669+
val itemPosition = when (position) {
670+
0 -> UserInfoDetailItemPosition.FIRST
671+
filteredDisplayList.size - 1 -> UserInfoDetailItemPosition.LAST
672+
else -> UserInfoDetailItemPosition.MIDDLE
704673
}
705-
}
706-
707-
private fun initUserInfoEditText(holder: ViewHolder, item: UserInfoDetailsItem) {
708-
holder.binding.userInfoEditTextEdit.setText(item.text)
709-
holder.binding.userInfoInputLayout.hint = item.hint
710-
holder.binding.userInfoEditTextEdit.addTextChangedListener(object : TextWatcher {
711-
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
712-
// unused atm
713-
}
714674

715-
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
716-
if (profileActivity.edit) {
717-
displayList!![holder.adapterPosition].text = holder.binding.userInfoEditTextEdit.text.toString()
718-
} else {
719-
filteredDisplayList[holder.adapterPosition].text =
720-
holder.binding.userInfoEditTextEdit.text.toString()
675+
if (profileActivity.edit) {
676+
val itemData = UserInfoDetailItemData(
677+
icon = item.icon,
678+
text = item.text.orEmpty(),
679+
hint = item.hint,
680+
scope = item.scope
681+
)
682+
val listeners = UserInfoDetailListeners(
683+
onTextChange = { newText ->
684+
if (profileActivity.edit) {
685+
displayList!![position].text = newText
686+
} else {
687+
filteredDisplayList[position].text = newText
688+
}
689+
},
690+
onScopeClick = { ScopeDialog(profileActivity, this, item.field, position).show() }
691+
)
692+
holder.composeView.setContent {
693+
MaterialTheme(colorScheme = colorScheme) {
694+
UserInfoDetailItemEditable(
695+
data = itemData,
696+
listeners = listeners,
697+
position = itemPosition,
698+
enabled = profileActivity.editableFields.contains(item.field.toString().lowercase())
699+
)
721700
}
722701
}
723-
724-
override fun afterTextChanged(s: Editable) {
725-
// unused atm
726-
}
727-
})
728-
}
729-
730-
private fun initScopeElements(item: UserInfoDetailsItem, holder: ViewHolder) {
731-
if (item.scope == null) {
732-
holder.binding.scope.visibility = View.GONE
733702
} else {
734-
holder.binding.scope.visibility = View.VISIBLE
735-
when (item.scope) {
736-
Scope.PRIVATE -> holder.binding.scope.setImageResource(R.drawable.ic_cellphone)
737-
Scope.LOCAL -> holder.binding.scope.setImageResource(R.drawable.ic_password)
738-
Scope.FEDERATED -> holder.binding.scope.setImageResource(R.drawable.ic_contacts)
739-
Scope.PUBLISHED -> holder.binding.scope.setImageResource(R.drawable.ic_link)
740-
null -> {
741-
// nothing
703+
val displayText = when (item.field) {
704+
Field.WEBSITE -> DisplayUtils.beautifyURL(item.text)
705+
Field.TWITTER -> DisplayUtils.beautifyTwitterHandle(item.text)
706+
else -> item.text.orEmpty()
707+
}
708+
holder.composeView.setContent {
709+
MaterialTheme(colorScheme = colorScheme) {
710+
UserInfoDetailItemViewOnly(
711+
userInfo = UserInfoDetailItemData(
712+
icon = item.icon,
713+
text = displayText,
714+
hint = item.hint,
715+
scope = item.scope
716+
),
717+
position = itemPosition,
718+
ellipsize = Field.EMAIL == item.field
719+
)
742720
}
743721
}
744-
holder.binding.scope.contentDescription = holder.binding.scope.context.getString(
745-
R.string.scope_toggle_description,
746-
item.hint
747-
)
748722
}
749723
}
750724

@@ -774,7 +748,5 @@ class ProfileActivity : BaseActivity() {
774748
private val TAG = ProfileActivity::class.java.simpleName
775749
private const val DEFAULT_CACHE_SIZE: Int = 20
776750
private const val DEFAULT_RETRIES: Long = 3
777-
private const val HIGH_EMPHASIS_ALPHA: Float = 0.87f
778-
private const val MEDIUM_EMPHASIS_ALPHA: Float = 0.6f
779751
}
780752
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
package com.nextcloud.talk.profile
8+
9+
import androidx.annotation.DrawableRes
10+
import com.nextcloud.talk.models.json.userprofile.Scope
11+
12+
data class UserInfoDetailItemData(
13+
@param:DrawableRes val icon: Int,
14+
val text: String,
15+
val hint: String,
16+
val scope: Scope?
17+
)
18+
19+
data class UserInfoDetailListeners(val onTextChange: (String) -> Unit, val onScopeClick: (() -> Unit)?)

0 commit comments

Comments
 (0)