Create direct room: start creating all the required stuff

This commit is contained in:
ganfra 2019-07-17 18:30:14 +02:00
parent bb3b5788ba
commit 838003b68a
21 changed files with 442 additions and 21 deletions

View File

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
@ -42,6 +43,10 @@ class RxSession(private val session: Session) {
return session.livePushers().asObservable().observeOn(Schedulers.computation())
}
fun liveUsers(): Observable<List<User>> {
return session.liveUsers().asObservable().observeOn(Schedulers.computation())
}
}
fun Session.rx(): RxSession {

View File

@ -36,6 +36,12 @@ interface UserService {
* @param userId the userId to look for.
* @return a Livedata of user with userId
*/
fun observeUser(userId: String): LiveData<User?>
fun liveUser(userId: String): LiveData<User?>
/**
* Observe a live list of users sorted alphabetically
* @return a Livedata of users
*/
fun liveUsers(): LiveData<List<User>>
}

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.model.UserEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.fetchCopied
@ -33,12 +34,12 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
override fun getUser(userId: String): User? {
val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() }
?: return null
?: return null
return userEntity.asDomain()
}
override fun observeUser(userId: String): LiveData<User?> {
override fun liveUser(userId: String): LiveData<User?> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
UserEntity.where(realm, userId)
}
@ -48,4 +49,13 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
.firstOrNull()
}
}
override fun liveUsers(): LiveData<List<User>> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
realm.where(UserEntity::class.java).sort(UserEntityFields.DISPLAY_NAME)
}
return Transformations.map(liveRealmData) { results ->
results.map { it.asDomain() }
}
}
}

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.util
import im.vector.matrix.android.api.MatrixPatterns
import timber.log.Timber
/**
@ -49,3 +50,10 @@ fun convertFromUTF8(s: String): String? {
null
}
}
fun String?.firstLetterOfDisplayName(): String {
if (this.isNullOrEmpty()) return ""
val isUserId = MatrixPatterns.isUserId(this)
val firstLetterIndex = if (isUserId) 1 else 0
return this[firstLetterIndex].toString().toUpperCase()
}

View File

@ -64,6 +64,7 @@
<activity android:name=".features.home.room.filtered.FilteredRoomsActivity" />
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
<activity android:name=".features.debug.DebugMenuActivity" />
<activity android:name=".features.home.createdirect.CreateDirectRoomActivity" />
<!-- Services -->

View File

@ -36,6 +36,7 @@ import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.home.HomeDetailFragment
import im.vector.riotx.features.home.HomeDrawerFragment
import im.vector.riotx.features.home.HomeModule
import im.vector.riotx.features.home.createdirect.CreateDirectRoomFragment
import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.detail.timeline.action.*
@ -153,6 +154,8 @@ interface ScreenComponent {
fun inject(pushGatewaysFragment: PushGatewaysFragment)
fun inject(createDirectRoomFragment: CreateDirectRoomFragment)
@Component.Factory
interface Factory {
fun create(vectorComponent: VectorComponent,

View File

@ -30,6 +30,8 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsVie
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
import im.vector.riotx.features.home.*
import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel
import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory
import im.vector.riotx.features.home.group.GroupListViewModel
import im.vector.riotx.features.home.group.GroupListViewModel_AssistedFactory
import im.vector.riotx.features.home.room.detail.RoomDetailViewModel
@ -158,6 +160,9 @@ interface ViewModelModule {
@Binds
fun bindCreateRoomViewModelFactory(factory: CreateRoomViewModel_AssistedFactory): CreateRoomViewModel.Factory
@Binds
fun bindCreateDirectRoomViewModelFactory(factory: CreateDirectRoomViewModel_AssistedFactory): CreateDirectRoomViewModel.Factory
@Binds
fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory

View File

@ -26,10 +26,10 @@ import com.amulyakhare.textdrawable.TextDrawable
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.target.Target
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp
@ -41,7 +41,7 @@ import javax.inject.Inject
* This helper centralise ways to retrieve avatar into ImageView or even generic Target<Drawable>
*/
class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){
class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) {
companion object {
private const val THUMBNAIL_SIZE = 250
@ -92,9 +92,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
return if (text.isEmpty()) {
TextDrawable.builder().buildRound("", avatarColor)
} else {
val isUserId = MatrixPatterns.isUserId(text)
val firstLetterIndex = if (isUserId) 1 else 0
val firstLetter = text[firstLetterIndex].toString().toUpperCase()
val firstLetter = text.firstLetterOfDisplayName()
TextDrawable.builder()
.beginConfig()
.bold()

View File

@ -73,21 +73,21 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
.subscribe { list ->
list.let { summaries ->
val peopleNotifications = summaries
.filter { it.isDirect }
.map { it.notificationCount }
.takeIf { it.isNotEmpty() }
?.sumBy { i -> i }
?: 0
.filter { it.isDirect }
.map { it.notificationCount }
.takeIf { it.isNotEmpty() }
?.sumBy { i -> i }
?: 0
val peopleHasHighlight = summaries
.filter { it.isDirect }
.any { it.highlightCount > 0 }
val roomsNotifications = summaries
.filter { !it.isDirect }
.map { it.notificationCount }
.takeIf { it.isNotEmpty() }
?.sumBy { i -> i }
?: 0
.filter { !it.isDirect }
.map { it.notificationCount }
.takeIf { it.isNotEmpty() }
?.sumBy { i -> i }
?: 0
val roomsHasHighlight = summaries
.filter { !it.isDirect }
.any { it.highlightCount > 0 }

View File

@ -52,7 +52,7 @@ class HomeDrawerFragment : VectorBaseFragment() {
replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer)
}
session.observeUser(session.sessionParams.credentials.userId).observeK(this) { user ->
session.liveUser(session.sessionParams.credentials.userId).observeK(this) { user ->
if (user != null) {
avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
homeDrawerUsernameView.text = user.displayName

View File

@ -0,0 +1,44 @@
/*
*
* * Copyright 2019 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.riotx.features.home.createdirect
import android.content.Context
import android.content.Intent
import im.vector.riotx.R
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.platform.VectorBaseActivity
class CreateDirectRoomActivity : VectorBaseActivity() {
override fun getLayoutRes() = R.layout.activity_simple
override fun initUiAndData() {
if (isFirstCreation()) {
addFragment(CreateDirectRoomFragment(), R.id.simpleFragmentContainer)
}
}
companion object {
fun getIntent(context: Context): Intent {
return Intent(context, CreateDirectRoomActivity::class.java)
}
}
}

View File

@ -0,0 +1,70 @@
/*
*
* * Copyright 2019 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.riotx.features.home.createdirect
import com.airbnb.epoxy.EpoxyController
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
class CreateDirectRoomController @Inject constructor(private val avatarRenderer: AvatarRenderer) : EpoxyController() {
private var state: CreateDirectRoomViewState? = null
var callback: Callback? = null
init {
requestModelBuild()
}
fun setData(state: CreateDirectRoomViewState) {
this.state = state
requestModelBuild()
}
override fun buildModels() {
val currentState = state ?: return
val knownUsers = currentState.knownUsers() ?: return
var lastFirstLetter: String? = null
knownUsers.forEach { user ->
val currentFirstLetter = user.displayName.firstLetterOfDisplayName()
val showLetter = lastFirstLetter != currentFirstLetter
lastFirstLetter = currentFirstLetter
createDirectRoomUserItem {
id(user.userId)
userId(user.userId)
showLetter(showLetter)
firstLetter(currentFirstLetter)
name(user.displayName)
avatarUrl(user.avatarUrl)
avatarRenderer(avatarRenderer)
clickListener { _ ->
callback?.onItemClick(user)
}
}
}
}
interface Callback {
fun onItemClick(user: User)
}
}

View File

@ -0,0 +1,62 @@
/*
*
* * Copyright 2019 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.riotx.features.home.createdirect
import android.os.Bundle
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_create_direct_room.*
import javax.inject.Inject
class CreateDirectRoomFragment : VectorBaseFragment(), CreateDirectRoomController.Callback {
override fun getLayoutResId() = R.layout.fragment_create_direct_room
private val viewModel: CreateDirectRoomViewModel by fragmentViewModel()
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
@Inject lateinit var directRoomController: CreateDirectRoomController
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupRecyclerView()
viewModel.subscribe(this) { renderState(it) }
}
private fun setupRecyclerView() {
recyclerView.setHasFixedSize(true)
directRoomController.callback = this
recyclerView.setController(directRoomController)
}
private fun renderState(state: CreateDirectRoomViewState) {
directRoomController.setData(state)
}
override fun onItemClick(user: User) {
vectorBaseActivity.notImplemented("IMPLEMENT ON USER CLICKED")
}
}

View File

@ -0,0 +1,56 @@
/*
*
* * Copyright 2019 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.riotx.features.home.createdirect
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_create_direct_room_user)
abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute var showLetter: Boolean = false
@EpoxyAttribute var firstLetter: String = ""
@EpoxyAttribute var name: String? = null
@EpoxyAttribute var userId: String = ""
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener)
holder.nameView.text = name
holder.letterView.visibility = if (showLetter) View.VISIBLE else View.INVISIBLE
holder.letterView.text = firstLetter
avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView)
}
class Holder : VectorEpoxyHolder() {
val letterView by bind<TextView>(R.id.createDirectRoomUserLetter)
val nameView by bind<TextView>(R.id.createDirectRoomUserName)
val avatarImageView by bind<ImageView>(R.id.createDirectRoomUserAvatar)
}
}

View File

@ -0,0 +1,59 @@
/*
*
* * Copyright 2019 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.riotx.features.home.createdirect
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.VectorViewModel
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState,
private val session: Session)
: VectorViewModel<CreateDirectRoomViewState>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel
}
companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? {
val fragment: CreateDirectRoomFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.createDirectRoomViewModelFactory.create(state)
}
}
init {
observeKnownUsers()
}
private fun observeKnownUsers() {
session.rx().liveUsers().execute {
this.copy(knownUsers = it)
}
}
}

View File

@ -0,0 +1,28 @@
/*
*
* * Copyright 2019 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.riotx.features.home.createdirect
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.user.model.User
data class CreateDirectRoomViewState(
val knownUsers: Async<List<User>> = Uninitialized
) : MvRxState

View File

@ -144,7 +144,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
}
override fun createDirectChat() {
vectorBaseActivity.notImplemented("creating direct chat")
navigator.openCreateDirectRoom(requireActivity())
}
private fun setupRecyclerView() {
@ -248,7 +248,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
return super.onBackPressed()
}
// RoomSummaryController.Callback **************************************************************
// RoomSummaryController.Callback **************************************************************
override fun onRoomSelected(room: RoomSummary) {
roomListViewModel.accept(RoomListActions.SelectRoom(room))

View File

@ -25,6 +25,8 @@ import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.riotx.features.debug.DebugMenuActivity
import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity
import im.vector.riotx.features.home.createdirect.CreateDirectRoomFragment
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
@ -68,6 +70,11 @@ class DefaultNavigator @Inject constructor() : Navigator {
context.startActivity(intent)
}
override fun openCreateDirectRoom(context: Context) {
val intent = CreateDirectRoomActivity.getIntent(context)
context.startActivity(intent)
}
override fun openRoomsFiltering(context: Context) {
val intent = FilteredRoomsActivity.newIntent(context)
context.startActivity(intent)

View File

@ -29,6 +29,8 @@ interface Navigator {
fun openCreateRoom(context: Context)
fun openCreateDirectRoom(context: Context)
fun openRoomDirectory(context: Context)
fun openRoomsFiltering(context: Context)

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<im.vector.riotx.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/stateView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_create_direct_room_user" />
</im.vector.riotx.core.platform.StateView>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp">
<TextView
android:id="@+id/createDirectRoomUserLetter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:textColor="#7e899c"
android:textSize="20sp"
android:textStyle="normal"
tools:text="C" />
<ImageView
android:id="@+id/createDirectRoomUserAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="48dp"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/createDirectRoomUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
tools:text="@tools:sample/full_names" />
</LinearLayout>