Merge pull request #159 from vector-im/feature/home_rework

Feature/home rework
This commit is contained in:
Benoit Marty 2019-06-04 12:54:38 +02:00 committed by GitHub
commit 647a066c90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
129 changed files with 3196 additions and 1231 deletions

View File

@ -11,7 +11,7 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.android.tools.build:gradle:3.4.1'
classpath 'com.google.gms:google-services:4.2.0'
classpath "com.airbnb.okreplay:gradle-plugin:1.4.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

View File

@ -18,7 +18,6 @@ package im.vector.matrix.android.api.session.events.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Types
import im.vector.matrix.android.internal.di.MoshiProvider
import timber.log.Timber
@ -29,15 +28,19 @@ typealias Content = Map<String, @JvmSuppressWildcards Any>
/**
* This methods is a facility method to map a json content to a model.
*/
inline fun <reified T> Content?.toModel(): T? {
inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
return this?.let {
val moshi = MoshiProvider.providesMoshi()
val moshiAdapter = moshi.adapter(T::class.java)
try {
return moshiAdapter.fromJsonValue(it)
} catch (e: JsonDataException) {
Timber.e(e, "Failed to parse content")
return null
return try {
moshiAdapter.fromJsonValue(it)
} catch (e: Exception) {
if (catchError) {
Timber.e(e, "To model failed : $e")
null
} else {
throw e
}
}
}
}

View File

@ -18,6 +18,7 @@

package im.vector.matrix.android.api.session.user

import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.user.model.User

/**
@ -32,4 +33,11 @@ interface UserService {
*/
fun getUser(userId: String): User?

/**
* Observe a live user from a userId
* @param userId the userId to look for.
* @return a Livedata of user with userId
*/
fun observeUser(userId: String): LiveData<User?>

}

View File

@ -0,0 +1,35 @@
/*
* 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.matrix.android.internal.database.mapper

import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.database.model.UserEntity

internal object UserMapper {

fun map(userEntity: UserEntity): User {
return User(
userEntity.userId,
userEntity.displayName,
userEntity.avatarUrl
)
}
}

internal fun UserEntity.asDomain(): User {
return UserMapper.map(this)
}

View File

@ -231,6 +231,11 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
return userService.getUser(userId)
}

override fun observeUser(userId: String): LiveData<User?> {
assert(isOpen)
return userService.observeUser(userId)
}

// Private methods *****************************************************************************

private fun assertMainThread() {

View File

@ -18,9 +18,13 @@

package im.vector.matrix.android.internal.session.user

import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.user.UserService
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.query.where
import im.vector.matrix.android.internal.util.fetchCopied
@ -29,12 +33,19 @@ internal class DefaultUserService(private val monarchy: Monarchy) : UserService

override fun getUser(userId: String): User? {
val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() }
?: return null
?: return null

return User(
userEntity.userId,
userEntity.displayName,
userEntity.avatarUrl
)
return userEntity.asDomain()
}

override fun observeUser(userId: String): LiveData<User?> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
UserEntity.where(realm, userId)
}
return Transformations.map(liveRealmData) { results ->
results
.map { it.asDomain() }
.firstOrNull()
}
}
}

View File

@ -0,0 +1,52 @@
{
"data": [
{
"displayName": "Long display name useful to test layout with a long display name",
"mxid": "@longmatrixidbecausesometimesuserschooselongmxid:matrix.org",
"message": "William Shakespeare (bapt. 26 April 1564 23 April 1616) was an English poet, playwright and actor, widely regarded as the greatest writer in the English language and the world's greatest dramatist. He is often called England's national poet and the \"Bard of Avon\". His extant works, including collaborations, consist of approximately 39 plays, 154 sonnets, two long narrative poems, and a few other verses, some of uncertain authorship. His plays have been translated into every major living language and are performed more often than those of any other playwright.\n\nShakespeare was born and raised in Stratford-upon-Avon, Warwickshire. At the age of 18, he married Anne Hathaway, with whom he had three children: Susanna and twins Hamnet and Judith. Sometime between 1585 and 1592, he began a successful career in London as an actor, writer, and part-owner of a playing company called the Lord Chamberlain's Men, later known as the King's Men. At age 49 (around 1613), he appears to have retired to Stratford, where he died three years later. Few records of Shakespeare's private life survive; this has stimulated considerable speculation about such matters as his physical appearance, his sexuality, his religious beliefs, and whether the works attributed to him were written by others. Such theories are often criticised for failing to adequately note that few records survive of most commoners of the period.\n\nShakespeare produced most of his known works between 1589 and 1613. His early plays were primarily comedies and histories and are regarded as some of the best work produced in these genres. Until about 1608, he wrote mainly tragedies, among them Hamlet, Othello, King Lear, and Macbeth, all considered to be among the finest works in the English language. In the last phase of his life, he wrote tragicomedies (also known as romances) and collaborated with other playwrights.\n\nMany of Shakespeare's plays were published in editions of varying quality and accuracy in his lifetime. However, in 1623, two fellow actors and friends of Shakespeare's, John Heminges and Henry Condell, published a more definitive text known as the First Folio, a posthumous collected edition of Shakespeare's dramatic works that included all but two of his plays. The volume was prefaced with a poem by Ben Jonson, in which Jonson presciently hails Shakespeare in a now-famous quote as \"not of an age, but for all time\".\n\nThroughout the 20th and 21st centuries, Shakespeare's works have been continually adapted and rediscovered by new movements in scholarship and performance. His plays remain popular and are studied, performed, and reinterpreted through various cultural and political contexts around the world.",
"roomName": "Matrix HQ",
"roomAlias": "#matrix:matrix.org",
"roomTopic": "Welcome to Matrix HQ! Here is the rest of the room topic…"
},
{
"displayName": "benoit",
"mxid": "@benoit:matrix.org",
"message": "Hello!",
"roomName": "Room name very loooooooong with some details",
"roomAlias": "#matrix:matrix.org",
"roomTopic": "Room topic very loooooooong with some details"
},
{
"displayName": "ganfra",
"mxid": "@ganfra:matrix.org",
"message": "How are you?",
"roomName": "Room name very loooooooong with some details",
"roomAlias": "#matrix:matrix.org",
"roomTopic": "Room topic very loooooooong with some details"
},
{
"displayName": "Manu",
"mxid": "@manu:matrix.org",
"message": "Great weather today!",
"roomName": "Room name very loooooooong with some details",
"roomAlias": "#matrix:matrix.org",
"roomTopic": "Room topic very loooooooong with some details"
},
{
"displayName": "Giom",
"mxid": "@giom:matrix.org",
"message": "Let's do a picnic",
"roomName": "Room name very loooooooong with some details",
"roomAlias": "#matrix:matrix.org",
"roomTopic": "Room topic very loooooooong with some details"
},
{
"displayName": "Nad",
"mxid": "@nadonomy:matrix.org",
"message": "Yes, great idea",
"roomName": "Room name very loooooooong with some details",
"roomAlias": "#matrix:matrix.org",
"roomTopic": "Room topic very loooooooong with some details"
}
]
}

View File

@ -44,6 +44,8 @@
android:label="@string/title_activity_emoji_reaction_picker" />

<activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
<activity android:name=".features.home.room.detail.RoomDetailActivity" />

<service
android:name=".core.services.CallService"

View File

@ -0,0 +1,19 @@
/*
* 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.riotredesign.core.animations

const val ANIMATION_DURATION_SHORT = 200L

View File

@ -0,0 +1,37 @@
/*
* 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.riotredesign.core.animations

import android.animation.Animator

open class SimpleAnimatorListener : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {
// No op
}

override fun onAnimationEnd(animation: Animator?) {
// No op
}

override fun onAnimationCancel(animation: Animator?) {
// No op
}

override fun onAnimationStart(animation: Animator?) {
// No op
}
}

View File

@ -18,15 +18,18 @@ package im.vector.riotredesign.core.di

import android.content.Context
import android.content.Context.MODE_PRIVATE
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringArrayProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.HomeRoomListObservableStore
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
import im.vector.riotredesign.features.home.room.list.AlphabeticalRoomComparator
import im.vector.riotredesign.features.home.room.list.ChronologicalRoomComparator
import im.vector.riotredesign.features.navigation.DefaultNavigator
import im.vector.riotredesign.features.navigation.Navigator
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import org.koin.dsl.module.module

@ -50,20 +53,20 @@ class AppModule(private val context: Context) {
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
}

single {
RoomSelectionRepository(get())
}

single {
SelectedGroupStore()
}

single {
VisibleRoomStore()
HomeRoomListObservableStore()
}

single {
RoomSummaryComparator()
ChronologicalRoomComparator()
}

single {
AlphabeticalRoomComparator()
}

single {
@ -78,6 +81,9 @@ class AppModule(private val context: Context) {
Matrix.getInstance().currentSession!!
}

factory { (fragment: Fragment) ->
DefaultNavigator(fragment) as Navigator
}

}
}

View File

@ -28,9 +28,10 @@ class ErrorFormatter(val stringProvider: StringProvider) {
return failure.localizedMessage
}

fun toHumanReadable(throwable: Throwable): String {
fun toHumanReadable(throwable: Throwable?): String {

return when (throwable) {
null -> ""
is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network)
else -> throwable.localizedMessage
}

View File

@ -20,6 +20,7 @@ import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.FrameLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
@ -43,14 +44,13 @@ class ButtonStateView @JvmOverloads constructor(context: Context, attrs: Attribu
fun onRetryClicked()
}

// Big or Flat button
var button: Button

init {
View.inflate(context, R.layout.view_button_state, this)
layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)

buttonStateButton.setOnClickListener {
callback?.onButtonClicked()
}

buttonStateRetry.setOnClickListener {
callback?.onRetryClicked()
}
@ -62,20 +62,32 @@ class ButtonStateView @JvmOverloads constructor(context: Context, attrs: Attribu
0, 0)
.apply {
try {
buttonStateButton.text = getString(R.styleable.ButtonStateView_bsv_button_text)
if (getBoolean(R.styleable.ButtonStateView_bsv_use_flat_button, true)) {
button = buttonStateButtonFlat
buttonStateButtonBig.isVisible = false
} else {
button = buttonStateButtonBig
buttonStateButtonFlat.isVisible = false
}

button.text = getString(R.styleable.ButtonStateView_bsv_button_text)
buttonStateLoaded.setImageDrawable(getDrawable(R.styleable.ButtonStateView_bsv_loaded_image_src))
} finally {
recycle()
}
}

button.setOnClickListener {
callback?.onButtonClicked()
}
}

fun render(newState: State) {
if (newState == State.Button) {
buttonStateButton.isVisible = true
button.isVisible = true
} else {
// We use isInvisible because we want to keep button space in the layout
buttonStateButton.isInvisible = true
button.isInvisible = true
}

buttonStateLoading.isVisible = newState == State.Loading

View File

@ -0,0 +1,61 @@
/*
* 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.riotredesign.core.platform

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.Checkable
import androidx.constraintlayout.widget.ConstraintLayout

class CheckableConstraintLayout : ConstraintLayout, Checkable {

private var mChecked = false

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

override fun isChecked(): Boolean {
return mChecked
}

override fun setChecked(b: Boolean) {
if (b != mChecked) {
mChecked = b
refreshDrawableState()
}
}

override fun toggle() {
isChecked = !mChecked
}

public override fun onCreateDrawableState(extraSpace: Int): IntArray {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
if (isChecked) {
View.mergeDrawableStates(drawableState, CHECKED_STATE_SET)
}
return drawableState
}

companion object {
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
}
}

View File

@ -17,9 +17,9 @@
package im.vector.riotredesign.core.platform

import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import im.vector.riotredesign.R
import kotlinx.android.synthetic.main.view_state.view.*
@ -30,7 +30,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
sealed class State {
object Content : State()
object Loading : State()
data class Empty(val message: CharSequence? = null) : State()
data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : State()
data class Error(val message: CharSequence? = null) : State()
}

@ -52,7 +52,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?

init {
View.inflate(context, R.layout.view_state, this)
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
layoutParams = LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
errorRetryView.setOnClickListener {
eventCallback?.onRetryClicked()
}
@ -62,35 +62,33 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?

private fun update(newState: State) {
when (newState) {
is StateView.State.Content -> {
is State.Content -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.INVISIBLE
contentView?.visibility = View.VISIBLE
}
is StateView.State.Loading -> {
is State.Loading -> {
progressBar.visibility = View.VISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.INVISIBLE
contentView?.visibility = View.INVISIBLE
}
is StateView.State.Empty -> {
is State.Empty -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.VISIBLE
emptyImageView.setImageDrawable(newState.image)
emptyMessageView.text = newState.message
if (contentView != null) {
contentView!!.visibility = View.INVISIBLE
}
emptyTitleView.text = newState.title
contentView?.visibility = View.INVISIBLE
}
is StateView.State.Error -> {
is State.Error -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.VISIBLE
emptyView.visibility = View.INVISIBLE
errorMessageView.text = newState.message
if (contentView != null) {
contentView!!.visibility = View.INVISIBLE
}
contentView?.visibility = View.INVISIBLE
}
}
}

View File

@ -49,11 +49,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
* UI
* ========================================================================================== */

@Nullable
@JvmField
@BindView(R.id.toolbar)
var toolbar: Toolbar? = null

@Nullable
@JvmField
@BindView(R.id.vector_coordinator_layout)
@ -245,14 +240,16 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
protected fun isFirstCreation() = savedInstanceState == null

/**
* Configure the Toolbar. It MUST be present in your layout with id "toolbar"
* Configure the Toolbar, with default back button.
*/
protected fun configureToolbar() {
protected fun configureToolbar(toolbar: Toolbar, displayBack: Boolean = true) {
setSupportActionBar(toolbar)

supportActionBar?.let {
it.setDisplayShowHomeEnabled(true)
it.setDisplayHomeAsUpEnabled(true)
if (displayBack) {
supportActionBar?.let {
it.setDisplayShowHomeEnabled(true)
it.setDisplayHomeAsUpEnabled(true)
}
}
}

@ -297,8 +294,12 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
* Temporary method
* ========================================================================================== */

fun notImplemented() {
toast(getString(R.string.not_implemented))
fun notImplemented(message: String = "") {
if (message.isNotBlank()) {
toast(getString(R.string.not_implemented) + ": $message")
} else {
toast(getString(R.string.not_implemented))
}
}

}

View File

@ -22,14 +22,17 @@ import android.view.*
import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes
import androidx.annotation.MainThread
import androidx.appcompat.widget.Toolbar
import butterknife.ButterKnife
import butterknife.Unbinder
import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util.assertMainThread
import com.google.android.material.snackbar.Snackbar
import im.vector.riotredesign.features.navigation.Navigator
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
import timber.log.Timber

abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
@ -41,6 +44,12 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
activity as VectorBaseActivity
}

/* ==========================================================================================
* Navigator
* ========================================================================================== */

protected val navigator: Navigator by inject { parametersOf(this) }

/* ==========================================================================================
* Life cycle
* ========================================================================================== */
@ -123,6 +132,20 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
return this
}

/* ==========================================================================================
* Toolbar
* ========================================================================================== */

/**
* Configure the Toolbar.
*/
protected fun setupToolbar(toolbar: Toolbar) {
val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) {
parentActivity.configure(toolbar)
}
}

/* ==========================================================================================
* Disposable
* ========================================================================================== */

View File

@ -29,4 +29,9 @@ object DateProvider {
return LocalDateTime.ofInstant(instant, zoneId)
}

fun currentLocalDateTime(): LocalDateTime {
val instant = Instant.now()
return LocalDateTime.ofInstant(instant, zoneId)
}

}

View File

@ -67,10 +67,12 @@ object AvatarRenderer {
identifier: String,
name: String?,
target: Target<Drawable>) {
if (name.isNullOrEmpty()) {
return
val displayName = if (name.isNullOrBlank()) {
identifier
} else {
name
}
val placeholder = getPlaceholderDrawable(context, identifier, name)
val placeholder = getPlaceholderDrawable(context, identifier, displayName)
buildGlideRequest(glideRequest, avatarUrl)
.placeholder(placeholder)
.into(target)

View File

@ -21,7 +21,6 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
@ -32,16 +31,12 @@ import com.airbnb.mvrx.viewModel
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.hideKeyboard
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.core.platform.OnBackPressed
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
import im.vector.riotredesign.features.rageshake.BugReporter
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotredesign.features.settings.VectorSettingsActivity
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
import kotlinx.android.synthetic.main.activity_home.*
import org.koin.android.ext.android.inject
@ -69,15 +64,13 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
bindScope(getOrCreateScope(HomeModule.HOME_SCOPE))
homeNavigator.activity = this
drawerLayout.addDrawerListener(drawerListener)
if (savedInstanceState == null) {
if (isFirstCreation()) {
val homeDrawerFragment = HomeDrawerFragment.newInstance()
val loadingDetail = LoadingRoomDetailFragment.newInstance()
val loadingDetail = LoadingFragment.newInstance()
replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer)
replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer)
}
homeActivityViewModel.openRoomLiveData.observeEvent(this) {
homeNavigator.openRoomDetail(it, null)
}

homeActivityViewModel.isLoading.observe(this, Observer<Boolean> {
// TODO better UI
if (it) {
@ -113,36 +106,21 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
}

override fun configure(toolbar: Toolbar) {
setSupportActionBar(toolbar)
supportActionBar?.setHomeButtonEnabled(true)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val drawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, 0, 0)
drawerLayout.addDrawerListener(drawerToggle)
drawerToggle.syncState()
configureToolbar(toolbar, false)
}

override fun getMenuRes() = R.menu.home

override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
android.R.id.home -> {
drawerLayout.openDrawer(GravityCompat.START)
return true
}
R.id.sliding_menu_settings -> {
startActivity(VectorSettingsActivity.getIntent(this, "TODO"))
return true
}
R.id.sliding_menu_sign_out -> {
R.id.sliding_menu_sign_out -> {
SignOutUiWorker(this).perform(Matrix.getInstance().currentSession!!)
return true
}
// TODO Temporary code here to create a room
R.id.tmp_menu_create_room -> {
// Start Activity for now
startActivity(Intent(this, RoomDirectoryActivity::class.java))
return true
}
}

return true
@ -160,8 +138,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
}

private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
if (fm.backStackEntryCount == 0)
return false
// if (fm.backStackEntryCount == 0)
// return false

val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed()
for (f in reverseOrder) {
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)

View File

@ -18,26 +18,31 @@ package im.vector.riotredesign.features.home

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import io.reactivex.rxkotlin.subscribeBy
import im.vector.riotredesign.features.home.group.ALL_COMMUNITIES_GROUP_ID
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit

data class EmptyState(val isEmpty: Boolean = true) : MvRxState

class HomeActivityViewModel(state: EmptyState,
private val session: Session,
roomSelectionRepository: RoomSelectionRepository
private val selectedGroupStore: SelectedGroupStore,
private val homeRoomListStore: HomeRoomListObservableStore
) : VectorViewModel<EmptyState>(state), Session.Listener {

companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> {
@ -45,8 +50,9 @@ class HomeActivityViewModel(state: EmptyState,
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
val session = Matrix.getInstance().currentSession!!
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
return HomeActivityViewModel(state, session, roomSelectionRepository)
val selectedGroupStore = viewModelContext.activity.get<SelectedGroupStore>()
val homeRoomListObservableSource = viewModelContext.activity.get<HomeRoomListObservableStore>()
return HomeActivityViewModel(state, session, selectedGroupStore, homeRoomListObservableSource)
}
}

@ -54,29 +60,41 @@ class HomeActivityViewModel(state: EmptyState,
val isLoading: LiveData<Boolean>
get() = _isLoading

private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
val openRoomLiveData: LiveData<LiveEvent<String>>
get() = _openRoomLiveData

init {
session.addListener(this)
val lastSelectedRoomId = roomSelectionRepository.lastSelectedRoom()
if (lastSelectedRoomId == null || session.getRoom(lastSelectedRoomId) == null) {
getTheFirstRoomWhenAvailable()
} else {
_openRoomLiveData.postValue(LiveEvent(lastSelectedRoomId))
}
observeRoomAndGroup()
}

private fun getTheFirstRoomWhenAvailable() {
session.rx().liveRoomSummaries()
.filter { it.isNotEmpty() }
.first(emptyList())
.subscribeBy {
val firstRoom = it.firstOrNull()
if (firstRoom != null) {
_openRoomLiveData.postValue(LiveEvent(firstRoom.roomId))
}
private fun observeRoomAndGroup() {
Observable
.combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>(
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
selectedGroupStore.observe(),
BiFunction { rooms, selectedGroupOption ->
val selectedGroup = selectedGroupOption.orNull()
val filteredDirectRooms = rooms
.filter { it.isDirect }
.filter {
if (selectedGroup == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) {
true
} else {
it.otherMemberIds
.intersect(selectedGroup.userIds)
.isNotEmpty()
}
}

val filteredGroupRooms = rooms
.filter { !it.isDirect }
.filter {
selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID
|| selectedGroup?.roomIds?.contains(it.roomId) ?: true
}
filteredDirectRooms + filteredGroupRooms
}
)
.subscribe {
homeRoomListStore.post(it)
}
.disposeOnClear()
}
@ -87,8 +105,6 @@ class HomeActivityViewModel(state: EmptyState,
session.createRoom(createRoomParams, object : MatrixCallback<String> {
override fun onSuccess(data: String) {
_isLoading.value = false
// Open room id
_openRoomLiveData.postValue(LiveEvent(data))
}

override fun onFailure(failure: Throwable) {

View File

@ -0,0 +1,154 @@
/*
* 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.riotredesign.features.home

import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import androidx.core.view.forEachIndexed
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.room.list.RoomListFragment
import im.vector.riotredesign.features.home.room.list.RoomListParams
import im.vector.riotredesign.features.home.room.list.UnreadCounterBadgeView
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_home_detail.*


@Parcelize
data class HomeDetailParams(
val groupId: String,
val groupName: String,
val groupAvatar: String
) : Parcelable


private const val CURRENT_DISPLAY_MODE = "CURRENT_DISPLAY_MODE"

private const val INDEX_CATCHUP = 0
private const val INDEX_PEOPLE = 1
private const val INDEX_ROOMS = 2

class HomeDetailFragment : VectorBaseFragment() {

private val params: HomeDetailParams by args()
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
private lateinit var currentDisplayMode: RoomListFragment.DisplayMode

private val viewModel: HomeDetailViewModel by fragmentViewModel()

override fun getLayoutResId(): Int {
return R.layout.fragment_home_detail
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
currentDisplayMode = savedInstanceState?.getSerializable(CURRENT_DISPLAY_MODE) as? RoomListFragment.DisplayMode
?: RoomListFragment.DisplayMode.HOME
switchDisplayMode(currentDisplayMode)
setupBottomNavigationView()
setupToolbar()
}


override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(CURRENT_DISPLAY_MODE, currentDisplayMode)
super.onSaveInstanceState(outState)
}

private fun setupToolbar() {
val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) {
parentActivity.configure(groupToolbar)
}
groupToolbar.title = ""
AvatarRenderer.render(
params.groupAvatar,
params.groupId,
params.groupName,
groupToolbarAvatarImageView
)
groupToolbarAvatarImageView.setOnClickListener {
vectorBaseActivity.notImplemented("Group click in toolbar")
}
}

private fun setupBottomNavigationView() {
bottomNavigationView.setOnNavigationItemSelectedListener {
val displayMode = when (it.itemId) {
R.id.bottom_action_home -> RoomListFragment.DisplayMode.HOME
R.id.bottom_action_people -> RoomListFragment.DisplayMode.PEOPLE
R.id.bottom_action_rooms -> RoomListFragment.DisplayMode.ROOMS
else -> RoomListFragment.DisplayMode.HOME
}
if (currentDisplayMode != displayMode) {
currentDisplayMode = displayMode
switchDisplayMode(displayMode)
}
true
}

val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView
menuView.forEachIndexed { index, view ->
val itemView = view as BottomNavigationItemView
val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false)
val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView)
itemView.addView(badgeLayout)
unreadCounterBadgeViews.add(index, unreadCounterBadgeView)
}
}

private fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) {
groupToolbarTitleView.setText(displayMode.titleRes)
updateSelectedFragment(displayMode)
}

private fun updateSelectedFragment(displayMode: RoomListFragment.DisplayMode) {
val fragmentTag = "FRAGMENT_TAG_${displayMode.name}"
var fragment = childFragmentManager.findFragmentByTag(fragmentTag)
if (fragment == null) {
fragment = RoomListFragment.newInstance(RoomListParams(displayMode))
}
childFragmentManager.beginTransaction()
.replace(R.id.roomListContainer, fragment, fragmentTag)
.addToBackStack(fragmentTag)
.commit()
}


override fun invalidate() = withState(viewModel) {
unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup))
unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
}

companion object {

fun newInstance(args: HomeDetailParams): HomeDetailFragment {
return HomeDetailFragment().apply {
setArguments(args)
}
}

}
}

View File

@ -0,0 +1,82 @@
/*
* 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.riotredesign.features.home

import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.riotredesign.core.platform.VectorViewModel
import org.koin.android.ext.android.get

/**
* View model used to update the home bottom bar notification counts
*/
class HomeDetailViewModel(initialState: HomeDetailViewState,
private val homeRoomListStore: HomeRoomListObservableStore)
: VectorViewModel<HomeDetailViewState>(initialState) {

companion object : MvRxViewModelFactory<HomeDetailViewModel, HomeDetailViewState> {

@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: HomeDetailViewState): HomeDetailViewModel? {
val homeRoomListStore = viewModelContext.activity.get<HomeRoomListObservableStore>()
return HomeDetailViewModel(state, homeRoomListStore)
}
}

init {
observeRoomSummaries()
}

// PRIVATE METHODS *****************************************************************************

private fun observeRoomSummaries() {
homeRoomListStore
.observe()
.subscribe { list ->
list.let { summaries ->
val peopleNotifications = summaries
.filter { it.isDirect }
.map { it.notificationCount }
.reduce { acc, i -> acc + i }
val peopleHasHighlight = summaries
.filter { it.isDirect }
.any { it.highlightCount > 0 }

val roomsNotifications = summaries
.filter { !it.isDirect }
.map { it.notificationCount }
.reduce { acc, i -> acc + i }
val roomsHasHighlight = summaries
.filter { !it.isDirect }
.any { it.highlightCount > 0 }

setState {
copy(
notificationCountCatchup = peopleNotifications + roomsNotifications,
notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight,
notificationCountPeople = peopleNotifications,
notificationHighlightPeople = peopleHasHighlight,
notificationCountRooms = roomsNotifications,
notificationHighlightRooms = roomsHasHighlight
)
}
}
}
.disposeOnClear()
}

}

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.riotredesign.features.home

import com.airbnb.mvrx.MvRxState

data class HomeDetailViewState(
val notificationCountCatchup: Int = 0,
val notificationHighlightCatchup: Boolean = false,
val notificationCountPeople: Int = 0,
val notificationHighlightPeople: Boolean = false,
val notificationCountRooms: Int = 0,
val notificationHighlightRooms: Boolean = false
) : MvRxState

View File

@ -17,11 +17,14 @@
package im.vector.riotredesign.features.home

import android.os.Bundle
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.observeK
import im.vector.riotredesign.core.extensions.replaceChildFragment
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.group.GroupListFragment
import im.vector.riotredesign.features.home.room.list.RoomListFragment
import kotlinx.android.synthetic.main.fragment_home_drawer.*
import org.koin.android.ext.android.inject

class HomeDrawerFragment : VectorBaseFragment() {

@ -32,16 +35,26 @@ class HomeDrawerFragment : VectorBaseFragment() {
}
}

val session by inject<Session>()

override fun getLayoutResId() = R.layout.fragment_home_drawer

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null) {
val groupListFragment = GroupListFragment.newInstance()
replaceChildFragment(groupListFragment, R.id.groupListFragmentContainer)
val roomListFragment = RoomListFragment.newInstance()
replaceChildFragment(roomListFragment, R.id.roomListFragmentContainer)
replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer)
}

session.observeUser(session.sessionParams.credentials.userId).observeK(this) { user ->
if (user != null) {
AvatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
homeDrawerUsernameView.text = user.displayName
homeDrawerUserIdView.text = user.userId
}
}
homeDrawerHeaderSettingsView.setOnClickListener {
navigator.openSettings()
}
}

}

View File

@ -25,11 +25,16 @@ import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserControl
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.home.group.GroupSummaryController
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
import im.vector.riotredesign.features.home.room.detail.timeline.factory.DefaultItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.factory.MessageItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.factory.NoticeItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.list.RoomSummaryController
import im.vector.riotredesign.features.html.EventHtmlRenderer
import org.koin.core.parameter.parametersOf
import org.koin.dsl.module.module

class HomeModule {
@ -37,8 +42,6 @@ class HomeModule {
companion object {
const val HOME_SCOPE = "HOME_SCOPE"
const val ROOM_DETAIL_SCOPE = "ROOM_DETAIL_SCOPE"
const val ROOM_LIST_SCOPE = "ROOM_LIST_SCOPE"
const val GROUP_LIST_SCOPE = "GROUP_LIST_SCOPE"
}

val definition = module {
@ -50,31 +53,37 @@ class HomeModule {
}

scope(HOME_SCOPE) {
HomePermalinkHandler(get())
HomePermalinkHandler(get(), get())
}

// Fragment scopes

factory {
TimelineDateFormatter(get())
}

factory {
NoticeEventFormatter(get())
}

factory { (fragment: Fragment) ->
val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get())
val timelineDateFormatter = TimelineDateFormatter(get())
val noticeEventFormatter = get<NoticeEventFormatter>(parameters = { parametersOf(fragment) })
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
val colorProvider = ColorProvider(fragment.requireContext())
val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer,get())
val timelineDateFormatter = get<TimelineDateFormatter>()
val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer, get())

val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory,
roomNameItemFactory = RoomNameItemFactory(get()),
roomTopicItemFactory = RoomTopicItemFactory(get()),
roomMemberItemFactory = RoomMemberItemFactory(get()),
roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()),
callItemFactory = CallItemFactory(get()),
val timelineItemFactory = TimelineItemFactory(
messageItemFactory = messageItemFactory,
noticeItemFactory = NoticeItemFactory(noticeEventFormatter),
defaultItemFactory = DefaultItemFactory()
)
TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider)
}

factory {
RoomSummaryController(get())
RoomSummaryController(get(), get(), get())
}

factory {

View File

@ -18,11 +18,10 @@ package im.vector.riotredesign.features.home

import androidx.core.view.GravityCompat
import androidx.fragment.app.FragmentManager
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
import im.vector.riotredesign.features.home.room.detail.RoomDetailFragment
import im.vector.riotredesign.features.navigation.Navigator
import kotlinx.android.synthetic.main.activity_home.*
import timber.log.Timber

@ -32,22 +31,24 @@ class HomeNavigator {

private var rootRoomId: String? = null

fun openSelectedGroup(groupSummary: GroupSummary) {
Timber.v("Open selected group ${groupSummary.groupId}")
activity?.let {
val args = HomeDetailParams(groupSummary.groupId, groupSummary.displayName, groupSummary.avatarUrl)
val homeDetailFragment = HomeDetailFragment.newInstance(args)
it.drawerLayout?.closeDrawer(GravityCompat.START)
it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer)
}
}

fun openRoomDetail(roomId: String,
eventId: String?,
addToBackstack: Boolean = false) {
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
navigator: Navigator) {
Timber.v("Open room detail $roomId - $eventId")
activity?.let {
//TODO enable eventId permalink. It doesn't work enough at the moment.
val args = RoomDetailArgs(roomId)
val roomDetailFragment = RoomDetailFragment.newInstance(args)
it.drawerLayout?.closeDrawer(GravityCompat.START)
if (addToBackstack) {
it.addFragmentToBackstack(roomDetailFragment, R.id.homeDetailFragmentContainer, roomId)
} else {
rootRoomId = roomId
clearBackStack(it.supportFragmentManager)
it.replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
}
navigator.openRoom(roomId)
}
}


View File

@ -19,8 +19,10 @@ package im.vector.riotredesign.features.home
import android.net.Uri
import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.riotredesign.features.navigation.Navigator

class HomePermalinkHandler(private val navigator: HomeNavigator) {
class HomePermalinkHandler(private val homeNavigator: HomeNavigator,
private val navigator: Navigator) {

fun launch(deepLink: String?) {
val uri = deepLink?.let { Uri.parse(it) }
@ -34,16 +36,16 @@ class HomePermalinkHandler(private val navigator: HomeNavigator) {
val permalinkData = PermalinkParser.parse(deepLink)
when (permalinkData) {
is PermalinkData.EventLink -> {
navigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, true)
homeNavigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, navigator)
}
is PermalinkData.RoomLink -> {
navigator.openRoomDetail(permalinkData.roomIdOrAlias, null, true)
homeNavigator.openRoomDetail(permalinkData.roomIdOrAlias, null, navigator)
}
is PermalinkData.GroupLink -> {
navigator.openGroupDetail(permalinkData.groupId)
homeNavigator.openGroupDetail(permalinkData.groupId)
}
is PermalinkData.UserLink -> {
navigator.openUserDetail(permalinkData.userId)
homeNavigator.openUserDetail(permalinkData.userId)
}
is PermalinkData.FallbackLink -> {


View File

@ -0,0 +1,35 @@
/*
* 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.riotredesign.features.home

import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.core.utils.RxStore
import im.vector.riotredesign.features.home.room.list.RoomListDisplayModeFilter
import im.vector.riotredesign.features.home.room.list.RoomListFragment
import io.reactivex.Observable

class HomeRoomListObservableStore : RxStore<List<RoomSummary>>() {

fun observeFilteredBy(displayMode: RoomListFragment.DisplayMode): Observable<List<RoomSummary>> {
return observe()
.flatMapSingle {
Observable.fromIterable(it).filter(RoomListDisplayModeFilter(displayMode)).toList()
}
}


}

View File

@ -0,0 +1,49 @@
/*
*
* * 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.riotredesign.features.home

import android.graphics.drawable.AnimationDrawable
import android.os.Bundle
import android.view.View
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_loading.*

class LoadingFragment : VectorBaseFragment() {

companion object {

fun newInstance(): LoadingFragment {
return LoadingFragment()
}
}

override fun getLayoutResId() = R.layout.fragment_loading

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val background = animatedLogoImageView.background
if (background is AnimationDrawable) {
background.start()
}
}


}

View File

@ -22,13 +22,12 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.platform.StateView
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomeNavigator
import kotlinx.android.synthetic.main.fragment_group_list.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope

class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback {

@ -39,17 +38,20 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback
}

private val viewModel: GroupListViewModel by fragmentViewModel()
private val homeNavigator by inject<HomeNavigator>()
private val groupController by inject<GroupSummaryController>()

override fun getLayoutResId() = R.layout.fragment_group_list

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(HomeModule.GROUP_LIST_SCOPE))
groupController.callback = this
stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(groupController)
viewModel.subscribe { renderState(it) }
viewModel.openGroupLiveData.observeEvent(this) {
homeNavigator.openSelectedGroup(it)
}
}

private fun renderState(state: GroupListViewState) {

View File

@ -16,17 +16,26 @@

package im.vector.riotredesign.features.home.group

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.rx.rx
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.core.utils.LiveEvent
import org.koin.android.ext.android.get

const val ALL_COMMUNITIES_GROUP_ID = "ALL_COMMUNITIES_GROUP_ID"

class GroupListViewModel(initialState: GroupListViewState,
private val selectedGroupHolder: SelectedGroupStore,
private val session: Session
private val session: Session,
private val stringProvider: StringProvider
) : VectorViewModel<GroupListViewState>(initialState) {

companion object : MvRxViewModelFactory<GroupListViewModel, GroupListViewState> {
@ -35,19 +44,27 @@ class GroupListViewModel(initialState: GroupListViewState,
override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? {
val currentSession = viewModelContext.activity.get<Session>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
return GroupListViewModel(state, selectedGroupHolder, currentSession)
val stringProvider = viewModelContext.activity.get<StringProvider>()
return GroupListViewModel(state, selectedGroupHolder, currentSession, stringProvider)
}
}

private val _openGroupLiveData = MutableLiveData<LiveEvent<GroupSummary>>()
val openGroupLiveData: LiveData<LiveEvent<GroupSummary>>
get() = _openGroupLiveData

init {
observeGroupSummaries()
observeState()
observeSelectionState()
}

private fun observeState() {
subscribe {
val selectedGroup = Option.fromNullable(it.selectedGroup)
selectedGroupHolder.post(selectedGroup)
private fun observeSelectionState() {
selectSubscribe(GroupListViewState::selectedGroup) {
if (it != null) {
_openGroupLiveData.postValue(LiveEvent(it))
val optionGroup = Option.fromNullable(it)
selectedGroupHolder.post(optionGroup)
}
}
}

@ -62,17 +79,23 @@ class GroupListViewModel(initialState: GroupListViewState,
private fun handleSelectGroup(action: GroupListActions.SelectGroup) = withState { state ->
if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
setState { copy(selectedGroup = action.groupSummary) }
} else {
setState { copy(selectedGroup = null) }
}
}


private fun observeGroupSummaries() {
session
.rx().liveGroupSummaries()
.map {
val myUser = session.getUser(session.sessionParams.credentials.userId)
val allCommunityGroup = GroupSummary(
groupId = ALL_COMMUNITIES_GROUP_ID,
displayName = stringProvider.getString(R.string.group_all_communities),
avatarUrl = myUser?.avatarUrl ?: "")
listOf(allCommunityGroup) + it
}
.execute { async ->
copy(asyncGroups = async)
val newSelectedGroup = selectedGroup ?: async()?.firstOrNull()
copy(asyncGroups = async, selectedGroup = newSelectedGroup)
}
}


View File

@ -17,12 +17,13 @@
package im.vector.riotredesign.features.home.group

import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.platform.CheckableFrameLayout
import im.vector.riotredesign.core.platform.CheckableConstraintLayout
import im.vector.riotredesign.features.home.AvatarRenderer

@EpoxyModelClass(layout = R.layout.item_group)
@ -36,14 +37,16 @@ abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() {

override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.isSelected = selected
holder.rootView.setOnClickListener { listener?.invoke() }
holder.groupNameView.text = groupName
holder.rootView.isChecked = selected
AvatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView)
}

class Holder : VectorEpoxyHolder() {
val avatarImageView by bind<ImageView>(R.id.groupAvatarImageView)
val rootView by bind<CheckableFrameLayout>(R.id.itemGroupLayout)
val groupNameView by bind<TextView>(R.id.groupNameView)
val rootView by bind<CheckableConstraintLayout>(R.id.itemGroupLayout)
}

}

View File

@ -1,47 +0,0 @@
/*
* 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.riotredesign.features.home.room.detail

import android.graphics.drawable.AnimationDrawable
import android.os.Bundle
import android.view.View
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_loading_room_detail.*

class LoadingRoomDetailFragment : VectorBaseFragment() {

companion object {

fun newInstance(): LoadingRoomDetailFragment {
return LoadingRoomDetailFragment()
}
}

override fun getLayoutResId() = R.layout.fragment_loading_room_detail

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val background = animatedLogoImageView.background
if (background is AnimationDrawable) {
background.start()
}
}


}

View File

@ -25,7 +25,6 @@ sealed class RoomDetailActions {

data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
object IsDisplayed : RoomDetailActions()
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()

View File

@ -0,0 +1,63 @@
/*
*
* * 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.riotredesign.features.home.room.detail

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseActivity

class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {

override fun getLayoutRes(): Int {
return R.layout.activity_room_detail
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isFirstCreation()) {
val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
?: return
val roomDetailFragment = RoomDetailFragment.newInstance(roomDetailArgs)
replaceFragment(roomDetailFragment, R.id.roomDetailContainer)
}
}

override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}

companion object {

private const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS"

fun newIntent(context: Context, roomDetailArgs: RoomDetailArgs): Intent {
return Intent(context, RoomDetailActivity::class.java).apply {
putExtra(EXTRA_ROOM_DETAIL_ARGS, roomDetailArgs)
}
}


}

}

View File

@ -42,7 +42,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader
@ -66,7 +66,6 @@ import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.extensions.hideKeyboard
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.*
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
@ -154,6 +153,7 @@ class RoomDetailFragment :
}
}

private val roomDetailArgs: RoomDetailArgs by args()
private val session by inject<Session>()
private val glideRequests by lazy {
GlideApp.with(this)
@ -180,8 +180,8 @@ class RoomDetailFragment :
super.onActivityCreated(savedInstanceState)
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
setupToolbar(roomToolbar)
setupRecyclerView()
setupToolbar()
setupComposer()
setupAttachmentButton()
setupInviteView()
@ -213,7 +213,7 @@ class RoomDetailFragment :
}
SendMode.EDIT,
SendMode.QUOTE,
SendMode.REPLY -> {
SendMode.REPLY -> {
commandAutocompletePolicy.enabled = false
if (event == null) {
//we should ignore? can this happen?
@ -276,7 +276,7 @@ class RoomDetailFragment :
if (resultCode == RESULT_OK && data != null) {
when (requestCode) {
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
REACTION_SELECT_REQUEST_CODE -> {
REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
@ -288,20 +288,8 @@ class RoomDetailFragment :
}
}

override fun onResume() {
super.onResume()
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
}

// PRIVATE METHODS *****************************************************************************

private fun setupToolbar() {
val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) {
parentActivity.configure(toolbar)
}
}

private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView)
@ -444,24 +432,24 @@ class RoomDetailFragment :
private fun onSendChoiceClicked(dialogListItem: DialogListItem) {
Timber.v("On send choice clicked: $dialogListItem")
when (dialogListItem) {
is DialogListItem.SendFile -> {
is DialogListItem.SendFile -> {
// launchFileIntent
}
is DialogListItem.SendVoice -> {
is DialogListItem.SendVoice -> {
//launchAudioRecorderIntent()
}
is DialogListItem.SendSticker -> {
is DialogListItem.SendSticker -> {
//startStickerPickerActivity()
}
is DialogListItem.TakePhotoVideo ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
// launchCamera()
}
is DialogListItem.TakePhoto ->
is DialogListItem.TakePhoto ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) {
openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE)
}
is DialogListItem.TakeVideo ->
is DialogListItem.TakeVideo ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) {
// launchNativeVideoRecorder()
}
@ -476,7 +464,7 @@ class RoomDetailFragment :
private fun renderState(state: RoomDetailViewState) {
renderRoomSummary(state)
val summary = state.asyncRoomSummary()
val inviter = state.inviter()
val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) {
timelineEventController.setTimeline(state.timeline)
inviteView.visibility = View.GONE
@ -488,20 +476,20 @@ class RoomDetailFragment :
} else if (summary?.membership == Membership.INVITE && inviter != null) {
inviteView.visibility = View.VISIBLE
inviteView.render(inviter, VectorInviteView.Mode.LARGE)
} else {
//TODO : close the screen
} else if (state.asyncInviter.complete) {
vectorBaseActivity.finish()
}
}

private fun renderRoomSummary(state: RoomDetailViewState) {
state.asyncRoomSummary()?.let {
toolbarTitleView.text = it.displayName
AvatarRenderer.render(it, toolbarAvatarImageView)
roomToolbarTitleView.text = it.displayName
AvatarRenderer.render(it, roomToolbarAvatarImageView)
if (it.topic.isNotEmpty()) {
toolbarSubtitleView.visibility = View.VISIBLE
toolbarSubtitleView.text = it.topic
roomToolbarSubtitleView.visibility = View.VISIBLE
roomToolbarSubtitleView.text = it.topic
} else {
toolbarSubtitleView.visibility = View.GONE
roomToolbarSubtitleView.visibility = View.GONE
}
}
}
@ -513,20 +501,20 @@ class RoomDetailFragment :
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
when (sendMessageResult) {
is SendMessageResult.MessageSent,
is SendMessageResult.SlashCommandHandled -> {
is SendMessageResult.SlashCommandHandled -> {
// Clear composer
composerLayout.composerEditText.text = null
}
is SendMessageResult.SlashCommandError -> {
is SendMessageResult.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
}
is SendMessageResult.SlashCommandUnknown -> {
is SendMessageResult.SlashCommandUnknown -> {
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
}
is SendMessageResult.SlashCommandResultOk -> {
is SendMessageResult.SlashCommandResultOk -> {
// Ignore
}
is SendMessageResult.SlashCommandResultError -> {
is SendMessageResult.SlashCommandResultError -> {
displayCommandError(sendMessageResult.throwable.localizedMessage)
}
is SendMessageResult.SlashCommandNotImplemented -> {
@ -577,11 +565,8 @@ class RoomDetailFragment :

override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val roomId = (arguments?.get(MvRx.KEY_ARG) as? RoomDetailArgs)?.roomId
if (roomId.isNullOrBlank()) {
Timber.e("Missing RoomId, cannot open bottomsheet")
return false
}
val roomId = roomDetailArgs.roomId

this.view?.hideKeyboard()
MessageActionsBottomSheet
.newInstance(roomId, informationData)
@ -624,22 +609,22 @@ class RoomDetailFragment :
it?.getContentIfNotHandled()?.let { actionData ->

when (actionData.actionId) {
MessageMenuViewModel.ACTION_ADD_REACTION -> {
MessageMenuViewModel.ACTION_ADD_REACTION -> {
val eventId = actionData.data?.toString() ?: return
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE)
}
MessageMenuViewModel.ACTION_COPY -> {
MessageMenuViewModel.ACTION_COPY -> {
//I need info about the current selected message :/
copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false)
val snack = Snackbar.make(view!!, requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
snack.show()
}
MessageMenuViewModel.ACTION_DELETE -> {
MessageMenuViewModel.ACTION_DELETE -> {
val eventId = actionData.data?.toString() ?: return
roomDetailViewModel.process(RoomDetailActions.RedactAction(eventId, context?.getString(R.string.event_redacted_by_user_reason)))
}
MessageMenuViewModel.ACTION_SHARE -> {
MessageMenuViewModel.ACTION_SHARE -> {
//TODO current data communication is too limited
//Need to now the media type
actionData.data?.toString()?.let {
@ -682,25 +667,25 @@ class RoomDetailFragment :
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
.show()
}
MessageMenuViewModel.ACTION_QUICK_REACT -> {
MessageMenuViewModel.ACTION_QUICK_REACT -> {
//eventId,ClickedOn,Opposite
(actionData.data as? Triple<String, String, String>)?.let { (eventId, clickedOn, opposite) ->
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite))
}
}
MessageMenuViewModel.ACTION_EDIT -> {
MessageMenuViewModel.ACTION_EDIT -> {
val eventId = actionData.data.toString()
roomDetailViewModel.process(RoomDetailActions.EnterEditMode(eventId))
}
MessageMenuViewModel.ACTION_QUOTE -> {
MessageMenuViewModel.ACTION_QUOTE -> {
val eventId = actionData.data.toString()
roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(eventId))
}
MessageMenuViewModel.ACTION_REPLY -> {
MessageMenuViewModel.ACTION_REPLY -> {
val eventId = actionData.data.toString()
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
}
else -> {
else -> {
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
}
}
@ -759,7 +744,7 @@ class RoomDetailFragment :
}

fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
val snack = Snackbar.make(view!!, message, Snackbar.LENGTH_SHORT)
val snack = Snackbar.make(view!!, message, duration)
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
snack.show()
}

View File

@ -37,7 +37,6 @@ import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.command.CommandParser
import im.vector.riotredesign.features.command.ParsedCommand
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import io.reactivex.rxkotlin.subscribeBy
import org.commonmark.parser.Parser
@ -49,8 +48,7 @@ import java.util.concurrent.TimeUnit


class RoomDetailViewModel(initialState: RoomDetailViewState,
private val session: Session,
private val visibleRoomHolder: VisibleRoomStore
private val session: Session
) : VectorViewModel<RoomDetailViewState>(initialState) {

private val room = session.getRoom(initialState.roomId)!!
@ -66,8 +64,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? {
val currentSession = viewModelContext.activity.get<Session>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
return RoomDetailViewModel(state, currentSession, visibleRoomHolder)
return RoomDetailViewModel(state, currentSession)
}
}

@ -82,21 +79,20 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,

fun process(action: RoomDetailActions) {
when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
is RoomDetailActions.SendMedia -> handleSendMedia(action)
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
is RoomDetailActions.LoadMore -> handleLoadMore(action)
is RoomDetailActions.SendReaction -> handleSendReaction(action)
is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
is RoomDetailActions.RejectInvite -> handleRejectInvite()
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.SendMedia -> handleSendMedia(action)
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
is RoomDetailActions.LoadMore -> handleLoadMore(action)
is RoomDetailActions.SendReaction -> handleSendReaction(action)
is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
is RoomDetailActions.RejectInvite -> handleRejectInvite()
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action)
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action)
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
}
}

@ -136,69 +132,69 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
val slashCommandResult = CommandParser.parseSplashCommand(action.text)

when (slashCommandResult) {
is ParsedCommand.ErrorNotACommand -> {
is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
}
is ParsedCommand.ErrorSyntax -> {
is ParsedCommand.ErrorSyntax -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)))
}
is ParsedCommand.ErrorEmptySlashCommand -> {
is ParsedCommand.ErrorEmptySlashCommand -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/")))
}
is ParsedCommand.ErrorUnknownSlashCommand -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)))
}
is ParsedCommand.Invite -> {
is ParsedCommand.Invite -> {
handleInviteSlashCommand(slashCommandResult)
}
is ParsedCommand.SetUserPowerLevel -> {
is ParsedCommand.SetUserPowerLevel -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
}
is ParsedCommand.ClearScalarToken -> {
is ParsedCommand.ClearScalarToken -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
}
is ParsedCommand.SetMarkdown -> {
is ParsedCommand.SetMarkdown -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
}
is ParsedCommand.UnbanUser -> {
is ParsedCommand.UnbanUser -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
}
is ParsedCommand.BanUser -> {
is ParsedCommand.BanUser -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
}
is ParsedCommand.KickUser -> {
is ParsedCommand.KickUser -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
}
is ParsedCommand.JoinRoom -> {
is ParsedCommand.JoinRoom -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
}
is ParsedCommand.PartRoom -> {
is ParsedCommand.PartRoom -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
}
is ParsedCommand.SendEmote -> {
is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
}
is ParsedCommand.ChangeTopic -> {
is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(slashCommandResult)
}
is ParsedCommand.ChangeDisplayName -> {
is ParsedCommand.ChangeDisplayName -> {
// TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
}
}
}
SendMode.EDIT -> {
SendMode.EDIT -> {
room.editTextMessage(state.selectedEvent?.root?.eventId ?: "", action.text, action.autoMarkdown)
setState {
copy(
@ -208,7 +204,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
}
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
}
SendMode.QUOTE -> {
SendMode.QUOTE -> {
val messageContent: MessageContent? =
state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel()
?: state.selectedEvent?.root?.content.toModel()
@ -234,7 +230,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
}
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
}
SendMode.REPLY -> {
SendMode.REPLY -> {
state.selectedEvent?.let {
room.replyToMessage(it.root, action.text)
setState {
@ -356,10 +352,6 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
displayedEventsObservable.accept(action)
}

private fun handleIsDisplayed() {
visibleRoomHolder.post(roomId)
}

private fun handleLoadMore(action: RoomDetailActions.LoadMore) {
timeline.paginate(action.direction, PAGINATION_COUNT)
}
@ -388,6 +380,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
}
}
}

private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) {
room.getTimeLineEvent(action.eventId)?.let {
setState {
@ -400,7 +393,6 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
}



private fun observeEventDisplayedActions() {
// We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on.
@ -429,7 +421,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
summary.lastMessage?.sender?.let { senderId ->
session.getUser(senderId)
}?.also {
setState { copy(inviter = Success(it)) }
setState { copy(asyncInviter = Success(it)) }
}
}
}

View File

@ -44,7 +44,7 @@ data class RoomDetailViewState(
val roomId: String,
val eventId: String?,
val timeline: Timeline? = null,
val inviter: Async<User> = Uninitialized,
val asyncInviter: Async<User> = Uninitialized,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val asyncTimelineData: Async<TimelineData> = Uninitialized,
val sendMode: SendMode = SendMode.REGULAR,

View File

@ -24,9 +24,7 @@ import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@ -229,10 +227,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
} else {
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
val mergedData = mergedEvents.map { mergedEvent ->
val eventContent: RoomMember? = mergedEvent.root.content.toModel()
val prevEventContent: RoomMember? = mergedEvent.root.prevContent.toModel()
val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, mergedEvent)
val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, mergedEvent)
val senderAvatar = mergedEvent.senderAvatar()
val senderName = mergedEvent.senderName()
MergedHeaderItem.Data(
userId = mergedEvent.root.sender ?: "",
avatarUrl = senderAvatar,

View File

@ -1,59 +0,0 @@
/*
* 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.riotredesign.features.home.room.detail.timeline.factory

import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_

class CallItemFactory(private val stringProvider: StringProvider) {

fun create(event: TimelineEvent): NoticeItem? {
val text = buildNoticeText(event.root, event.senderName) ?: return null
return NoticeItem_()
.noticeText(text)
.avatarUrl(event.senderAvatar)
.memberName(event.senderName)
}

private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
return when {
EventType.CALL_INVITE == event.type -> {
val content = event.content.toModel<CallInviteContent>() ?: return null
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
return if (isVideoCall) {
stringProvider.getString(R.string.notice_placed_video_call, senderName)
} else {
stringProvider.getString(R.string.notice_placed_voice_call, senderName)
}
}
EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, senderName)
EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName)
else -> null
}

}


}

View File

@ -16,28 +16,24 @@

package im.vector.riotredesign.features.home.room.detail.timeline.factory

import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_

class RoomTopicItemFactory(private val stringProvider: StringProvider) {
class NoticeItemFactory(private val eventFormatter: NoticeEventFormatter) {

fun create(event: TimelineEvent): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null
val senderName = event.senderName()
val senderAvatar = event.senderAvatar()

val content: RoomTopicContent = event.root.content.toModel() ?: return null
val text = if (content.topic.isNullOrEmpty()) {
stringProvider.getString(R.string.notice_room_topic_removed, event.senderName)
} else {
stringProvider.getString(R.string.notice_room_topic_changed, event.senderName, content.topic)
}
return NoticeItem_()
.noticeText(text)
.avatarUrl(event.senderAvatar)
.memberName(event.senderName)
.noticeText(formattedText)
.avatarUrl(senderAvatar)
.memberName(senderName)
}


View File

@ -1,55 +0,0 @@
/*
* 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.riotredesign.features.home.room.detail.timeline.factory

import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_


class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvider) {

fun create(event: TimelineEvent): NoticeItem? {
val noticeText = buildNoticeText(event.root, event.senderName) ?: return null
return NoticeItem_()
.noticeText(noticeText)
.avatarUrl(event.senderAvatar)
.memberName(event.senderName)
}

private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
val content = event.content.toModel<RoomHistoryVisibilityContent>() ?: return null
val formattedVisibility = when (content.historyVisibility) {
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited)
RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined)
RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable)
}
return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility)
}


}


View File

@ -1,135 +0,0 @@
/*
* 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.riotredesign.features.home.room.detail.timeline.factory

import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_


//TODO : complete with call membership events¬
class RoomMemberItemFactory(private val stringProvider: StringProvider) {

fun create(event: TimelineEvent): NoticeItem? {
val eventContent: RoomMember? = event.root.content.toModel()
val prevEventContent: RoomMember? = event.root.prevContent.toModel()
val noticeText = buildRoomMemberNotice(event, eventContent, prevEventContent) ?: return null
val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, event)
val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event)

return NoticeItem_()
.userId(event.root.sender ?: "")
.noticeText(noticeText)
.avatarUrl(senderAvatar)
.memberName(senderName)
}

private fun buildRoomMemberNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
return if (isMembershipEvent) {
buildMembershipNotice(event, eventContent, prevEventContent)
} else {
buildProfileNotice(event, eventContent, prevEventContent)
}
}

private fun buildProfileNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val displayText = StringBuilder()
// Check display name has been changed
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
val displayNameText = when {
prevEventContent?.displayName.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_display_name_set, event.root.sender, eventContent?.displayName)
eventContent?.displayName.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_display_name_removed, event.root.sender, prevEventContent?.displayName)
else ->
stringProvider.getString(R.string.notice_display_name_changed_from,
event.root.sender, prevEventContent?.displayName, eventContent?.displayName)
}
displayText.append(displayNameText)
}
// Check whether the avatar has been changed
if (!TextUtils.equals(eventContent?.avatarUrl, prevEventContent?.avatarUrl)) {
val displayAvatarText = if (displayText.isNotEmpty()) {
displayText.append(" ")
stringProvider.getString(R.string.notice_avatar_changed_too)
} else {
stringProvider.getString(R.string.notice_avatar_url_changed, event.senderName)
}
displayText.append(displayAvatarText)
}
return displayText.toString()
}

private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val senderDisplayName = event.senderName ?: event.root.sender
val targetDisplayName = eventContent?.displayName ?: event.root.sender
return when {
Membership.INVITE == eventContent?.membership -> {
// TODO get userId
val selfUserId = ""
when {
eventContent.thirdPartyInvite != null ->
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
TextUtils.equals(event.root.stateKey, selfUserId) ->
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
event.root.stateKey.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
else ->
stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
}
}
Membership.JOIN == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_join, senderDisplayName)
Membership.LEAVE == eventContent?.membership ->
// 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
return if (TextUtils.equals(event.root.sender, event.root.stateKey)) {
if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
} else {
val leftDisplayName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event)
stringProvider.getString(R.string.notice_room_leave, leftDisplayName)
}
} else if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.JOIN) {
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.BAN) {
stringProvider.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName)
} else {
null
}
Membership.BAN == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
Membership.KNOCK == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
else -> null
}
}


}


View File

@ -1,45 +0,0 @@
/*
* 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.riotredesign.features.home.room.detail.timeline.factory

import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_

class RoomNameItemFactory(private val stringProvider: StringProvider) {

fun create(event: TimelineEvent): NoticeItem? {

val content: RoomNameContent = event.root.content.toModel() ?: return null
val text = if (!TextUtils.isEmpty(content.name)) {
stringProvider.getString(R.string.notice_room_name_changed, event.senderName, content.name)
} else {
stringProvider.getString(R.string.notice_room_name_removed, event.senderName)
}
return NoticeItem_()
.noticeText(text)
.avatarUrl(event.senderAvatar)
.memberName(event.senderName)
}


}

View File

@ -23,11 +23,7 @@ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController

class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
private val roomNameItemFactory: RoomNameItemFactory,
private val roomTopicItemFactory: RoomTopicItemFactory,
private val roomMemberItemFactory: RoomMemberItemFactory,
private val roomHistoryVisibilityItemFactory: RoomHistoryVisibilityItemFactory,
private val callItemFactory: CallItemFactory,
private val noticeItemFactory: NoticeItemFactory,
private val defaultItemFactory: DefaultItemFactory) {

fun create(event: TimelineEvent,
@ -36,23 +32,22 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,

val computedModel = try {
when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event)
EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event)
EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event)
EventType.STATE_HISTORY_VISIBILITY -> roomHistoryVisibilityItemFactory.create(event)
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)

EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> callItemFactory.create(event)
EventType.CALL_ANSWER -> noticeItemFactory.create(event)

EventType.ENCRYPTED,
EventType.ENCRYPTION,
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER,
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)

else -> null
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)
else -> null
}
} catch (e: Exception) {
defaultItemFactory.create(event, e)

View File

@ -0,0 +1,185 @@
/*
* 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.riotredesign.features.home.room.detail.timeline.format

import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
import timber.log.Timber

class NoticeEventFormatter(private val stringProvider: StringProvider) {

fun format(timelineEvent: TimelineEvent): CharSequence? {
return when (timelineEvent.root.type) {
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName())
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderName)
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.senderName)
else -> {
Timber.v("Type ${timelineEvent.root.type} not handled by this formatter")
null
}
}
}

private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
val content = event.content.toModel<RoomNameContent>() ?: return null
return if (!TextUtils.isEmpty(content.name)) {
stringProvider.getString(R.string.notice_room_name_changed, senderName, content.name)
} else {
stringProvider.getString(R.string.notice_room_name_removed, senderName)
}
}

private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
val content = event.content.toModel<RoomTopicContent>() ?: return null
return if (content.topic.isNullOrEmpty()) {
stringProvider.getString(R.string.notice_room_topic_removed, senderName)
} else {
stringProvider.getString(R.string.notice_room_topic_changed, senderName, content.topic)
}
}

private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
val content = event.content.toModel<RoomHistoryVisibilityContent>() ?: return null
val formattedVisibility = when (content.historyVisibility) {
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited)
RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined)
RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable)
}
return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility)
}

private fun formatCallEvent(event: Event, senderName: String?): CharSequence? {
return when {
EventType.CALL_INVITE == event.type -> {
val content = event.content.toModel<CallInviteContent>() ?: return null
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
return if (isVideoCall) {
stringProvider.getString(R.string.notice_placed_video_call, senderName)
} else {
stringProvider.getString(R.string.notice_placed_voice_call, senderName)
}
}
EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, senderName)
EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName)
else -> null
}
}

private fun formatRoomMemberEvent(event: Event, senderName: String?): String? {
val eventContent: RoomMember? = event.content.toModel()
val prevEventContent: RoomMember? = event.prevContent.toModel()
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
return if (isMembershipEvent) {
buildMembershipNotice(event, senderName, eventContent, prevEventContent)
} else {
buildProfileNotice(event, senderName, eventContent, prevEventContent)
}
}

private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val displayText = StringBuilder()
// Check display name has been changed
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
val displayNameText = when {
prevEventContent?.displayName.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_display_name_set, event.sender, eventContent?.displayName)
eventContent?.displayName.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_display_name_removed, event.sender, prevEventContent?.displayName)
else ->
stringProvider.getString(R.string.notice_display_name_changed_from,
event.sender, prevEventContent?.displayName, eventContent?.displayName)
}
displayText.append(displayNameText)
}
// Check whether the avatar has been changed
if (!TextUtils.equals(eventContent?.avatarUrl, prevEventContent?.avatarUrl)) {
val displayAvatarText = if (displayText.isNotEmpty()) {
displayText.append(" ")
stringProvider.getString(R.string.notice_avatar_changed_too)
} else {
stringProvider.getString(R.string.notice_avatar_url_changed, senderName)
}
displayText.append(displayAvatarText)
}
return displayText.toString()
}

private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val senderDisplayName = senderName ?: event.sender
val targetDisplayName = eventContent?.displayName ?: event.sender
return when {
Membership.INVITE == eventContent?.membership -> {
// TODO get userId
val selfUserId = ""
when {
eventContent.thirdPartyInvite != null ->
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
TextUtils.equals(event.stateKey, selfUserId) ->
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
event.stateKey.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
else ->
stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
}
}
Membership.JOIN == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_join, senderDisplayName)
Membership.LEAVE == eventContent?.membership ->
// 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
return if (TextUtils.equals(event.sender, event.stateKey)) {
if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
} else {
stringProvider.getString(R.string.notice_room_leave, senderDisplayName)
}
} else if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.JOIN) {
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.BAN) {
stringProvider.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName)
} else {
null
}
Membership.BAN == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
Membership.KNOCK == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
else -> null
}
}

}

View File

@ -16,25 +16,11 @@

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

import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent

object RoomMemberEventHelper {

fun senderAvatar(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? {
return if (eventContent?.membership == Membership.LEAVE && eventContent.avatarUrl == null && prevEventContent?.avatarUrl != null) {
prevEventContent.avatarUrl
} else {
event.senderAvatar
}
}

fun senderName(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? {
return if (eventContent?.membership == Membership.LEAVE && eventContent.displayName == null && prevEventContent?.displayName != null) {
prevEventContent.displayName
} else {
event.senderName
}
}
}

View File

@ -19,8 +19,8 @@ package im.vector.riotredesign.features.home.room.detail.timeline.helper
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.extensions.localDateTime

@ -64,6 +64,26 @@ fun TimelineEvent.isDisplayable(): Boolean {
// }
//}

fun TimelineEvent.senderAvatar(): String? {
// We might have no avatar when user leave, so we try to get it from prevContent
return senderAvatar
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
root.prevContent.toModel<RoomMember>()?.avatarUrl
} else {
null
}
}

fun TimelineEvent.senderName(): String? {
// We might have no senderName when user leave, so we try to get it from prevContent
return senderName
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
root.prevContent.toModel<RoomMember>()?.displayName
} else {
null
}
}

fun TimelineEvent.canBeMerged(): Boolean {
return root.type == EventType.STATE_ROOM_MEMBER
}

View File

@ -0,0 +1,32 @@
/*
* 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.riotredesign.features.home.room.list

import im.vector.matrix.android.api.session.room.model.RoomSummary

class AlphabeticalRoomComparator
: Comparator<RoomSummary> {

override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
return when {
rightRoomSummary?.displayName == null -> -1
leftRoomSummary?.displayName == null -> 1
else -> leftRoomSummary.displayName.compareTo(rightRoomSummary.displayName)
}

}
}

View File

@ -0,0 +1,47 @@
/*
* 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.riotredesign.features.home.room.list

import im.vector.matrix.android.api.session.room.model.RoomSummary

class ChronologicalRoomComparator : Comparator<RoomSummary> {

override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
var rightTimestamp = 0L
var leftTimestamp = 0L
if (null != leftRoomSummary) {
leftTimestamp = leftRoomSummary.lastMessage?.originServerTs ?: 0
}
if (null != rightRoomSummary) {
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0
}
return if (rightRoomSummary?.lastMessage == null) {
-1
} else if (leftRoomSummary?.lastMessage == null) {
1
} else {
val deltaTimestamp = rightTimestamp - leftTimestamp
if (deltaTimestamp > 0) {
1
} else if (deltaTimestamp < 0) {
-1
} else {
0
}
}
}
}

View File

@ -41,7 +41,7 @@ abstract class RoomCategoryItem : VectorEpoxyModel<RoomCategoryItem.Holder>() {
val expandedArrowDrawable = ContextCompat.getDrawable(holder.rootView.context, expandedArrowDrawableRes)?.also {
DrawableCompat.setTint(it, tintColor)
}
holder.unreadCounterBadgeView.render(unreadCount, showHighlighted)
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadCount, showHighlighted))
holder.titleView.setCompoundDrawablesWithIntrinsicBounds(expandedArrowDrawable, null, null, null)
holder.titleView.text = title
holder.rootView.setOnClickListener { listener?.invoke() }

View File

@ -16,21 +16,17 @@

package im.vector.riotredesign.features.home.room.list

import android.content.SharedPreferences
import androidx.recyclerview.widget.DefaultItemAnimator

private const val SHARED_PREFS_SELECTED_ROOM_KEY = "SHARED_PREFS_SELECTED_ROOM_KEY"
private const val ANIM_DURATION_IN_MILLIS = 200L

class RoomSelectionRepository(private val sharedPreferences: SharedPreferences) {
class RoomListAnimator : DefaultItemAnimator() {

fun lastSelectedRoom(): String? {
return sharedPreferences.getString(SHARED_PREFS_SELECTED_ROOM_KEY, null)
init {
addDuration = ANIM_DURATION_IN_MILLIS
removeDuration = ANIM_DURATION_IN_MILLIS
moveDuration = 0
changeDuration = 0
}

fun saveLastSelectedRoom(roomId: String) {
sharedPreferences.edit()
.putString(SHARED_PREFS_SELECTED_ROOM_KEY, roomId)
.apply()
}

}

}

View File

@ -0,0 +1,32 @@
/*
* 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.riotredesign.features.home.room.list

import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import io.reactivex.functions.Predicate

class RoomListDisplayModeFilter(private val displayMode: RoomListFragment.DisplayMode) : Predicate<RoomSummary> {

override fun test(roomSummary: RoomSummary): Boolean {
return when (displayMode) {
RoomListFragment.DisplayMode.HOME -> roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE
RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
}
}
}

View File

@ -16,94 +16,255 @@

package im.vector.riotredesign.features.home.room.list

import android.animation.Animator
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.os.Parcelable
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.*
import com.google.android.material.floatingactionbutton.FloatingActionButton
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.animations.ANIMATION_DURATION_SHORT
import im.vector.riotredesign.core.animations.SimpleAnimatorListener
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.extensions.setupAsSearch
import im.vector.riotredesign.core.platform.OnBackPressed
import im.vector.riotredesign.core.platform.StateView
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomeNavigator
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_list.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope

class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback {
@Parcelize
data class RoomListParams(
val displayMode: RoomListFragment.DisplayMode
) : Parcelable


class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, OnBackPressed {

lateinit var fabButton: FloatingActionButton

private var isFabMenuOpened = false

enum class DisplayMode(@StringRes val titleRes: Int) {
HOME(R.string.bottom_action_home),
PEOPLE(R.string.bottom_action_people),
ROOMS(R.string.bottom_action_rooms)
}

companion object {
fun newInstance(): RoomListFragment {
return RoomListFragment()
fun newInstance(roomListParams: RoomListParams): RoomListFragment {
return RoomListFragment().apply {
setArguments(roomListParams)
}
}
}

private val roomListParams: RoomListParams by args()
private val roomController by inject<RoomSummaryController>()
private val homeNavigator by inject<HomeNavigator>()
private val roomListViewModel: RoomListViewModel by fragmentViewModel()

override fun getLayoutResId() = R.layout.fragment_room_list

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(HomeModule.ROOM_LIST_SCOPE))
setupCreateRoomButton()
setupRecyclerView()
setupFilterView()
roomListViewModel.subscribe { renderState(it) }
roomListViewModel.openRoomLiveData.observeEvent(this) {
homeNavigator.openRoomDetail(it, null)
navigator.openRoom(it)
}

isFabMenuOpened = false
}

private fun setupCreateRoomButton() {
fabButton = when (roomListParams.displayMode) {
DisplayMode.HOME -> createRoomButton
DisplayMode.PEOPLE -> createChatRoomButton
else -> createGroupRoomButton
}

fabButton.isVisible = true

createRoomButton.setOnClickListener {
toggleFabMenu()
}
createChatRoomButton.setOnClickListener {
createDirectChat()
}
createGroupRoomButton.setOnClickListener {
openRoomDirectory()
}

createRoomItemChat.setOnClickListener {
toggleFabMenu()
createDirectChat()
}
createRoomItemGroup.setOnClickListener {
toggleFabMenu()
openRoomDirectory()
}

createRoomTouchGuard.setOnClickListener {
toggleFabMenu()
}

createRoomTouchGuard.isClickable = false

// Hide FAB when list is scrolling
epoxyRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
fabButton.removeCallbacks(showFabRunnable)

when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
fabButton.postDelayed(showFabRunnable, 1000)
}
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {
fabButton.hide()
}
}
}
})
}

private fun toggleFabMenu() {
isFabMenuOpened = !isFabMenuOpened

if (isFabMenuOpened) {
createRoomItemChat.isVisible = true
createRoomItemGroup.isVisible = true

createRoomButton.animate()
.setDuration(ANIMATION_DURATION_SHORT)
.rotation(135f)
createRoomItemChat.animate()
.setDuration(ANIMATION_DURATION_SHORT)
.translationY(-resources.getDimension(R.dimen.fab_menu_offset_1))
createRoomItemGroup.animate()
.setDuration(ANIMATION_DURATION_SHORT)
.translationY(-resources.getDimension(R.dimen.fab_menu_offset_2))
createRoomTouchGuard.animate()
.setDuration(ANIMATION_DURATION_SHORT)
.alpha(0.6f)
.setListener(null)
createRoomTouchGuard.isClickable = true
} else {
createRoomButton.animate()
.setDuration(ANIMATION_DURATION_SHORT)
.rotation(0f)
createRoomItemChat.animate()
.setDuration(ANIMATION_DURATION_SHORT)
.translationY(0f)
createRoomItemGroup.animate()
.setDuration(ANIMATION_DURATION_SHORT)
.translationY(0f)
createRoomTouchGuard.animate()
.setDuration(ANIMATION_DURATION_SHORT)
.alpha(0f)
.setListener(object : SimpleAnimatorListener() {
override fun onAnimationCancel(animation: Animator?) {
animation?.removeListener(this)
}

override fun onAnimationEnd(animation: Animator?) {
// Use isFabMenuOpened because it may have been open meanwhile
createRoomItemChat.isVisible = isFabMenuOpened
createRoomItemGroup.isVisible = isFabMenuOpened
}
})
createRoomTouchGuard.isClickable = false
}
}

private fun openRoomDirectory() {
navigator.openRoomDirectory()
}

private fun createDirectChat() {
vectorBaseActivity.notImplemented("creating direct chat")
}

private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
epoxyRecyclerView.layoutManager = layoutManager
epoxyRecyclerView.itemAnimator = RoomListAnimator()
roomController.callback = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController)
}

private fun setupFilterView() {
filterRoomView.setupAsSearch()
filterRoomView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
roomListViewModel.accept(RoomListActions.FilterRooms(s))
}
})
private val showFabRunnable = Runnable {
fabButton.show()
}

private fun renderState(state: RoomListViewState) {
when (state.asyncRooms) {
when (state.asyncFilteredRooms) {
is Incomplete -> renderLoading()
is Success -> renderSuccess(state)
is Fail -> renderFailure(state.asyncRooms.error)
is Fail -> renderFailure(state.asyncFilteredRooms.error)
}
}

private fun renderSuccess(state: RoomListViewState) {
if (state.asyncRooms().isNullOrEmpty()) {
stateView.state = StateView.State.Empty(getString(R.string.room_list_empty))
val allRooms = state.asyncRooms()
val filteredRooms = state.asyncFilteredRooms()
if (filteredRooms.isNullOrEmpty()) {
renderEmptyState(allRooms)
} else {
stateView.state = StateView.State.Content
}
roomController.setData(state)
}

private fun renderEmptyState(allRooms: List<RoomSummary>?) {
val hasNoRoom = allRooms
?.filter {
it.membership == Membership.JOIN || it.membership == Membership.INVITE
}
.isNullOrEmpty()
val emptyState = when (roomListParams.displayMode) {
DisplayMode.HOME -> {
if (hasNoRoom) {
StateView.State.Empty(
getString(R.string.room_list_catchup_welcome_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_catchup),
getString(R.string.room_list_catchup_welcome_body)
)
} else {
StateView.State.Empty(
getString(R.string.room_list_catchup_empty_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_noun_party_popper),
getString(R.string.room_list_catchup_empty_body))
}
}
DisplayMode.PEOPLE ->
StateView.State.Empty(
getString(R.string.room_list_people_empty_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_chat),
getString(R.string.room_list_people_empty_body)
)
DisplayMode.ROOMS ->
StateView.State.Empty(
getString(R.string.room_list_rooms_empty_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_group),
getString(R.string.room_list_rooms_empty_body)
)
}
stateView.state = emptyState
}

private fun renderLoading() {
stateView.state = StateView.State.Loading
}
@ -116,6 +277,15 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback {
stateView.state = StateView.State.Error(message)
}

override fun onBackPressed(): Boolean {
if (isFabMenuOpened) {
toggleFabMenu()
return true
}

return super.onBackPressed()
}

// RoomSummaryController.Callback **************************************************************

override fun onRoomSelected(room: RoomSummary) {

View File

@ -23,28 +23,21 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import io.reactivex.Observable
import io.reactivex.functions.Function3
import im.vector.riotredesign.features.home.HomeRoomListObservableStore
import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit

typealias RoomListFilterName = CharSequence

class RoomListViewModel(initialState: RoomListViewState,
private val session: Session,
private val selectedGroupHolder: SelectedGroupStore,
private val visibleRoomHolder: VisibleRoomStore,
private val roomSelectionRepository: RoomSelectionRepository,
private val roomSummaryComparator: RoomSummaryComparator)
private val homeRoomListObservableSource: HomeRoomListObservableStore,
private val alphabeticalRoomComparator: AlphabeticalRoomComparator,
private val chronologicalRoomComparator: ChronologicalRoomComparator)
: VectorViewModel<RoomListViewState>(initialState) {

companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {
@ -52,15 +45,15 @@ class RoomListViewModel(initialState: RoomListViewState,
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? {
val currentSession = viewModelContext.activity.get<Session>()
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
val roomSummaryComparator = viewModelContext.activity.get<RoomSummaryComparator>()
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator)
val homeRoomListObservableSource = viewModelContext.activity.get<HomeRoomListObservableStore>()
val chronologicalRoomComparator = viewModelContext.activity.get<ChronologicalRoomComparator>()
val alphabeticalRoomComparator = viewModelContext.activity.get<AlphabeticalRoomComparator>()
return RoomListViewModel(state, currentSession, homeRoomListObservableSource, alphabeticalRoomComparator, chronologicalRoomComparator)
}
}


private val displayMode = initialState.displayMode
private val roomListDisplayModeFilter = RoomListDisplayModeFilter(displayMode)
private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty())

private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
@ -69,7 +62,6 @@ class RoomListViewModel(initialState: RoomListViewState,

init {
observeRoomSummaries()
observeVisibleRoom()
}

fun accept(action: RoomListActions) {
@ -82,11 +74,8 @@ class RoomListViewModel(initialState: RoomListViewState,

// PRIVATE METHODS *****************************************************************************

private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state ->
if (state.visibleRoomId != action.roomSummary.roomId) {
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
}
private fun handleSelectRoom(action: RoomListActions.SelectRoom) {
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
}

private fun handleFilterRooms(action: RoomListActions.FilterRooms) {
@ -98,61 +87,21 @@ class RoomListViewModel(initialState: RoomListViewState,
this.toggle(action.category)
}

private fun observeVisibleRoom() {
visibleRoomHolder.observe()
.doOnNext {
setState { copy(visibleRoomId = it) }
}
.subscribe()
.disposeOnClear()
}

private fun observeRoomSummaries() {
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>(
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
selectedGroupHolder.observe(),
roomListFilter.throttleLast(300, TimeUnit.MILLISECONDS),
Function3 { rooms, selectedGroupOption, filterRoomOption ->
val filteredRooms = filterRooms(rooms, filterRoomOption)
val selectedGroup = selectedGroupOption.orNull()
val filteredDirectRooms = filteredRooms
.filter { it.isDirect }
.filter {
if (selectedGroup == null) {
true
} else {
it.otherMemberIds
.intersect(selectedGroup.userIds)
.isNotEmpty()
}
}

val filteredGroupRooms = filteredRooms
.filter { !it.isDirect }
.filter {
selectedGroup?.roomIds?.contains(it.roomId) ?: true
}
buildRoomSummaries(filteredDirectRooms + filteredGroupRooms)
homeRoomListObservableSource
.observe()
.execute { asyncRooms ->
copy(asyncRooms = asyncRooms)
}
)

homeRoomListObservableSource.observeFilteredBy(displayMode)
.map { buildRoomSummaries(it) }
.execute { async ->
copy(
asyncRooms = async
)
copy(asyncFilteredRooms = async)
}
}

private fun filterRooms(rooms: List<RoomSummary>, filterRoomOption: Option<RoomListFilterName>): List<RoomSummary> {
val filterRoom = filterRoomOption.orNull()
return rooms.filter {
if (filterRoom.isNullOrBlank()) {
true
} else {
it.displayName.contains(other = filterRoom, ignoreCase = true)
}
}
}

private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>()
@ -174,13 +123,19 @@ class RoomListViewModel(initialState: RoomListViewState,
}
}

val roomComparator = when (displayMode) {
RoomListFragment.DisplayMode.HOME -> chronologicalRoomComparator
RoomListFragment.DisplayMode.PEOPLE -> chronologicalRoomComparator
RoomListFragment.DisplayMode.ROOMS -> alphabeticalRoomComparator
}

return RoomSummaries().apply {
put(RoomCategory.INVITE, invites.sortedWith(roomSummaryComparator))
put(RoomCategory.FAVOURITE, favourites.sortedWith(roomSummaryComparator))
put(RoomCategory.DIRECT, directChats.sortedWith(roomSummaryComparator))
put(RoomCategory.GROUP, groupRooms.sortedWith(roomSummaryComparator))
put(RoomCategory.LOW_PRIORITY, lowPriorities.sortedWith(roomSummaryComparator))
put(RoomCategory.SERVER_NOTICE, serverNotices.sortedWith(roomSummaryComparator))
put(RoomCategory.INVITE, invites.sortedWith(roomComparator))
put(RoomCategory.FAVOURITE, favourites.sortedWith(roomComparator))
put(RoomCategory.DIRECT, directChats.sortedWith(roomComparator))
put(RoomCategory.GROUP, groupRooms.sortedWith(roomComparator))
put(RoomCategory.LOW_PRIORITY, lowPriorities.sortedWith(roomComparator))
put(RoomCategory.SERVER_NOTICE, serverNotices.sortedWith(roomComparator))
}
}


View File

@ -24,8 +24,9 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R

data class RoomListViewState(
val asyncRooms: Async<RoomSummaries> = Uninitialized,
val visibleRoomId: String? = null,
val displayMode: RoomListFragment.DisplayMode,
val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val asyncFilteredRooms: Async<RoomSummaries> = Uninitialized,
val isInviteExpanded: Boolean = true,
val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = true,
@ -34,6 +35,8 @@ data class RoomListViewState(
val isServerNoticeRoomsExpanded: Boolean = true
) : MvRxState {

constructor(args: RoomListParams) : this(displayMode = args.displayMode)

fun isCategoryExpanded(roomCategory: RoomCategory): Boolean {
return when (roomCategory) {
RoomCategory.INVITE -> isInviteExpanded
@ -69,5 +72,5 @@ enum class RoomCategory(@StringRes val titleRes: Int) {
}

fun RoomSummaries?.isNullOrEmpty(): Boolean {
return this == null || isEmpty()
return this == null || this.values.flatten().isEmpty()
}

View File

@ -18,16 +18,25 @@ package im.vector.riotredesign.features.home.room.list

import androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.core.resources.DateProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter

class RoomSummaryController(private val stringProvider: StringProvider
class RoomSummaryController(private val stringProvider: StringProvider,
private val eventFormatter: NoticeEventFormatter,
private val timelineDateFormatter: TimelineDateFormatter
) : TypedEpoxyController<RoomListViewState>() {

var callback: Callback? = null

override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncRooms()
val roomSummaries = viewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) {
return@forEach
@ -37,7 +46,7 @@ class RoomSummaryController(private val stringProvider: StringProvider
callback?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries, viewState.visibleRoomId)
buildRoomModels(summaries)
}
}
}
@ -71,18 +80,41 @@ class RoomSummaryController(private val stringProvider: StringProvider
}
}

private fun buildRoomModels(summaries: List<RoomSummary>, selectedRoomId: String?) {
private fun buildRoomModels(summaries: List<RoomSummary>) {
summaries.forEach { roomSummary ->
val unreadCount = roomSummary.notificationCount
val showHighlighted = roomSummary.highlightCount > 0
val isSelected = roomSummary.roomId == selectedRoomId

var lastMessageFormatted: CharSequence = ""
var lastMessageTime: CharSequence = ""
val lastMessage = roomSummary.lastMessage
if (lastMessage != null) {
val date = lastMessage.localDateTime()
val currentData = DateProvider.currentLocalDateTime()
val isSameDay = date.toLocalDate() == currentData.toLocalDate()
//TODO: get formatted
if (lastMessage.type == EventType.MESSAGE) {
val content = lastMessage.content?.toModel<MessageContent>()
lastMessageFormatted = content?.body ?: ""
} else {
lastMessageFormatted = lastMessage.type
}
lastMessageTime = if (isSameDay) {
timelineDateFormatter.formatMessageHour(date)
} else {
//TODO: change this
timelineDateFormatter.formatMessageDay(date)
}


}
roomSummaryItem {
id(roomSummary.roomId)
roomId(roomSummary.roomId)
lastEventTime(lastMessageTime)
lastFormattedEvent(lastMessageFormatted)
roomName(roomSummary.displayName)
avatarUrl(roomSummary.avatarUrl)
selected(isSelected)
showHighlighted(showHighlighted)
unreadCount(unreadCount)
listener { callback?.onRoomSelected(roomSummary) }

View File

@ -26,7 +26,7 @@ object RoomSummaryFormatter {
*/
fun formatUnreadMessagesCounter(count: Int): String {
return if (count > 999) {
"${count / 1000}.${count % 1000 / 100}K"
"${count / 1000}.${count % 1000 / 100}k"
} else {
count.toString()
}

View File

@ -16,6 +16,7 @@

package im.vector.riotredesign.features.home.room.list

import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
@ -23,7 +24,6 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.platform.CheckableFrameLayout
import im.vector.riotredesign.features.home.AvatarRenderer


@ -32,8 +32,9 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {

@EpoxyAttribute lateinit var roomName: CharSequence
@EpoxyAttribute lateinit var roomId: String
@EpoxyAttribute lateinit var lastFormattedEvent: CharSequence
@EpoxyAttribute lateinit var lastEventTime: CharSequence
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var unreadCount: Int = 0
@EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
@ -41,18 +42,21 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {

override fun bind(holder: Holder) {
super.bind(holder)
holder.unreadCounterBadgeView.render(unreadCount, showHighlighted)
holder.rootView.isChecked = selected
holder.rootView.setOnClickListener { listener?.invoke() }
holder.titleView.text = roomName
holder.lastEventTimeView.text = lastEventTime
holder.lastEventView.text = lastFormattedEvent
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadCount, showHighlighted))
AvatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
}

class Holder : VectorEpoxyHolder() {
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
val titleView by bind<TextView>(R.id.roomNameView)
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
val lastEventView by bind<TextView>(R.id.roomLastEventView)
val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
val rootView by bind<CheckableFrameLayout>(R.id.itemRoomLayout)
val rootView by bind<ViewGroup>(R.id.itemRoomLayout)
}

}

View File

@ -29,24 +29,24 @@ class UnreadCounterBadgeView : AppCompatTextView {

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

fun render(count: Int, highlighted: Boolean) {
if (count == 0) {
fun render(state: State) {
if (state.count == 0) {
visibility = View.INVISIBLE
} else {
visibility = View.VISIBLE
val bgRes = if (highlighted) {
val bgRes = if (state.highlighted) {
R.drawable.bg_unread_highlight
} else {
R.drawable.bg_unread_notification
}
setBackgroundResource(bgRes)
text = RoomSummaryFormatter.formatUnreadMessagesCounter(count)
text = RoomSummaryFormatter.formatUnreadMessagesCounter(state.count)
}
}

enum class Status {
NOTIFICATION,
HIGHLIGHT
}
data class State(
val count: Int,
val highlighted: Boolean
)

}

View File

@ -21,7 +21,6 @@ import android.graphics.Color
import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R
@ -52,13 +51,13 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib

fun render(sender: User, mode: Mode = Mode.LARGE) {
if (mode == Mode.LARGE) {
updateLayoutParams { height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT }
updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT }
AvatarRenderer.render(sender.avatarUrl, sender.userId, sender.displayName, inviteAvatarView)
inviteIdentifierView.text = sender.userId
inviteNameView.text = sender.displayName
inviteLabelView.text = context.getString(R.string.send_you_invite)
} else {
updateLayoutParams { height = ConstraintLayout.LayoutParams.WRAP_CONTENT }
updateLayoutParams { height = LayoutParams.WRAP_CONTENT }
inviteAvatarView.visibility = View.GONE
inviteIdentifierView.visibility = View.GONE
inviteNameView.visibility = View.GONE

View File

@ -0,0 +1,53 @@
/*
* 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.riotredesign.features.navigation

import android.app.Activity
import android.content.Intent
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotredesign.features.home.room.detail.RoomDetailActivity
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotredesign.features.roomdirectory.roompreview.RoomPreviewActivity
import im.vector.riotredesign.features.settings.VectorSettingsActivity

class DefaultNavigator(private val fraqment: Fragment) : Navigator {

val activity: Activity = fraqment.requireActivity()

override fun openRoom(roomId: String) {
val args = RoomDetailArgs(roomId)
val intent = RoomDetailActivity.newIntent(activity, args)
activity.startActivity(intent)
}

override fun openRoomPreview(publicRoom: PublicRoom) {
val intent = RoomPreviewActivity.getIntent(activity, publicRoom)
activity.startActivity(intent)
}

override fun openRoomDirectory() {
val intent = Intent(activity, RoomDirectoryActivity::class.java)
activity.startActivity(intent)
}

override fun openSettings() {
val intent = VectorSettingsActivity.getIntent(activity, "TODO")
activity.startActivity(intent)
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.riotredesign.features.navigation

import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom

interface Navigator {

fun openRoom(roomId: String)

fun openRoomPreview(publicRoom: PublicRoom)

fun openRoomDirectory()

fun openSettings()

}

View File

@ -27,6 +27,7 @@ import butterknife.OnCheckedChanged
import butterknife.OnTextChanged
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_bug_report.*
import timber.log.Timber

/**
@ -68,7 +69,7 @@ class BugReportActivity : VectorBaseActivity() {
override fun getLayoutRes() = R.layout.activity_bug_report

override fun initUiAndData() {
configureToolbar()
configureToolbar(bugReportToolbar)

if (BugReporter.screenshot != null) {
mScreenShotPreview.setImageBitmap(BugReporter.screenshot)

View File

@ -34,6 +34,7 @@ import androidx.lifecycle.ViewModelProviders
import com.google.android.material.tabs.TabLayout
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.*
import timber.log.Timber

/**
@ -80,8 +81,7 @@ class EmojiReactionPickerActivity : VectorBaseActivity() {
}

override fun initUiAndData() {

configureToolbar()
configureToolbar(emojiPickerToolbar)

requestEmojivUnicode10CompatibleFont()


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.riotredesign.features.roomdirectory

/**
* Join state of a room
*/
enum class JoinState {
NOT_JOINED,
JOINING,
JOINING_ERROR,
// Room is joined and this is confirmed by the sync
JOINED
}

View File

@ -24,19 +24,13 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.setTextOrHide
import im.vector.riotredesign.core.platform.ButtonStateView
import im.vector.riotredesign.features.home.AvatarRenderer

@EpoxyModelClass(layout = R.layout.item_public_room)
abstract class PublicRoomItem : VectorEpoxyModel<PublicRoomItem.Holder>() {

enum class JoinState {
NOT_JOINED,
JOINING,
JOINING_ERROR,
JOINED
}

@EpoxyAttribute
var avatarUrl: String? = null

@ -46,6 +40,12 @@ abstract class PublicRoomItem : VectorEpoxyModel<PublicRoomItem.Holder>() {
@EpoxyAttribute
var roomName: String? = null

@EpoxyAttribute
var roomAlias: String? = null

@EpoxyAttribute
var roomTopic: String? = null

@EpoxyAttribute
var nbOfMembers: Int = 0

@ -63,6 +63,8 @@ abstract class PublicRoomItem : VectorEpoxyModel<PublicRoomItem.Holder>() {

AvatarRenderer.render(avatarUrl, roomId!!, roomName, holder.avatarView)
holder.nameView.text = roomName
holder.aliasView.setTextOrHide(roomAlias)
holder.topicView.setTextOrHide(roomTopic)
// TODO Use formatter for big numbers?
holder.counterView.text = nbOfMembers.toString()

@ -92,6 +94,8 @@ abstract class PublicRoomItem : VectorEpoxyModel<PublicRoomItem.Holder>() {

val avatarView by bind<ImageView>(R.id.itemPublicRoomAvatar)
val nameView by bind<TextView>(R.id.itemPublicRoomName)
val aliasView by bind<TextView>(R.id.itemPublicRoomAlias)
val topicView by bind<TextView>(R.id.itemPublicRoomTopic)
val counterView by bind<TextView>(R.id.itemPublicRoomMembersCount)

val buttonState by bind<ButtonStateView>(R.id.itemPublicRoomButtonState)

View File

@ -82,24 +82,30 @@ class PublicRoomsController(private val stringProvider: StringProvider,
roomId(publicRoom.roomId)
avatarUrl(publicRoom.avatarUrl)
roomName(publicRoom.name)
roomAlias(publicRoom.canonicalAlias)
roomTopic(publicRoom.topic)
nbOfMembers(publicRoom.numJoinedMembers)
when {
viewState.joinedRoomsIds.contains(publicRoom.roomId) -> joinState(PublicRoomItem.JoinState.JOINED)
viewState.joiningRoomsIds.contains(publicRoom.roomId) -> joinState(PublicRoomItem.JoinState.JOINING)
viewState.joiningErrorRoomsIds.contains(publicRoom.roomId) -> joinState(PublicRoomItem.JoinState.JOINING_ERROR)
else -> joinState(PublicRoomItem.JoinState.NOT_JOINED)

val joinState = when {
viewState.joinedRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINED
viewState.joiningRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINING
viewState.joiningErrorRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINING_ERROR
else -> JoinState.NOT_JOINED
}

joinState(joinState)

joinListener {
callback?.onPublicRoomJoin(publicRoom)
}
globalListener {
callback?.onPublicRoomClicked(publicRoom)
callback?.onPublicRoomClicked(publicRoom, joinState)
}
}
}

interface Callback {
fun onPublicRoomClicked(publicRoom: PublicRoom)
fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState)
fun onPublicRoomJoin(publicRoom: PublicRoom)
fun loadMore()
}

View File

@ -17,6 +17,7 @@
package im.vector.riotredesign.features.roomdirectory

import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
@ -43,14 +44,6 @@ import java.util.concurrent.TimeUnit
/**
* What can be improved:
* - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect
*
* TODO For Nad:
* Display number of rooms?
* Picto size are not correct
* Where I put the room directory picker?
* World Readable badge
* Guest can join badge
*
*/
class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback {

@ -60,6 +53,8 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback

override fun getLayoutResId() = R.layout.fragment_public_rooms

override fun getMenuRes() = R.menu.menu_room_directory

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

@ -83,10 +78,6 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
vectorBaseActivity.notImplemented()
}

publicRoomsChangeDirectory.setOnClickListener {
vectorBaseActivity.addFragmentToBackstack(RoomDirectoryPickerFragment(), R.id.simpleFragmentContainer)
}

viewModel.joinRoomErrorLiveData.observe(this, Observer {
it.getContentIfNotHandled()?.let { throwable ->
Snackbar.make(publicRoomsCoordinator, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT)
@ -95,6 +86,17 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
})
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_room_directory_change_protocol -> {
vectorBaseActivity.addFragmentToBackstack(RoomDirectoryPickerFragment(), R.id.simpleFragmentContainer)
true
}
else ->
super.onOptionsItemSelected(item)
}
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))
@ -114,9 +116,23 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
publicRoomsList.setController(publicRoomsController)
}

override fun onPublicRoomClicked(publicRoom: PublicRoom) {
override fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) {
Timber.v("PublicRoomClicked: $publicRoom")
vectorBaseActivity.notImplemented()

when (joinState) {
JoinState.JOINED -> {
navigator.openRoom(publicRoom.roomId)
}
JoinState.NOT_JOINED,
JoinState.JOINING_ERROR -> {
// ROOM PREVIEW
navigator.openRoomPreview(publicRoom)
}
else -> {
Snackbar.make(publicRoomsCoordinator, getString(R.string.please_wait), Snackbar.LENGTH_SHORT)
.show()
}
}
}

override fun onPublicRoomJoin(publicRoom: PublicRoom) {
@ -131,8 +147,5 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
override fun invalidate() = withState(viewModel) { state ->
// Populate list with Epoxy
publicRoomsController.setData(state)

// Directory name
publicRoomsDirectoryName.text = state.roomDirectoryDisplayName
}
}

View File

@ -82,22 +82,25 @@ class RoomDirectoryViewModel(initialState: PublicRoomsViewState,
session
.rx()
.liveRoomSummaries()
.execute { async ->
val joinedRoomIds = async.invoke()
.subscribe { list ->
val joinedRoomIds = list
// Keep only joined room
?.filter { it.membership == Membership.JOIN }
?.map { it.roomId }
?.toList()
?: emptyList()

copy(
joinedRoomsIds = joinedRoomIds,
// Remove (newly) joined room id from the joining room list
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) },
// Remove (newly) joined room id from the joining room list in error
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) }
)
setState {
copy(
joinedRoomsIds = joinedRoomIds,
// Remove (newly) joined room id from the joining room list
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) },
// Remove (newly) joined room id from the joining room list in error
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) }
)
}
}
.disposeOnClear()
}

fun setRoomDirectoryData(roomDirectoryData: RoomDirectoryData) {

View File

@ -41,7 +41,7 @@ class RoomDirectoryPickerController(private val stringProvider: StringProvider,

when (asyncThirdPartyProtocol) {
is Success -> {
val directories = roomDirectoryListCreator.computeDirectories(asyncThirdPartyProtocol.invoke())
val directories = roomDirectoryListCreator.computeDirectories(asyncThirdPartyProtocol())

directories.forEach {
buildDirectory(it)
@ -88,7 +88,7 @@ class RoomDirectoryPickerController(private val stringProvider: StringProvider,
}

interface Callback {
fun onRoomDirectoryClicked(roomDirectory: RoomDirectoryData)
fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData)
fun retry()
}


View File

@ -61,7 +61,7 @@ class RoomDirectoryPickerFragment : VectorBaseFragment(), RoomDirectoryPickerCon
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.action_add_custom_hs) {
// TODO
vectorBaseActivity.notImplemented()
vectorBaseActivity.notImplemented("Entering custom homeserver")
return true
}


View File

@ -0,0 +1,88 @@
/*
* 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.riotredesign.features.roomdirectory.roompreview

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.widget.Toolbar
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.addFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
import kotlinx.android.parcel.Parcelize
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope

@Parcelize
data class RoomPreviewData(
val roomId: String,
val roomName: String?,
val topic: String?,
val worldReadable: Boolean,
val avatarUrl: String?
) : Parcelable


class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {

companion object {
private const val ARG = "ARG"

fun getIntent(context: Context, publicRoom: PublicRoom): Intent {
return Intent(context, RoomPreviewActivity::class.java).apply {
putExtra(ARG, RoomPreviewData(
roomId = publicRoom.roomId,
roomName = publicRoom.name,
topic = publicRoom.topic,
worldReadable = publicRoom.worldReadable,
avatarUrl = publicRoom.avatarUrl
))
}
}
}

override fun getLayoutRes() = R.layout.activity_simple

override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))
}

override fun initUiAndData() {
if (isFirstCreation()) {
val args = intent.getParcelableExtra<RoomPreviewData>(ARG)

if (args.worldReadable) {
// TODO Room preview: Note: M does not recommend to use /events anymore, so for now we just display the room preview
// TODO the same way if it was not world readable
addFragment(RoomPreviewNoPreviewFragment.newInstance(args), R.id.simpleFragmentContainer)
} else {
addFragment(RoomPreviewNoPreviewFragment.newInstance(args), R.id.simpleFragmentContainer)
}
}
}

}

View File

@ -0,0 +1,114 @@
/*
* 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.riotredesign.features.roomdirectory.roompreview

import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.transition.TransitionManager
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotredesign.R
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.extensions.setTextOrHide
import im.vector.riotredesign.core.platform.ButtonStateView
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.roomdirectory.JoinState
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
import kotlinx.android.synthetic.main.fragment_room_preview_no_preview.*
import org.koin.android.ext.android.get
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope

/**
* Note: this Fragment is also used for world readable room for the moment
*/
class RoomPreviewNoPreviewFragment : VectorBaseFragment() {

companion object {
fun newInstance(arg: RoomPreviewData): Fragment {
return RoomPreviewNoPreviewFragment().apply { setArguments(arg) }
}
}

private val errorFormatter = get<ErrorFormatter>()
private val roomPreviewViewModel: RoomPreviewViewModel by fragmentViewModel()

private val roomPreviewData: RoomPreviewData by args()

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))
setupToolbar(roomPreviewNoPreviewToolbar)
}

override fun getLayoutResId() = R.layout.fragment_room_preview_no_preview

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// Toolbar
AvatarRenderer.render(roomPreviewData.avatarUrl, roomPreviewData.roomId, roomPreviewData.roomName, roomPreviewNoPreviewToolbarAvatar)
roomPreviewNoPreviewToolbarTitle.text = roomPreviewData.roomName

// Screen
AvatarRenderer.render(roomPreviewData.avatarUrl, roomPreviewData.roomId, roomPreviewData.roomName, roomPreviewNoPreviewAvatar)
roomPreviewNoPreviewName.text = roomPreviewData.roomName
roomPreviewNoPreviewTopic.setTextOrHide(roomPreviewData.topic)

if (roomPreviewData.worldReadable) {
roomPreviewNoPreviewLabel.setText(R.string.room_preview_world_readable_room_not_supported_yet)
} else {
roomPreviewNoPreviewLabel.setText(R.string.room_preview_no_preview)
}

roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
roomPreviewViewModel.joinRoom()
}

override fun onRetryClicked() {
// Same action
onButtonClicked()
}
}
}

override fun invalidate() = withState(roomPreviewViewModel) { state ->
TransitionManager.beginDelayedTransition(roomPreviewNoPreviewRoot)

roomPreviewNoPreviewJoin.render(
when (state.roomJoinState) {
JoinState.NOT_JOINED -> ButtonStateView.State.Button
JoinState.JOINING -> ButtonStateView.State.Loading
JoinState.JOINED -> ButtonStateView.State.Loaded
JoinState.JOINING_ERROR -> ButtonStateView.State.Error
}
)

roomPreviewNoPreviewError.setTextOrHide(errorFormatter.toHumanReadable(state.lastError))

if (state.roomJoinState == JoinState.JOINED) {
// Quit this screen
requireActivity().finish()
// Open room
navigator.openRoom(roomPreviewData.roomId)
}
}
}

View File

@ -0,0 +1,104 @@
/*
* 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.riotredesign.features.roomdirectory.roompreview

import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.features.roomdirectory.JoinState
import org.koin.android.ext.android.get
import timber.log.Timber

class RoomPreviewViewModel(initialState: RoomPreviewViewState,
private val session: Session) : VectorViewModel<RoomPreviewViewState>(initialState) {

companion object : MvRxViewModelFactory<RoomPreviewViewModel, RoomPreviewViewState> {

@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomPreviewViewState): RoomPreviewViewModel? {
val currentSession = viewModelContext.activity.get<Session>()

return RoomPreviewViewModel(state, currentSession)
}
}

init {
// Observe joined room (from the sync)
observeJoinedRooms()
}

private fun observeJoinedRooms() {
session
.rx()
.liveRoomSummaries()
.subscribe { list ->
withState { state ->
val isRoomJoined = list
// Keep only joined room
?.filter { it.membership == Membership.JOIN }
?.map { it.roomId }
?.toList()
?.contains(state.roomId) == true

if (isRoomJoined) {
setState {
copy(
roomJoinState = JoinState.JOINED
)
}
}
}
}
.disposeOnClear()
}

fun joinRoom() = withState { state ->
if (state.roomJoinState == JoinState.JOINING) {
// Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}

setState {
copy(
roomJoinState = JoinState.JOINING,
lastError = null
)
}

session.joinRoom(state.roomId, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined
}

override fun onFailure(failure: Throwable) {
setState {
copy(
roomJoinState = JoinState.JOINING_ERROR,
lastError = failure
)
}
}
})
}

}

View File

@ -0,0 +1,32 @@
/*
* 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.riotredesign.features.roomdirectory.roompreview

import com.airbnb.mvrx.MvRxState
import im.vector.riotredesign.features.roomdirectory.JoinState

data class RoomPreviewViewState(
// The room id
val roomId: String = "",
// Current state of the room in preview
val roomJoinState: JoinState = JoinState.NOT_JOINED,
// Last error of join room request
val lastError: Throwable? = null
) : MvRxState {

constructor(args: RoomPreviewData) : this(roomId = args.roomId)
}

View File

@ -24,6 +24,7 @@ import androidx.preference.PreferenceFragmentCompat
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_vector_settings.*
import org.koin.android.ext.android.inject

/**
@ -45,7 +46,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
private val session by inject<Session>()

override fun initUiAndData() {
configureToolbar()
configureToolbar(settingsToolbar)

if (isFirstCreation()) {
vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId)

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorAccent" android:state_checked="true" />
<item android:color="@color/vector_silver_color" />
<item android:color="#7E899C" />
</selector>

View File

@ -3,7 +3,7 @@

<item android:state_checked="true">
<shape>
<solid android:color="@android:color/white" />
<solid android:color="#10000000" />
<corners android:radius="4dp" />
</shape>
</item>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
android:shape="rectangle">

<corners android:radius="40dp" />

<solid android:color="@color/rosy_pink" />
</shape>

View File

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>

<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
android:shape="rectangle">

<corners android:radius="40dp" />

<solid android:color="@color/grey_lynch" />
</shape>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="7dp"
android:height="12dp"
android:viewportWidth="7"
android:viewportHeight="12">
<path
android:pathData="M1,11l5,-5 -5,-5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="14"
android:viewportHeight="14">
<path
android:pathData="M6,0v6H0v2h6v6h2V8h6V6H8V0z"
android:fillColor="#FFF"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="34dp"
android:height="20dp"
android:viewportWidth="34"
android:viewportHeight="20">
<path
android:pathData="M19,9.5a8.38,8.38 0,0 1,-0.9 3.8,8.5 8.5,0 0,1 -7.6,4.7 8.38,8.38 0,0 1,-3.8 -0.9L1,19l1.9,-5.7A8.38,8.38 0,0 1,2 9.5a8.5,8.5 0,0 1,4.7 -7.6,8.38 8.38,0 0,1 3.8,-0.9h0.5a8.48,8.48 0,0 1,8 8v0.5z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#FFF"
android:strokeLineCap="round"/>
<path
android:pathData="M28.5,6v8M24.5,10h8"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#FFF"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="20dp"
android:viewportWidth="32"
android:viewportHeight="20">
<path
android:pathData="M1,7h16M1,13h16M7,1L5,19M13,1l-2,18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#FFF"
android:strokeLineCap="round"/>
<path
android:pathData="M26.5,6v8M22.5,10h8"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#FFF"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="22dp"
android:viewportWidth="20"
android:viewportHeight="22">
<path
android:pathData="M1,8l9,-7 9,7v11a2,2 0,0 1,-2 2H3a2,2 0,0 1,-2 -2V8z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#7E899C"
android:strokeLineCap="round"/>
<path
android:pathData="M7,21V11h6v10"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#7E899C"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M19,9.5a8.38,8.38 0,0 1,-0.9 3.8,8.5 8.5,0 0,1 -7.6,4.7 8.38,8.38 0,0 1,-3.8 -0.9L1,19l1.9,-5.7A8.38,8.38 0,0 1,2 9.5a8.5,8.5 0,0 1,4.7 -7.6,8.38 8.38,0 0,1 3.8,-0.9h0.5a8.48,8.48 0,0 1,8 8v0.5z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#7E899C"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="20dp"
android:viewportWidth="18"
android:viewportHeight="20">
<path
android:pathData="M1,7h16M1,13h16M7,1L5,19M13,1l-2,18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#7E899C"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M62.382,26.685c-10.46,-2.55 -18.09,-0.21 -23,2.6 -0.82,-1 -1.68,-1.91 -2.58,-2.81 -0.9,-0.9 -1.79,-1.79 -2.74,-2.56a26.81,26.81 0,0 0,3.42 -20.23,2 2,0 1,0 -3.9,0.87 22.93,22.93 0,0 1,-2.67 16.93c-5.05,-3.46 -10.18,-5.07 -13.08,-2.64l-0.32,0.26a2,2 0,0 0,-0.22 0.27,5.05 5.05,0 0,0 -1.12,2.6l-16,38.57a2,2 0,0 0,1.74 2.81,1.89 1.89,0 0,0 0.77,-0.15l38.56,-16a5.06,5.06 0,0 0,2.6 -1.13,1.51 1.51,0 0,0 0.28,-0.22c0.1,-0.094 0.188,-0.202 0.26,-0.32 2.41,-2.88 0.85,-7.95 -2.56,-13 4.23,-2.28 10.73,-4.12 19.58,-2a2,2 0,0 0,0.98 -3.85zM22.682,50.585l-3.05,-18.38a42.26,42.26 0,0 0,5.18 6.26,37.92 37.92,0 0,0 9.7,7.22l-11.83,4.9zM12.312,54.875l-1.58,-9.52 5,-12.08 3.18,18.86 -6.6,2.74zM7.832,52.325l0.68,4.12 -2.89,1.24 2.21,-5.36zM41.482,42.745l-1.26,0.52c-2.31,-0.05 -7.31,-2.32 -12.58,-7.58 -5.27,-5.26 -7.58,-10.31 -7.64,-12.59l0.52,-1.26a2,2 0,0 1,0.8 -0.14c1.45,0 4.11,1 7.27,3.11a19.35,19.35 0,0 1,-2.2 2.33,2 2,0 0,0 2.59,3.05 24.05,24.05 0,0 0,2.77 -2.94c0.74,0.64 1.48,1.33 2.24,2.08 0.76,0.75 1.45,1.51 2.08,2.25a20.09,20.09 0,0 0,-3.32 3.24,2 2,0 0,0 0.38,2.8 2,2 0,0 0,2.78 -0.43,17.14 17.14,0 0,1 2.61,-2.5c2.64,3.89 3.48,7 2.96,8.06zM43.482,20.125l10.3,-10.34a2.001,2.001 0,0 1,2.83 2.83l-10.33,10.34a2,2 0,0 1,-1.42 0.58,2 2,0 0,1 -1.41,-0.58 2,2 0,0 1,0 -2.83h0.03zM42.322,2.215a2.01,2.01 0,1 1,4 -0.41l0.74,7.09a2,2 0,0 1,-1.78 2.2h-0.21a2,2 0,0 1,-2 -1.79l-0.75,-7.09z"
android:fillColor="#7E899C"
android:fillType="nonZero"/>
</vector>

View File

@ -1,13 +1,22 @@
<vector android:height="24dp" android:viewportHeight="22"
android:viewportWidth="22" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:fillType="evenOdd"
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#00000000"
android:fillType="evenOdd"
android:pathData="M11,11m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeColor="#03B381" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.4"/>
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M11,7L11,15" android:strokeColor="#03B381"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="1.4"/>
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M7,11L15,11" android:strokeColor="#03B381"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="1.4"/>
android:strokeWidth="1.4"
android:strokeColor="#03B381"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:fillType="evenOdd"
android:pathData="M11,7v8M7,11h8"
android:strokeWidth="1.4"
android:strokeColor="#03B381"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M10,10m-2.455,0a2.455,2.455 0,1 1,4.91 0a2.455,2.455 0,1 1,-4.91 0"
android:strokeLineJoin="round"
android:strokeWidth="1.2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#FFF"
android:strokeLineCap="round"/>
<path
android:pathData="M16.055,12.455a1.35,1.35 0,0 0,0.27 1.489l0.049,0.049a1.636,1.636 0,1 1,-2.316 2.315l-0.049,-0.049a1.35,1.35 0,0 0,-1.489 -0.27,1.35 1.35,0 0,0 -0.818,1.236v0.139a1.636,1.636 0,0 1,-3.273 0v-0.074a1.35,1.35 0,0 0,-0.884 -1.235,1.35 1.35,0 0,0 -1.489,0.27l-0.049,0.049a1.636,1.636 0,1 1,-2.315 -2.316l0.049,-0.049a1.35,1.35 0,0 0,0.27 -1.489,1.35 1.35,0 0,0 -1.236,-0.818h-0.139a1.636,1.636 0,0 1,0 -3.273h0.074a1.35,1.35 0,0 0,1.235 -0.884,1.35 1.35,0 0,0 -0.27,-1.489l-0.049,-0.049a1.636,1.636 0,1 1,2.316 -2.315l0.049,0.049a1.35,1.35 0,0 0,1.489 0.27h0.065a1.35,1.35 0,0 0,0.819 -1.236v-0.139a1.636,1.636 0,0 1,3.272 0v0.074a1.35,1.35 0,0 0,0.819 1.235,1.35 1.35,0 0,0 1.489,-0.27l0.049,-0.049a1.636,1.636 0,1 1,2.315 2.316l-0.049,0.049a1.35,1.35 0,0 0,-0.27 1.489v0.065a1.35,1.35 0,0 0,1.236 0.819h0.139a1.636,1.636 0,0 1,0 3.272h-0.074a1.35,1.35 0,0 0,-1.235 0.819z"
android:strokeLineJoin="round"
android:strokeWidth="1.2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#FFF"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,9 +1,13 @@
<vector android:height="24dp" android:viewportHeight="11"
android:viewportWidth="15" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M5.3033,10.0815L13.7886,1.5962"
android:strokeColor="#7E899C" android:strokeLineCap="round" android:strokeWidth="1.3"/>
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M5.3033,10.0815L1.0607,5.8388"
android:strokeColor="#7E899C" android:strokeLineCap="round" android:strokeWidth="1.3"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="15dp"
android:height="11dp"
android:viewportWidth="15"
android:viewportHeight="11">
<path
android:pathData="M5.303,10.081l8.486,-8.485M5.303,10.081L1.061,5.84"
android:strokeWidth="1.3"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#7E899C"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,11 +1,22 @@
<vector android:height="24dp" android:viewportHeight="16"
android:viewportWidth="15" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M14,15L14,13.4444C14,11.7262 12.5449,10.3333 10.75,10.3333L4.25,10.3333C2.4551,10.3333 1,11.7262 1,13.4444L1,15"
android:strokeColor="#7E899C" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.16666667"/>
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M4.25,4.1111a3.25,3.1111 0,1 0,6.5 0a3.25,3.1111 0,1 0,-6.5 0z"
android:strokeColor="#7E899C" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.16666667"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="15dp"
android:height="16dp"
android:viewportWidth="15"
android:viewportHeight="16">
<path
android:pathData="M14,15v-1.556c0,-1.718 -1.455,-3.11 -3.25,-3.11h-6.5c-1.795,0 -3.25,1.392 -3.25,3.11L1,15"
android:strokeLineJoin="round"
android:strokeWidth="1.167"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#7E899C"
android:strokeLineCap="round"/>
<path
android:pathData="M4.25,4.111a3.25,3.111 0,1 0,6.5 0a3.25,3.111 0,1 0,-6.5 0z"
android:strokeLineJoin="round"
android:strokeWidth="1.167"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#7E899C"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">

<size
android:width="8dp"
android:height="8dp" />

<solid android:color="#FF4B55" />

</shape>

View File

@ -6,7 +6,7 @@
android:orientation="vertical">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:id="@+id/bugReportToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

View File

@ -19,7 +19,7 @@
android:layout_height="wrap_content">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:id="@+id/emojiPickerToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/roomDetailContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">

</FrameLayout>

View File

@ -10,7 +10,7 @@
android:orientation="vertical">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:id="@+id/settingsToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

View File

@ -4,8 +4,7 @@
<im.vector.riotredesign.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/stateView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/dark">
android:layout_height="match_parent">

<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxyRecyclerView"

Some files were not shown because too many files have changed in this diff Show More