Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7aaebd4
[issue-2610] implement setting to override nick color
mitchnull Jan 3, 2021
889a9a1
[issue-2610] fix NoSuchElement error
mitchnull Jan 3, 2021
0aee15f
[issue-2610] Merge branch 'develop' of github.com:mitchnull/element-a…
mitchnull Jan 20, 2021
0e400dc
[issue-2610] Add Override Color menu item under More...
mitchnull Feb 3, 2021
0109cde
[issue-2610] Merge branch 'develop' of https://github.com/vector-im/e…
mitchnull Feb 3, 2021
f7d8127
[issue-2610] remove extra semicolon
mitchnull Feb 3, 2021
cc15f9b
[issue-2610] remove click handler from display-name
mitchnull Feb 6, 2021
db97046
[issue-2610] change menu text to "Override nick color"
mitchnull Feb 13, 2021
da10364
[issue-2610] Merge branch 'develop' of https://github.com/vector-im/e…
mitchnull Feb 13, 2021
6c26cfc
[issue-2610] Merge branch 'develop' of https://github.com/vector-im/e…
mitchnull Feb 16, 2021
ea01677
[issue-2610] Merge branch 'develop' of https://github.com/vector-im/e…
mitchnull Feb 18, 2021
1ec0956
[issue-2610] Merge branch 'develop' of https://github.com/vector-im/e…
mitchnull Feb 20, 2021
1019ffe
[issue-2610-override-nick-color-via-user-account-data] [issue-2610] M…
mitchnull Apr 12, 2021
40d48cc
[issue-2610-override-nick-color-via-user-account-data] Merge branch '…
mitchnull May 9, 2021
79a3be3
[issue-2610] Merge branch 'develop' of github.com:mitchnull/element-a…
mitchnull May 19, 2021
bf919b8
[issue-2610] Merge branch 'develop' of github.com:mitchnull/element-a…
mitchnull Aug 15, 2021
454baf8
Merge branch 'develop' into feature/issue-2610-override-nick-color-vi…
bmarty Dec 31, 2021
7ce9ece
Merge pull request #2614 from mitchnull/feature/issue-2610-override-n…
bmarty Dec 31, 2021
1d3cc52
Changelog new management
bmarty Dec 31, 2021
364457d
Move logic to dedicated ViewModel
bmarty Dec 31, 2021
07d2a15
Code cleanup
bmarty Dec 31, 2021
ddadefd
Move logic to ViewModel
bmarty Dec 31, 2021
1cb91ca
Use color parser
bmarty Dec 31, 2021
a7b72ed
Fix latest small bugs
bmarty Dec 31, 2021
6d8b5db
Fix latest small bugs
bmarty Dec 31, 2021
96d5652
Small cleanup
bmarty Dec 31, 2021
51c9c2f
Optimize call flow
bmarty Jan 5, 2022
608e01a
Merge branch 'develop' into feature/bma/nick_color_final
bmarty Jan 5, 2022
02a8fd2
Fix compilation issue after develop being merged.
bmarty Jan 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2614.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow changing nick colors from the member detail screen
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ object UserAccountDataTypes {
const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets"
const val TYPE_IDENTITY_SERVER = "m.identity_server"
const val TYPE_ACCEPTED_TERMS = "m.accepted_terms"
const val TYPE_OVERRIDE_COLORS = "im.vector.setting.override_colors"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this type need to be added to the matrix spec somewhere?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have another PR that implements the same functionality in element-desktop (matrix-org/matrix-react-sdk#5626). As far as I understand keys under im.vector.setting are free-to-use by clients, so I don't think it's absolutely necessary to include it in the matrix spec (apart from maybe documenting its existence so that other clients can pick this up if they wish)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right, using domain prefixed type is fine. Nowadays we should maybe consider using io.element.prefix.
More details here: https://spec.matrix.org/latest/#namespacing

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import im.vector.app.features.home.HomeDetailViewModel
import im.vector.app.features.home.PromoteRestrictedViewModel
import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel
import im.vector.app.features.home.UnreadMessagesSharedViewModel
import im.vector.app.features.home.UserColorAccountDataViewModel
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
import im.vector.app.features.home.room.detail.RoomDetailViewModel
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
Expand Down Expand Up @@ -412,6 +413,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(RoomMemberProfileViewModel::class)
fun roomMemberProfileViewModelFactory(factory: RoomMemberProfileViewModel.Factory): MavericksAssistedViewModelFactory<*, *>

@Binds
@IntoMap
@MavericksViewModelKey(UserColorAccountDataViewModel::class)
fun userColorAccountDataViewModelFactory(factory: UserColorAccountDataViewModel.Factory): MavericksAssistedViewModelFactory<*, *>

@Binds
@IntoMap
@MavericksViewModelKey(RoomPreviewViewModel::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ class HomeActivity :
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@Suppress("UNUSED")
private val analyticsAccountDataViewModel: AnalyticsAccountDataViewModel by viewModel()
@Suppress("UNUSED")
private val userColorAccountDataViewModel: UserColorAccountDataViewModel by viewModel()

private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
private val promoteRestrictedViewModel: PromoteRestrictedViewModel by viewModel()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.app.features.home

import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap
import timber.log.Timber

data class DummyState(
val dummy: Boolean = false
) : MavericksState

class UserColorAccountDataViewModel @AssistedInject constructor(
@Assisted initialState: DummyState,
private val session: Session,
private val matrixItemColorProvider: MatrixItemColorProvider
) : VectorViewModel<DummyState, EmptyAction, EmptyViewEvents>(initialState) {

@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<UserColorAccountDataViewModel, DummyState> {
override fun create(initialState: DummyState): UserColorAccountDataViewModel
}

companion object : MavericksViewModelFactory<UserColorAccountDataViewModel, DummyState> by hiltMavericksViewModelFactory()

init {
observeAccountData()
}

private fun observeAccountData() {
session.flow()
.liveUserAccountData(UserAccountDataTypes.TYPE_OVERRIDE_COLORS)
.unwrap()
.map { it.content.toModel<Map<String, String>>() }
.onEach { userColorAccountDataContent ->
if (userColorAccountDataContent == null) {
Timber.w("Invalid account data im.vector.setting.override_colors")
}
matrixItemColorProvider.setOverrideColors(userColorAccountDataContent)
}
.launchIn(viewModelScope)
}

override fun handle(action: EmptyAction) {
// No op
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

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

import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.VisibleForTesting
import im.vector.app.R
import im.vector.app.core.resources.ColorProvider
import org.matrix.android.sdk.api.util.MatrixItem
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.abs
Expand All @@ -44,6 +46,42 @@ class MatrixItemColorProvider @Inject constructor(
}
}

fun setOverrideColors(overrideColors: Map<String, String>?) {
cache.clear()
overrideColors?.forEach {
setOverrideColor(it.key, it.value)
}
}

fun setOverrideColor(id: String, colorSpec: String?): Boolean {
val color = parseUserColorSpec(colorSpec)
return if (color == null) {
cache.remove(id)
false
} else {
cache[id] = color
true
}
}

@ColorInt
private fun parseUserColorSpec(colorText: String?): Int? {
return if (colorText.isNullOrBlank()) {
null
} else {
try {
if (colorText.length == 1) {

@mitchnull mitchnull Jan 2, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will break if/when the palette is extended to 16 or more colors. Maybe it would be better to just try to parse it as int, and if that works then it's a color index (from the palette) otherwise parse with Color.parseColor. Another option would be to check if the text starts with a digit to select the palette version.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but the plan is to change this dialog to do something more user friendly, so it's acceptable like that to me.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, cool, thanks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

being able to set the colour by index is quite cryptic, I wouldn't have expected an input with a hex string hint to also allow a 0-9 index

something a UI iteration could help to improve

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is really a hidden feature. Sort of easter egg.
I see in my dream a nice dialog with all the 8 predefined colors, a color picker and a field to manually enter a html color. Even a reset button, instead of validating a empty dialog to reset the color.
Also note that now using the word "black" or any other color that Color.parse() understand will also work, not sure for the other non Android-Element Matrix clients compatibility though.

colorProvider.getColor(getUserColorByIndex(colorText.toInt()))
} else {
Color.parseColor(colorText)
}
} catch (e: Throwable) {
Timber.e(e, "Unable to parse color $colorText")
null
}
}
}

companion object {
@ColorRes
@VisibleForTesting
Expand All @@ -52,7 +90,12 @@ class MatrixItemColorProvider @Inject constructor(

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

return when (abs(hash) % 8) {
return getUserColorByIndex(abs(hash))
}

@ColorRes
private fun getUserColorByIndex(index: Int): Int {
return when (index % 8) {
1 -> R.color.element_name_02
2 -> R.color.element_name_03
3 -> R.color.element_name_04
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ sealed class RoomMemberProfileAction : VectorViewModelAction {
object VerifyUser : RoomMemberProfileAction()
object ShareRoomMemberProfile : RoomMemberProfileAction()
data class SetPowerLevel(val previousValue: Int, val newValue: Int, val askForValidation: Boolean) : RoomMemberProfileAction()
data class SetUserColorOverride(val newColorSpec: String) : RoomMemberProfileAction()
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class RoomMemberProfileController @Inject constructor(
fun onShowDeviceList()
fun onShowDeviceListNoCrossSigning()
fun onOpenDmClicked()
fun onOverrideColorClicked()
fun onJumpToReadReceiptClicked()
fun onMentionClicked()
fun onEditPowerLevel(currentRole: Role)
Expand Down Expand Up @@ -171,11 +172,20 @@ class RoomMemberProfileController @Inject constructor(

private fun buildMoreSection(state: RoomMemberProfileViewState) {
// More
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))

buildProfileAction(
id = "overrideColor",
editable = false,
title = stringProvider.getString(R.string.room_member_override_nick_color),
subtitle = state.userColorOverride,
divider = !state.isMine,
action = { callback?.onOverrideColorClicked() }
)

if (!state.isMine) {
val membership = state.asyncMembership() ?: return

buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))

buildProfileAction(
id = "direct",
editable = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.databinding.DialogBaseEditTextBinding
import im.vector.app.databinding.DialogShareQrCodeBinding
import im.vector.app.databinding.FragmentMatrixProfileBinding
import im.vector.app.databinding.ViewStubRoomMemberProfileHeaderBinding
Expand All @@ -51,6 +52,7 @@ import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailPendingAction
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet
import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs
import kotlinx.parcelize.Parcelize
Expand All @@ -68,7 +70,8 @@ data class RoomMemberProfileArgs(
class RoomMemberProfileFragment @Inject constructor(
private val roomMemberProfileController: RoomMemberProfileController,
private val avatarRenderer: AvatarRenderer,
private val roomDetailPendingActionStore: RoomDetailPendingActionStore
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
private val matrixItemColorProvider: MatrixItemColorProvider
) : VectorBaseFragment<FragmentMatrixProfileBinding>(),
RoomMemberProfileController.Callback {

Expand Down Expand Up @@ -200,6 +203,7 @@ class RoomMemberProfileFragment @Inject constructor(
headerViews.memberProfileIdView.text = userMatrixItem.id
val bestName = userMatrixItem.getBestName()
headerViews.memberProfileNameView.text = bestName
headerViews.memberProfileNameView.setTextColor(matrixItemColorProvider.getColor(userMatrixItem))
views.matrixProfileToolbarTitleView.text = bestName
avatarRenderer.render(userMatrixItem, headerViews.memberProfileAvatarView)
avatarRenderer.render(userMatrixItem, views.matrixProfileToolbarAvatarImageView)
Expand Down Expand Up @@ -321,6 +325,26 @@ class RoomMemberProfileFragment @Inject constructor(
navigator.openBigImageViewer(requireActivity(), view, userMatrixItem)
}

override fun onOverrideColorClicked(): Unit = withState(viewModel) { state ->
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val views = DialogBaseEditTextBinding.bind(layout)
views.editText.setText(state.userColorOverride)
views.editText.hint = "#000000"

MaterialAlertDialogBuilder(requireContext())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allowing the user to provide a hex string is quite verbose, perhaps another iteration of this could use a colour wheel UI

@mitchnull mitchnull Jan 5, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't include a full-fledged UI for this in my original PR because I didn't have the necessary expertise to do it quickly, and it wasn't clear that the PR / idea would be accepted at all (this is mostly a PoC and a working solution for us). I do agree that a proper color selector should be created for this (that can select a color from the current palette or select an arbitrary RGB color)

.setTitle(R.string.room_member_override_nick_color)
.setView(layout)
.setPositiveButton(R.string.ok) { _, _ ->
val newColor = views.editText.text.toString()
if (newColor != state.userColorOverride) {
viewModel.handle(RoomMemberProfileAction.SetUserColorOverride(newColor))
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}

override fun onEditPowerLevel(currentRole: Role) {
EditPowerLevelDialogs.showChoice(requireActivity(), R.string.power_level_edit_title, currentRole) { newPowerLevel ->
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(currentRole.value, newPowerLevel, true))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
Expand All @@ -42,8 +44,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
Expand All @@ -57,10 +61,12 @@ import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap

class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomMemberProfileViewState,
private val stringProvider: StringProvider,
private val session: Session) :
VectorViewModel<RoomMemberProfileViewState, RoomMemberProfileAction, RoomMemberProfileViewEvents>(initialState) {
class RoomMemberProfileViewModel @AssistedInject constructor(
@Assisted private val initialState: RoomMemberProfileViewState,
private val stringProvider: StringProvider,
private val matrixItemColorProvider: MatrixItemColorProvider,
private val session: Session
) : VectorViewModel<RoomMemberProfileViewState, RoomMemberProfileAction, RoomMemberProfileViewEvents>(initialState) {

@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<RoomMemberProfileViewModel, RoomMemberProfileViewState> {
Expand All @@ -85,6 +91,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
)
}
observeIgnoredState()
observeAccountData()
viewModelScope.launch(Dispatchers.Main) {
// Do we have a room member for this id.
val roomMember = withContext(Dispatchers.Default) {
Expand Down Expand Up @@ -121,6 +128,21 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
}
}

private fun observeAccountData() {
session.flow()
.liveUserAccountData(UserAccountDataTypes.TYPE_OVERRIDE_COLORS)
.unwrap()
.onEach {
val newUserColor = it.content.toModel<Map<String, String>>()?.get(initialState.userId)
setState {
copy(
userColorOverride = newUserColor
)
}
}
.launchIn(viewModelScope)
}

private fun observeIgnoredState() {
session.flow().liveIgnoredUsers()
.map { ignored ->
Expand All @@ -143,6 +165,31 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
is RoomMemberProfileAction.BanOrUnbanUser -> handleBanOrUnbanAction(action)
is RoomMemberProfileAction.KickUser -> handleKickAction(action)
RoomMemberProfileAction.InviteUser -> handleInviteAction()
is RoomMemberProfileAction.SetUserColorOverride -> handleSetUserColorOverride(action)
}.exhaustive
}

private fun handleSetUserColorOverride(action: RoomMemberProfileAction.SetUserColorOverride) {
val newOverrideColorSpecs = session.accountDataService()
.getUserAccountDataEvent(UserAccountDataTypes.TYPE_OVERRIDE_COLORS)
?.content
?.toModel<Map<String, String>>()
.orEmpty()
.toMutableMap()
if (matrixItemColorProvider.setOverrideColor(initialState.userId, action.newColorSpec)) {
newOverrideColorSpecs[initialState.userId] = action.newColorSpec
} else {
newOverrideColorSpecs.remove(initialState.userId)
}
viewModelScope.launch {
try {
session.accountDataService().updateUserAccountData(
type = UserAccountDataTypes.TYPE_OVERRIDE_COLORS,
content = newOverrideColorSpecs
)
} catch (failure: Throwable) {
_viewEvents.post(RoomMemberProfileViewEvents.Failure(failure))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ data class RoomMemberProfileViewState(
val allDevicesAreCrossSignedTrusted: Boolean = false,
val asyncMembership: Async<Membership> = Uninitialized,
val hasReadReceipt: Boolean = false,
val userColorOverride: String? = null,
val actionPermissions: ActionPermissions = ActionPermissions()
) : MavericksState {

Expand Down
Loading