Skip to content

Commit 17e485f

Browse files
authored
Merge pull request #4826 from vector-im/feature/bma/nick_color_final
Nick color
2 parents fd854a6 + 02a8fd2 commit 17e485f

File tree

12 files changed

+224
-8
lines changed

12 files changed

+224
-8
lines changed

changelog.d/2614.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow changing nick colors from the member detail screen

matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt

+1
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ object UserAccountDataTypes {
2727
const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets"
2828
const val TYPE_IDENTITY_SERVER = "m.identity_server"
2929
const val TYPE_ACCEPTED_TERMS = "m.accepted_terms"
30+
const val TYPE_OVERRIDE_COLORS = "im.vector.setting.override_colors"
3031
}

vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt

+6
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import im.vector.app.features.home.HomeDetailViewModel
4242
import im.vector.app.features.home.PromoteRestrictedViewModel
4343
import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel
4444
import im.vector.app.features.home.UnreadMessagesSharedViewModel
45+
import im.vector.app.features.home.UserColorAccountDataViewModel
4546
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
4647
import im.vector.app.features.home.room.detail.RoomDetailViewModel
4748
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
@@ -412,6 +413,11 @@ interface MavericksViewModelModule {
412413
@MavericksViewModelKey(RoomMemberProfileViewModel::class)
413414
fun roomMemberProfileViewModelFactory(factory: RoomMemberProfileViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
414415

416+
@Binds
417+
@IntoMap
418+
@MavericksViewModelKey(UserColorAccountDataViewModel::class)
419+
fun userColorAccountDataViewModelFactory(factory: UserColorAccountDataViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
420+
415421
@Binds
416422
@IntoMap
417423
@MavericksViewModelKey(RoomPreviewViewModel::class)

vector/src/main/java/im/vector/app/features/home/HomeActivity.kt

+2
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ class HomeActivity :
106106
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
107107
@Suppress("UNUSED")
108108
private val analyticsAccountDataViewModel: AnalyticsAccountDataViewModel by viewModel()
109+
@Suppress("UNUSED")
110+
private val userColorAccountDataViewModel: UserColorAccountDataViewModel by viewModel()
109111

110112
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
111113
private val promoteRestrictedViewModel: PromoteRestrictedViewModel by viewModel()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) 2021 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.home
18+
19+
import com.airbnb.mvrx.MavericksState
20+
import com.airbnb.mvrx.MavericksViewModelFactory
21+
import dagger.assisted.Assisted
22+
import dagger.assisted.AssistedFactory
23+
import dagger.assisted.AssistedInject
24+
import im.vector.app.core.di.MavericksAssistedViewModelFactory
25+
import im.vector.app.core.di.hiltMavericksViewModelFactory
26+
import im.vector.app.core.platform.EmptyAction
27+
import im.vector.app.core.platform.EmptyViewEvents
28+
import im.vector.app.core.platform.VectorViewModel
29+
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
30+
import kotlinx.coroutines.flow.launchIn
31+
import kotlinx.coroutines.flow.map
32+
import kotlinx.coroutines.flow.onEach
33+
import org.matrix.android.sdk.api.session.Session
34+
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
35+
import org.matrix.android.sdk.api.session.events.model.toModel
36+
import org.matrix.android.sdk.flow.flow
37+
import org.matrix.android.sdk.flow.unwrap
38+
import timber.log.Timber
39+
40+
data class DummyState(
41+
val dummy: Boolean = false
42+
) : MavericksState
43+
44+
class UserColorAccountDataViewModel @AssistedInject constructor(
45+
@Assisted initialState: DummyState,
46+
private val session: Session,
47+
private val matrixItemColorProvider: MatrixItemColorProvider
48+
) : VectorViewModel<DummyState, EmptyAction, EmptyViewEvents>(initialState) {
49+
50+
@AssistedFactory
51+
interface Factory : MavericksAssistedViewModelFactory<UserColorAccountDataViewModel, DummyState> {
52+
override fun create(initialState: DummyState): UserColorAccountDataViewModel
53+
}
54+
55+
companion object : MavericksViewModelFactory<UserColorAccountDataViewModel, DummyState> by hiltMavericksViewModelFactory()
56+
57+
init {
58+
observeAccountData()
59+
}
60+
61+
private fun observeAccountData() {
62+
session.flow()
63+
.liveUserAccountData(UserAccountDataTypes.TYPE_OVERRIDE_COLORS)
64+
.unwrap()
65+
.map { it.content.toModel<Map<String, String>>() }
66+
.onEach { userColorAccountDataContent ->
67+
if (userColorAccountDataContent == null) {
68+
Timber.w("Invalid account data im.vector.setting.override_colors")
69+
}
70+
matrixItemColorProvider.setOverrideColors(userColorAccountDataContent)
71+
}
72+
.launchIn(viewModelScope)
73+
}
74+
75+
override fun handle(action: EmptyAction) {
76+
// No op
77+
}
78+
}

vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt

+44-1
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616

1717
package im.vector.app.features.home.room.detail.timeline.helper
1818

19+
import android.graphics.Color
1920
import androidx.annotation.ColorInt
2021
import androidx.annotation.ColorRes
2122
import androidx.annotation.VisibleForTesting
2223
import im.vector.app.R
2324
import im.vector.app.core.resources.ColorProvider
2425
import org.matrix.android.sdk.api.util.MatrixItem
26+
import timber.log.Timber
2527
import javax.inject.Inject
2628
import javax.inject.Singleton
2729
import kotlin.math.abs
@@ -44,6 +46,42 @@ class MatrixItemColorProvider @Inject constructor(
4446
}
4547
}
4648

49+
fun setOverrideColors(overrideColors: Map<String, String>?) {
50+
cache.clear()
51+
overrideColors?.forEach {
52+
setOverrideColor(it.key, it.value)
53+
}
54+
}
55+
56+
fun setOverrideColor(id: String, colorSpec: String?): Boolean {
57+
val color = parseUserColorSpec(colorSpec)
58+
return if (color == null) {
59+
cache.remove(id)
60+
false
61+
} else {
62+
cache[id] = color
63+
true
64+
}
65+
}
66+
67+
@ColorInt
68+
private fun parseUserColorSpec(colorText: String?): Int? {
69+
return if (colorText.isNullOrBlank()) {
70+
null
71+
} else {
72+
try {
73+
if (colorText.length == 1) {
74+
colorProvider.getColor(getUserColorByIndex(colorText.toInt()))
75+
} else {
76+
Color.parseColor(colorText)
77+
}
78+
} catch (e: Throwable) {
79+
Timber.e(e, "Unable to parse color $colorText")
80+
null
81+
}
82+
}
83+
}
84+
4785
companion object {
4886
@ColorRes
4987
@VisibleForTesting
@@ -52,7 +90,12 @@ class MatrixItemColorProvider @Inject constructor(
5290

5391
userId?.toList()?.map { chr -> hash = (hash shl 5) - hash + chr.code }
5492

55-
return when (abs(hash) % 8) {
93+
return getUserColorByIndex(abs(hash))
94+
}
95+
96+
@ColorRes
97+
private fun getUserColorByIndex(index: Int): Int {
98+
return when (index % 8) {
5699
1 -> R.color.element_name_02
57100
2 -> R.color.element_name_03
58101
3 -> R.color.element_name_04

vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt

+1
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ sealed class RoomMemberProfileAction : VectorViewModelAction {
2828
object VerifyUser : RoomMemberProfileAction()
2929
object ShareRoomMemberProfile : RoomMemberProfileAction()
3030
data class SetPowerLevel(val previousValue: Int, val newValue: Int, val askForValidation: Boolean) : RoomMemberProfileAction()
31+
data class SetUserColorOverride(val newColorSpec: String) : RoomMemberProfileAction()
3132
}

vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt

+12-2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class RoomMemberProfileController @Inject constructor(
4242
fun onShowDeviceList()
4343
fun onShowDeviceListNoCrossSigning()
4444
fun onOpenDmClicked()
45+
fun onOverrideColorClicked()
4546
fun onJumpToReadReceiptClicked()
4647
fun onMentionClicked()
4748
fun onEditPowerLevel(currentRole: Role)
@@ -171,11 +172,20 @@ class RoomMemberProfileController @Inject constructor(
171172

172173
private fun buildMoreSection(state: RoomMemberProfileViewState) {
173174
// More
175+
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
176+
177+
buildProfileAction(
178+
id = "overrideColor",
179+
editable = false,
180+
title = stringProvider.getString(R.string.room_member_override_nick_color),
181+
subtitle = state.userColorOverride,
182+
divider = !state.isMine,
183+
action = { callback?.onOverrideColorClicked() }
184+
)
185+
174186
if (!state.isMine) {
175187
val membership = state.asyncMembership() ?: return
176188

177-
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
178-
179189
buildProfileAction(
180190
id = "direct",
181191
editable = false,

vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt

+25-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import im.vector.app.core.extensions.setTextOrHide
4343
import im.vector.app.core.platform.StateView
4444
import im.vector.app.core.platform.VectorBaseFragment
4545
import im.vector.app.core.utils.startSharePlainTextIntent
46+
import im.vector.app.databinding.DialogBaseEditTextBinding
4647
import im.vector.app.databinding.DialogShareQrCodeBinding
4748
import im.vector.app.databinding.FragmentMatrixProfileBinding
4849
import im.vector.app.databinding.ViewStubRoomMemberProfileHeaderBinding
@@ -51,6 +52,7 @@ import im.vector.app.features.displayname.getBestName
5152
import im.vector.app.features.home.AvatarRenderer
5253
import im.vector.app.features.home.room.detail.RoomDetailPendingAction
5354
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
55+
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
5456
import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet
5557
import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs
5658
import kotlinx.parcelize.Parcelize
@@ -68,7 +70,8 @@ data class RoomMemberProfileArgs(
6870
class RoomMemberProfileFragment @Inject constructor(
6971
private val roomMemberProfileController: RoomMemberProfileController,
7072
private val avatarRenderer: AvatarRenderer,
71-
private val roomDetailPendingActionStore: RoomDetailPendingActionStore
73+
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
74+
private val matrixItemColorProvider: MatrixItemColorProvider
7275
) : VectorBaseFragment<FragmentMatrixProfileBinding>(),
7376
RoomMemberProfileController.Callback {
7477

@@ -200,6 +203,7 @@ class RoomMemberProfileFragment @Inject constructor(
200203
headerViews.memberProfileIdView.text = userMatrixItem.id
201204
val bestName = userMatrixItem.getBestName()
202205
headerViews.memberProfileNameView.text = bestName
206+
headerViews.memberProfileNameView.setTextColor(matrixItemColorProvider.getColor(userMatrixItem))
203207
views.matrixProfileToolbarTitleView.text = bestName
204208
avatarRenderer.render(userMatrixItem, headerViews.memberProfileAvatarView)
205209
avatarRenderer.render(userMatrixItem, views.matrixProfileToolbarAvatarImageView)
@@ -321,6 +325,26 @@ class RoomMemberProfileFragment @Inject constructor(
321325
navigator.openBigImageViewer(requireActivity(), view, userMatrixItem)
322326
}
323327

328+
override fun onOverrideColorClicked(): Unit = withState(viewModel) { state ->
329+
val inflater = requireActivity().layoutInflater
330+
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
331+
val views = DialogBaseEditTextBinding.bind(layout)
332+
views.editText.setText(state.userColorOverride)
333+
views.editText.hint = "#000000"
334+
335+
MaterialAlertDialogBuilder(requireContext())
336+
.setTitle(R.string.room_member_override_nick_color)
337+
.setView(layout)
338+
.setPositiveButton(R.string.ok) { _, _ ->
339+
val newColor = views.editText.text.toString()
340+
if (newColor != state.userColorOverride) {
341+
viewModel.handle(RoomMemberProfileAction.SetUserColorOverride(newColor))
342+
}
343+
}
344+
.setNegativeButton(R.string.action_cancel, null)
345+
.show()
346+
}
347+
324348
override fun onEditPowerLevel(currentRole: Role) {
325349
EditPowerLevelDialogs.showChoice(requireActivity(), R.string.power_level_edit_title, currentRole) { newPowerLevel ->
326350
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(currentRole.value, newPowerLevel, true))

vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt

+51-4
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ import dagger.assisted.AssistedInject
2828
import im.vector.app.R
2929
import im.vector.app.core.di.MavericksAssistedViewModelFactory
3030
import im.vector.app.core.di.hiltMavericksViewModelFactory
31+
import im.vector.app.core.extensions.exhaustive
3132
import im.vector.app.core.mvrx.runCatchingToAsync
3233
import im.vector.app.core.platform.VectorViewModel
3334
import im.vector.app.core.resources.StringProvider
3435
import im.vector.app.features.displayname.getBestName
36+
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
3537
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
3638
import kotlinx.coroutines.Dispatchers
3739
import kotlinx.coroutines.flow.combine
@@ -42,8 +44,10 @@ import kotlinx.coroutines.launch
4244
import kotlinx.coroutines.withContext
4345
import org.matrix.android.sdk.api.query.QueryStringValue
4446
import org.matrix.android.sdk.api.session.Session
47+
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
4548
import org.matrix.android.sdk.api.session.events.model.EventType
4649
import org.matrix.android.sdk.api.session.events.model.toContent
50+
import org.matrix.android.sdk.api.session.events.model.toModel
4751
import org.matrix.android.sdk.api.session.profile.ProfileService
4852
import org.matrix.android.sdk.api.session.room.Room
4953
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
@@ -57,10 +61,12 @@ import org.matrix.android.sdk.api.util.toOptional
5761
import org.matrix.android.sdk.flow.flow
5862
import org.matrix.android.sdk.flow.unwrap
5963

60-
class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomMemberProfileViewState,
61-
private val stringProvider: StringProvider,
62-
private val session: Session) :
63-
VectorViewModel<RoomMemberProfileViewState, RoomMemberProfileAction, RoomMemberProfileViewEvents>(initialState) {
64+
class RoomMemberProfileViewModel @AssistedInject constructor(
65+
@Assisted private val initialState: RoomMemberProfileViewState,
66+
private val stringProvider: StringProvider,
67+
private val matrixItemColorProvider: MatrixItemColorProvider,
68+
private val session: Session
69+
) : VectorViewModel<RoomMemberProfileViewState, RoomMemberProfileAction, RoomMemberProfileViewEvents>(initialState) {
6470

6571
@AssistedFactory
6672
interface Factory : MavericksAssistedViewModelFactory<RoomMemberProfileViewModel, RoomMemberProfileViewState> {
@@ -85,6 +91,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
8591
)
8692
}
8793
observeIgnoredState()
94+
observeAccountData()
8895
viewModelScope.launch(Dispatchers.Main) {
8996
// Do we have a room member for this id.
9097
val roomMember = withContext(Dispatchers.Default) {
@@ -121,6 +128,21 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
121128
}
122129
}
123130

131+
private fun observeAccountData() {
132+
session.flow()
133+
.liveUserAccountData(UserAccountDataTypes.TYPE_OVERRIDE_COLORS)
134+
.unwrap()
135+
.onEach {
136+
val newUserColor = it.content.toModel<Map<String, String>>()?.get(initialState.userId)
137+
setState {
138+
copy(
139+
userColorOverride = newUserColor
140+
)
141+
}
142+
}
143+
.launchIn(viewModelScope)
144+
}
145+
124146
private fun observeIgnoredState() {
125147
session.flow().liveIgnoredUsers()
126148
.map { ignored ->
@@ -143,6 +165,31 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
143165
is RoomMemberProfileAction.BanOrUnbanUser -> handleBanOrUnbanAction(action)
144166
is RoomMemberProfileAction.KickUser -> handleKickAction(action)
145167
RoomMemberProfileAction.InviteUser -> handleInviteAction()
168+
is RoomMemberProfileAction.SetUserColorOverride -> handleSetUserColorOverride(action)
169+
}.exhaustive
170+
}
171+
172+
private fun handleSetUserColorOverride(action: RoomMemberProfileAction.SetUserColorOverride) {
173+
val newOverrideColorSpecs = session.accountDataService()
174+
.getUserAccountDataEvent(UserAccountDataTypes.TYPE_OVERRIDE_COLORS)
175+
?.content
176+
?.toModel<Map<String, String>>()
177+
.orEmpty()
178+
.toMutableMap()
179+
if (matrixItemColorProvider.setOverrideColor(initialState.userId, action.newColorSpec)) {
180+
newOverrideColorSpecs[initialState.userId] = action.newColorSpec
181+
} else {
182+
newOverrideColorSpecs.remove(initialState.userId)
183+
}
184+
viewModelScope.launch {
185+
try {
186+
session.accountDataService().updateUserAccountData(
187+
type = UserAccountDataTypes.TYPE_OVERRIDE_COLORS,
188+
content = newOverrideColorSpecs
189+
)
190+
} catch (failure: Throwable) {
191+
_viewEvents.post(RoomMemberProfileViewEvents.Failure(failure))
192+
}
146193
}
147194
}
148195

vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ data class RoomMemberProfileViewState(
4141
val allDevicesAreCrossSignedTrusted: Boolean = false,
4242
val asyncMembership: Async<Membership> = Uninitialized,
4343
val hasReadReceipt: Boolean = false,
44+
val userColorOverride: String? = null,
4445
val actionPermissions: ActionPermissions = ActionPermissions()
4546
) : MavericksState {
4647

0 commit comments

Comments
 (0)