diff --git a/build.gradle b/build.gradle index c54c0f595f..ca7654c48d 100644 --- a/build.gradle +++ b/build.gradle @@ -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" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 8603cb798c..e173c0f2a3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -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 /** * This methods is a facility method to map a json content to a model. */ -inline fun Content?.toModel(): T? { +inline fun 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 + } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index 8e90e5e970..356ea6cc4f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -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 + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/UserMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/UserMapper.kt new file mode 100644 index 0000000000..2389427c90 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/UserMapper.kt @@ -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) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 420d675c92..2b43c15c55 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -231,6 +231,11 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi return userService.getUser(userId) } + override fun observeUser(userId: String): LiveData { + assert(isOpen) + return userService.observeUser(userId) + } + // Private methods ***************************************************************************** private fun assertMainThread() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index b8e1f028bb..e3f68f06ee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -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 { + val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> + UserEntity.where(realm, userId) + } + return Transformations.map(liveRealmData) { results -> + results + .map { it.asDomain() } + .firstOrNull() + } } } \ No newline at end of file diff --git a/vector/sampledata/matrix.json b/vector/sampledata/matrix.json new file mode 100644 index 0000000000..b9c55b91ba --- /dev/null +++ b/vector/sampledata/matrix.json @@ -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" + } + ] +} diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 591b1885a7..1c3e843b47 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -44,6 +44,8 @@ android:label="@string/title_activity_emoji_reaction_picker" /> + + + DefaultNavigator(fragment) as Navigator + } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotredesign/core/error/ErrorFormatter.kt index af676c10fd..114e08b866 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/error/ErrorFormatter.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/ButtonStateView.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/ButtonStateView.kt index 151d34c05a..f9aaf0d05d 100755 --- a/vector/src/main/java/im/vector/riotredesign/core/platform/ButtonStateView.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/ButtonStateView.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/CheckableConstraintLayout.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/CheckableConstraintLayout.kt new file mode 100644 index 0000000000..3ecc707d4d --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/CheckableConstraintLayout.kt @@ -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) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/StateView.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/StateView.kt index e75fb26cb6..cdffd22cc8 100755 --- a/vector/src/main/java/im/vector/riotredesign/core/platform/StateView.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/StateView.kt @@ -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 } } } diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt index c940d70ba9..de1f0df0d8 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt @@ -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)) + } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseFragment.kt index d8d9331c92..ddfad8cc1f 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseFragment.kt @@ -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 * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotredesign/core/resources/DateProvider.kt b/vector/src/main/java/im/vector/riotredesign/core/resources/DateProvider.kt index cc01bdf183..8c485b1b73 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/resources/DateProvider.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/resources/DateProvider.kt @@ -29,4 +29,9 @@ object DateProvider { return LocalDateTime.ofInstant(instant, zoneId) } + fun currentLocalDateTime(): LocalDateTime { + val instant = Instant.now() + return LocalDateTime.ofInstant(instant, zoneId) + } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt index 012bdf8346..d86872a8b4 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt @@ -67,10 +67,12 @@ object AvatarRenderer { identifier: String, name: String?, target: Target) { - 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) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt index 1ceba3d5cd..538666e5c1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt @@ -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 { // 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) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt index 03529618df..c710b47782 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt @@ -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(state), Session.Listener { companion object : MvRxViewModelFactory { @@ -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() - return HomeActivityViewModel(state, session, roomSelectionRepository) + val selectedGroupStore = viewModelContext.activity.get() + val homeRoomListObservableSource = viewModelContext.activity.get() + return HomeActivityViewModel(state, session, selectedGroupStore, homeRoomListObservableSource) } } @@ -54,29 +60,41 @@ class HomeActivityViewModel(state: EmptyState, val isLoading: LiveData get() = _isLoading - private val _openRoomLiveData = MutableLiveData>() - val openRoomLiveData: LiveData> - 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, Option, List>( + 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 { override fun onSuccess(data: String) { _isLoading.value = false - // Open room id - _openRoomLiveData.postValue(LiveEvent(data)) } override fun onFailure(failure: Throwable) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDetailFragment.kt new file mode 100644 index 0000000000..ef6543b186 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDetailFragment.kt @@ -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() + 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) + } + } + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDetailViewModel.kt new file mode 100644 index 0000000000..8b6b773495 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDetailViewModel.kt @@ -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(initialState) { + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: HomeDetailViewState): HomeDetailViewModel? { + val homeRoomListStore = viewModelContext.activity.get() + 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() + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDetailViewState.kt new file mode 100644 index 0000000000..b85b79902b --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDetailViewState.kt @@ -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 \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt index 4458d456f2..0d63dd7382 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt @@ -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() + 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() } } - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 4ccbb0ae03..d6297b6ff6 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -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(parameters = { parametersOf(fragment) }) val timelineMediaSizeProvider = TimelineMediaSizeProvider() val colorProvider = ColorProvider(fragment.requireContext()) - val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer,get()) + val timelineDateFormatter = get() + 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 { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt index ad3cd4cfcf..8d29c0ab8e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomePermalinkHandler.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomePermalinkHandler.kt index 143ddb57de..dc387aa5b0 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomePermalinkHandler.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomePermalinkHandler.kt @@ -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 -> { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeRoomListObservableStore.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeRoomListObservableStore.kt new file mode 100644 index 0000000000..9eaa47a754 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeRoomListObservableStore.kt @@ -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>() { + + fun observeFilteredBy(displayMode: RoomListFragment.DisplayMode): Observable> { + return observe() + .flatMapSingle { + Observable.fromIterable(it).filter(RoomListDisplayModeFilter(displayMode)).toList() + } + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/LoadingFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/LoadingFragment.kt new file mode 100644 index 0000000000..de5eca7193 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/LoadingFragment.kt @@ -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() + } + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt index 5ed550a2fd..ba65cf72c5 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt @@ -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() private val groupController by inject() 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) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt index 1cd2380282..c06cbe5e4a 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt @@ -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(initialState) { companion object : MvRxViewModelFactory { @@ -35,19 +44,27 @@ class GroupListViewModel(initialState: GroupListViewState, override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? { val currentSession = viewModelContext.activity.get() val selectedGroupHolder = viewModelContext.activity.get() - return GroupListViewModel(state, selectedGroupHolder, currentSession) + val stringProvider = viewModelContext.activity.get() + return GroupListViewModel(state, selectedGroupHolder, currentSession, stringProvider) } } + private val _openGroupLiveData = MutableLiveData>() + val openGroupLiveData: LiveData> + 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) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt index eef0e8659a..167498493e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt @@ -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() { 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(R.id.groupAvatarImageView) - val rootView by bind(R.id.itemGroupLayout) + val groupNameView by bind(R.id.groupNameView) + val rootView by bind(R.id.itemGroupLayout) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/LoadingRoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/LoadingRoomDetailFragment.kt deleted file mode 100644 index eefb65662a..0000000000 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/LoadingRoomDetailFragment.kt +++ /dev/null @@ -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() - } - } - - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 3ab393a0db..ddee224bd1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -25,7 +25,6 @@ sealed class RoomDetailActions { data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMedia(val mediaFiles: List) : 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() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActivity.kt new file mode 100644 index 0000000000..d3dc4aad5b --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActivity.kt @@ -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) + } + } + + + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index ef9307716b..98d0325ad9 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -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() 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)?.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() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index bbb1baa2c4..b02a71e8da 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -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(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() - val visibleRoomHolder = viewModelContext.activity.get() - 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)) } } } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt index c0a83ad23c..6151b42571 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt @@ -44,7 +44,7 @@ data class RoomDetailViewState( val roomId: String, val eventId: String?, val timeline: Timeline? = null, - val inviter: Async = Uninitialized, + val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, val asyncTimelineData: Async = Uninitialized, val sendMode: SendMode = SendMode.REGULAR, diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 59a296a735..84f970471e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -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, diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt deleted file mode 100644 index 42f6ba0e5d..0000000000 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ /dev/null @@ -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() ?: 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 - } - - } - - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/NoticeItemFactory.kt similarity index 55% rename from vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt rename to vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index 34e558970b..fc756c9c12 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt deleted file mode 100644 index 3a3a91f1d2..0000000000 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt +++ /dev/null @@ -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() ?: 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) - } - - -} - - diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt deleted file mode 100644 index 77e5920cb7..0000000000 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt +++ /dev/null @@ -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 - } - } - - -} - - diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt deleted file mode 100644 index be33c44ed0..0000000000 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt +++ /dev/null @@ -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) - } - - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index d53544346e..db5e3e542c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/format/NoticeEventFormatter.kt new file mode 100644 index 0000000000..ce4598f642 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -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() ?: 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() ?: 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() ?: 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() ?: 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 + } + } + +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt index 499f92f7f3..83b1fa6c64 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt @@ -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 - } - } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 91c39e38c3..657d9e0367 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -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()?.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()?.displayName + } else { + null + } +} + fun TimelineEvent.canBeMerged(): Boolean { return root.type == EventType.STATE_ROOM_MEMBER } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/AlphabeticalRoomComparator.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/AlphabeticalRoomComparator.kt new file mode 100644 index 0000000000..c1031c4533 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/AlphabeticalRoomComparator.kt @@ -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 { + + override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int { + return when { + rightRoomSummary?.displayName == null -> -1 + leftRoomSummary?.displayName == null -> 1 + else -> leftRoomSummary.displayName.compareTo(rightRoomSummary.displayName) + } + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/ChronologicalRoomComparator.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/ChronologicalRoomComparator.kt new file mode 100644 index 0000000000..2ffe559d91 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/ChronologicalRoomComparator.kt @@ -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 { + + 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 + } + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomCategoryItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomCategoryItem.kt index 96f97e37d1..5a837fd0a9 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomCategoryItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomCategoryItem.kt @@ -41,7 +41,7 @@ abstract class RoomCategoryItem : VectorEpoxyModel() { 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() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSelectionRepository.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListAnimator.kt similarity index 56% rename from vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSelectionRepository.kt rename to vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListAnimator.kt index 9131d42994..687f5ac5c2 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSelectionRepository.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListAnimator.kt @@ -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() - } - -} - +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListDisplayModeFilter.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListDisplayModeFilter.kt new file mode 100644 index 0000000000..958c431a6d --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListDisplayModeFilter.kt @@ -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 { + + 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 + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt index 5a43a21e05..19b031cf3c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt @@ -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() - private val homeNavigator by inject() 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?) { + 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) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt index b9476ce528..c9ead506a0 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt @@ -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(initialState) { companion object : MvRxViewModelFactory { @@ -52,15 +45,15 @@ class RoomListViewModel(initialState: RoomListViewState, @JvmStatic override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? { val currentSession = viewModelContext.activity.get() - val roomSelectionRepository = viewModelContext.activity.get() - val selectedGroupHolder = viewModelContext.activity.get() - val visibleRoomHolder = viewModelContext.activity.get() - val roomSummaryComparator = viewModelContext.activity.get() - return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator) + val homeRoomListObservableSource = viewModelContext.activity.get() + val chronologicalRoomComparator = viewModelContext.activity.get() + val alphabeticalRoomComparator = viewModelContext.activity.get() + return RoomListViewModel(state, currentSession, homeRoomListObservableSource, alphabeticalRoomComparator, chronologicalRoomComparator) } } - + private val displayMode = initialState.displayMode + private val roomListDisplayModeFilter = RoomListDisplayModeFilter(displayMode) private val roomListFilter = BehaviorRelay.createDefault>(Option.empty()) private val _openRoomLiveData = MutableLiveData>() @@ -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, Option, Option, 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, filterRoomOption: Option): List { - val filterRoom = filterRoomOption.orNull() - return rooms.filter { - if (filterRoom.isNullOrBlank()) { - true - } else { - it.displayName.contains(other = filterRoom, ignoreCase = true) - } - } - } - private fun buildRoomSummaries(rooms: List): RoomSummaries { val invites = ArrayList() val favourites = ArrayList() @@ -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)) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt index 164b6d3b9a..a6afc66d9f 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt @@ -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 = Uninitialized, - val visibleRoomId: String? = null, + val displayMode: RoomListFragment.DisplayMode, + val asyncRooms: Async> = Uninitialized, + val asyncFilteredRooms: Async = 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() } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt index 24641da234..9d8e555f07 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt @@ -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() { 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, selectedRoomId: String?) { + private fun buildRoomModels(summaries: List) { 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() + 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) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryFormatter.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryFormatter.kt index 4666d99b87..a6463e3739 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryFormatter.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryFormatter.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt index e68926101d..bf6fbcaa1c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt @@ -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() { @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() { 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(R.id.roomUnreadCounterBadgeView) val titleView by bind(R.id.roomNameView) + val unreadCounterBadgeView by bind(R.id.roomUnreadCounterBadgeView) + val lastEventView by bind(R.id.roomLastEventView) + val lastEventTimeView by bind(R.id.roomLastEventTimeView) val avatarImageView by bind(R.id.roomAvatarImageView) - val rootView by bind(R.id.itemRoomLayout) + val rootView by bind(R.id.itemRoomLayout) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/UnreadCounterBadgeView.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/UnreadCounterBadgeView.kt index 44de73e8db..1f69a6a7cf 100755 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/UnreadCounterBadgeView.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/UnreadCounterBadgeView.kt @@ -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 + ) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/invite/VectorInviteView.kt b/vector/src/main/java/im/vector/riotredesign/features/invite/VectorInviteView.kt index 5cfddff247..54b1476ab8 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/invite/VectorInviteView.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/invite/VectorInviteView.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotredesign/features/navigation/DefaultNavigator.kt new file mode 100644 index 0000000000..34c774a37a --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/navigation/DefaultNavigator.kt @@ -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) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotredesign/features/navigation/Navigator.kt new file mode 100644 index 0000000000..a47bbaa3d4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/navigation/Navigator.kt @@ -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() + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/rageshake/BugReportActivity.kt index 8868ec3e60..48f8102126 100755 --- a/vector/src/main/java/im/vector/riotredesign/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/rageshake/BugReportActivity.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt index 7c8d537b6c..a75accd9ba 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt @@ -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() diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/JoinState.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/JoinState.kt new file mode 100644 index 0000000000..a521218119 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/JoinState.kt @@ -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 +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomItem.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomItem.kt index 6487b39c9d..8e30809f80 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomItem.kt @@ -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() { - enum class JoinState { - NOT_JOINED, - JOINING, - JOINING_ERROR, - JOINED - } - @EpoxyAttribute var avatarUrl: String? = null @@ -46,6 +40,12 @@ abstract class PublicRoomItem : VectorEpoxyModel() { @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() { 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() { val avatarView by bind(R.id.itemPublicRoomAvatar) val nameView by bind(R.id.itemPublicRoomName) + val aliasView by bind(R.id.itemPublicRoomAlias) + val topicView by bind(R.id.itemPublicRoomTopic) val counterView by bind(R.id.itemPublicRoomMembersCount) val buttonState by bind(R.id.itemPublicRoomButtonState) diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsController.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsController.kt index 50baf6d692..01309e9e1a 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsController.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt index aca1e30c10..5ffae5bbbf 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt @@ -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 } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/RoomDirectoryViewModel.kt similarity index 90% rename from vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsViewModel.kt rename to vector/src/main/java/im/vector/riotredesign/features/roomdirectory/RoomDirectoryViewModel.kt index 6f04f7861a..fa980b206f 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/RoomDirectoryViewModel.kt @@ -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) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/picker/RoomDirectoryPickerController.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/picker/RoomDirectoryPickerController.kt index 5b758e287e..e19c7a0b95 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/picker/RoomDirectoryPickerController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/picker/RoomDirectoryPickerController.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt index 6ac801063c..6365d39a83 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewActivity.kt new file mode 100644 index 0000000000..0728ad56d7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewActivity.kt @@ -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(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) + } + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt new file mode 100644 index 0000000000..074995557a --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -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() + 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) + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewViewModel.kt new file mode 100644 index 0000000000..131f2719c1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -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(initialState) { + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomPreviewViewState): RoomPreviewViewModel? { + val currentSession = viewModelContext.activity.get() + + 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 { + 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 + ) + } + } + }) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewViewState.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewViewState.kt new file mode 100644 index 0000000000..4ba4bdee2f --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewViewState.kt @@ -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) +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt index a657e1102e..6208c75e4b 100755 --- a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt @@ -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() override fun initUiAndData() { - configureToolbar() + configureToolbar(settingsToolbar) if (isFirstCreation()) { vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId) diff --git a/vector/src/main/res/color/home_bottom_nav_view_tint.xml b/vector/src/main/res/color/home_bottom_nav_view_tint.xml index ad4b5ff429..ff64c3c04c 100644 --- a/vector/src/main/res/color/home_bottom_nav_view_tint.xml +++ b/vector/src/main/res/color/home_bottom_nav_view_tint.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_room_item.xml b/vector/src/main/res/drawable/bg_group_item.xml similarity index 83% rename from vector/src/main/res/drawable/bg_room_item.xml rename to vector/src/main/res/drawable/bg_group_item.xml index 7a8d5873f9..221150eb8c 100644 --- a/vector/src/main/res/drawable/bg_room_item.xml +++ b/vector/src/main/res/drawable/bg_group_item.xml @@ -3,7 +3,7 @@ - + diff --git a/vector/src/main/res/drawable/bg_unread_highlight.xml b/vector/src/main/res/drawable/bg_unread_highlight.xml index 371c1e7d58..87d7cae17c 100644 --- a/vector/src/main/res/drawable/bg_unread_highlight.xml +++ b/vector/src/main/res/drawable/bg_unread_highlight.xml @@ -1,5 +1,8 @@ + android:shape="rectangle"> + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_unread_notification.xml b/vector/src/main/res/drawable/bg_unread_notification.xml index 496134a791..1f93e4dd97 100644 --- a/vector/src/main/res/drawable/bg_unread_notification.xml +++ b/vector/src/main/res/drawable/bg_unread_notification.xml @@ -1,6 +1,9 @@ + android:shape="rectangle"> + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_arrow_right.xml b/vector/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 0000000000..c314196b66 --- /dev/null +++ b/vector/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/drawable/ic_fab_add.xml b/vector/src/main/res/drawable/ic_fab_add.xml new file mode 100644 index 0000000000..62f2f987e3 --- /dev/null +++ b/vector/src/main/res/drawable/ic_fab_add.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_fab_add_chat.xml b/vector/src/main/res/drawable/ic_fab_add_chat.xml new file mode 100644 index 0000000000..3e1b9a0eb1 --- /dev/null +++ b/vector/src/main/res/drawable/ic_fab_add_chat.xml @@ -0,0 +1,21 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_fab_add_room.xml b/vector/src/main/res/drawable/ic_fab_add_room.xml new file mode 100644 index 0000000000..630150fe32 --- /dev/null +++ b/vector/src/main/res/drawable/ic_fab_add_room.xml @@ -0,0 +1,21 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_home_bottom_catchup.xml b/vector/src/main/res/drawable/ic_home_bottom_catchup.xml new file mode 100644 index 0000000000..f59c2d64c2 --- /dev/null +++ b/vector/src/main/res/drawable/ic_home_bottom_catchup.xml @@ -0,0 +1,22 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_home_bottom_chat.xml b/vector/src/main/res/drawable/ic_home_bottom_chat.xml new file mode 100644 index 0000000000..2f78524283 --- /dev/null +++ b/vector/src/main/res/drawable/ic_home_bottom_chat.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/drawable/ic_home_bottom_group.xml b/vector/src/main/res/drawable/ic_home_bottom_group.xml new file mode 100644 index 0000000000..29be415bf5 --- /dev/null +++ b/vector/src/main/res/drawable/ic_home_bottom_group.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/drawable/ic_noun_party_popper.xml b/vector/src/main/res/drawable/ic_noun_party_popper.xml new file mode 100644 index 0000000000..2ace9bd928 --- /dev/null +++ b/vector/src/main/res/drawable/ic_noun_party_popper.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_plus_circle.xml b/vector/src/main/res/drawable/ic_plus_circle.xml index d8b21310ca..0b7b4eecd6 100644 --- a/vector/src/main/res/drawable/ic_plus_circle.xml +++ b/vector/src/main/res/drawable/ic_plus_circle.xml @@ -1,13 +1,22 @@ - - + - - + android:strokeWidth="1.4" + android:strokeColor="#03B381" + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + diff --git a/vector/src/main/res/drawable/ic_settings_x.xml b/vector/src/main/res/drawable/ic_settings_x.xml new file mode 100644 index 0000000000..994da00c19 --- /dev/null +++ b/vector/src/main/res/drawable/ic_settings_x.xml @@ -0,0 +1,22 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_tick.xml b/vector/src/main/res/drawable/ic_tick.xml index 3b8c8d8331..11159342b6 100644 --- a/vector/src/main/res/drawable/ic_tick.xml +++ b/vector/src/main/res/drawable/ic_tick.xml @@ -1,9 +1,13 @@ - - - + + diff --git a/vector/src/main/res/drawable/ic_user.xml b/vector/src/main/res/drawable/ic_user.xml index e518f7507c..cc811c6073 100644 --- a/vector/src/main/res/drawable/ic_user.xml +++ b/vector/src/main/res/drawable/ic_user.xml @@ -1,11 +1,22 @@ - - - + + + diff --git a/vector/src/main/res/drawable/red_dot.xml b/vector/src/main/res/drawable/red_dot.xml new file mode 100644 index 0000000000..58e5454d5c --- /dev/null +++ b/vector/src/main/res/drawable/red_dot.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_bug_report.xml b/vector/src/main/res/layout/activity_bug_report.xml index 82916ee69a..4d87ced748 100644 --- a/vector/src/main/res/layout/activity_bug_report.xml +++ b/vector/src/main/res/layout/activity_bug_report.xml @@ -6,7 +6,7 @@ android:orientation="vertical"> diff --git a/vector/src/main/res/layout/activity_emoji_reaction_picker.xml b/vector/src/main/res/layout/activity_emoji_reaction_picker.xml index 8e1edab351..4744931b69 100644 --- a/vector/src/main/res/layout/activity_emoji_reaction_picker.xml +++ b/vector/src/main/res/layout/activity_emoji_reaction_picker.xml @@ -19,7 +19,7 @@ android:layout_height="wrap_content"> + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_vector_settings.xml b/vector/src/main/res/layout/activity_vector_settings.xml index a1d8c2f246..85954282d9 100755 --- a/vector/src/main/res/layout/activity_vector_settings.xml +++ b/vector/src/main/res/layout/activity_vector_settings.xml @@ -10,7 +10,7 @@ android:orientation="vertical"> diff --git a/vector/src/main/res/layout/fragment_group_list.xml b/vector/src/main/res/layout/fragment_group_list.xml index a075d51134..810fe3e47b 100644 --- a/vector/src/main/res/layout/fragment_group_list.xml +++ b/vector/src/main/res/layout/fragment_group_list.xml @@ -4,8 +4,7 @@ + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_home_drawer.xml b/vector/src/main/res/layout/fragment_home_drawer.xml index c99210895e..8bbd52c2be 100644 --- a/vector/src/main/res/layout/fragment_home_drawer.xml +++ b/vector/src/main/res/layout/fragment_home_drawer.xml @@ -1,23 +1,81 @@ + android:layout_height="match_parent" + android:clickable="true" + android:focusable="true"> + + + + + + + + + + + + - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/homeDrawerHeader" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_loading_room_detail.xml b/vector/src/main/res/layout/fragment_loading.xml similarity index 100% rename from vector/src/main/res/layout/fragment_loading_room_detail.xml rename to vector/src/main/res/layout/fragment_loading.xml diff --git a/vector/src/main/res/layout/fragment_public_rooms.xml b/vector/src/main/res/layout/fragment_public_rooms.xml index a369496d81..6db130e864 100644 --- a/vector/src/main/res/layout/fragment_public_rooms.xml +++ b/vector/src/main/res/layout/fragment_public_rooms.xml @@ -19,88 +19,52 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + android:background="?attr/colorPrimary" + app:contentInsetStartWithNavigation="0dp" + app:layout_scrollFlags="noScroll" + app:popupTheme="@style/ThemeOverlay.AppCompat.Light" + app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> - + android:layout_height="32dp" + android:layout_marginStart="8dp" + android:layout_marginLeft="8dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/layout_horizontal_margin" + android:layout_marginRight="@dimen/layout_horizontal_margin" + android:layout_marginBottom="8dp" + android:background="@drawable/bg_search_edit_text" + android:drawableStart="@drawable/ic_search_white" + android:drawableLeft="@drawable/ic_search_white" + android:drawablePadding="8dp" + android:drawableTint="#9fa9ba" + android:hint="@string/home_filter_placeholder_rooms" + android:lines="1" + android:paddingLeft="8dp" + android:paddingRight="8dp" /> - + - - - - -