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

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

View File

@ -11,7 +11,7 @@ buildscript {
} }
} }
dependencies { 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.google.gms:google-services:4.2.0'
classpath "com.airbnb.okreplay:gradle-plugin:1.4.0" classpath "com.airbnb.okreplay:gradle-plugin:1.4.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

View File

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

View File

@ -18,6 +18,7 @@
package im.vector.matrix.android.api.session.user package im.vector.matrix.android.api.session.user
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
/** /**
@ -32,4 +33,11 @@ interface UserService {
*/ */
fun getUser(userId: String): User? fun getUser(userId: String): User?
/**
* Observe a live user from a userId
* @param userId the userId to look for.
* @return a Livedata of user with userId
*/
fun observeUser(userId: String): LiveData<User?>
} }

View File

@ -0,0 +1,35 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.database.model.UserEntity
internal object UserMapper {
fun map(userEntity: UserEntity): User {
return User(
userEntity.userId,
userEntity.displayName,
userEntity.avatarUrl
)
}
}
internal fun UserEntity.asDomain(): User {
return UserMapper.map(this)
}

View File

@ -231,6 +231,11 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
return userService.getUser(userId) return userService.getUser(userId)
} }
override fun observeUser(userId: String): LiveData<User?> {
assert(isOpen)
return userService.observeUser(userId)
}
// Private methods ***************************************************************************** // Private methods *****************************************************************************
private fun assertMainThread() { private fun assertMainThread() {

View File

@ -18,9 +18,13 @@
package im.vector.matrix.android.internal.session.user package im.vector.matrix.android.internal.session.user
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.fetchCopied 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? { override fun getUser(userId: String): User? {
val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() }
?: return null ?: return null
return User( return userEntity.asDomain()
userEntity.userId, }
userEntity.displayName,
userEntity.avatarUrl override fun observeUser(userId: String): LiveData<User?> {
) val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
UserEntity.where(realm, userId)
}
return Transformations.map(liveRealmData) { results ->
results
.map { it.asDomain() }
.firstOrNull()
}
} }
} }

View File

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

View File

@ -44,6 +44,8 @@
android:label="@string/title_activity_emoji_reaction_picker" /> android:label="@string/title_activity_emoji_reaction_picker" />
<activity android:name=".features.roomdirectory.RoomDirectoryActivity" /> <activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
<service <service
android:name=".core.services.CallService" android:name=".core.services.CallService"

View File

@ -0,0 +1,19 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.animations
const val ANIMATION_DURATION_SHORT = 200L

View File

@ -0,0 +1,37 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.animations
import android.animation.Animator
open class SimpleAnimatorListener : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {
// No op
}
override fun onAnimationEnd(animation: Animator?) {
// No op
}
override fun onAnimationCancel(animation: Animator?) {
// No op
}
override fun onAnimationStart(animation: Animator?) {
// No op
}
}

View File

@ -18,15 +18,18 @@ package im.vector.riotredesign.core.di
import android.content.Context import android.content.Context
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.core.error.ErrorFormatter import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.resources.LocaleProvider import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringArrayProvider import im.vector.riotredesign.core.resources.StringArrayProvider
import im.vector.riotredesign.core.resources.StringProvider import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.HomeRoomListObservableStore
import im.vector.riotredesign.features.home.group.SelectedGroupStore import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.room.list.AlphabeticalRoomComparator
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository import im.vector.riotredesign.features.home.room.list.ChronologicalRoomComparator
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator import im.vector.riotredesign.features.navigation.DefaultNavigator
import im.vector.riotredesign.features.navigation.Navigator
import im.vector.riotredesign.features.notifications.NotificationDrawerManager import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import org.koin.dsl.module.module import org.koin.dsl.module.module
@ -50,20 +53,20 @@ class AppModule(private val context: Context) {
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE) context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
} }
single {
RoomSelectionRepository(get())
}
single { single {
SelectedGroupStore() SelectedGroupStore()
} }
single { single {
VisibleRoomStore() HomeRoomListObservableStore()
} }
single { single {
RoomSummaryComparator() ChronologicalRoomComparator()
}
single {
AlphabeticalRoomComparator()
} }
single { single {
@ -78,6 +81,9 @@ class AppModule(private val context: Context) {
Matrix.getInstance().currentSession!! Matrix.getInstance().currentSession!!
} }
factory { (fragment: Fragment) ->
DefaultNavigator(fragment) as Navigator
}
} }
} }

View File

@ -28,9 +28,10 @@ class ErrorFormatter(val stringProvider: StringProvider) {
return failure.localizedMessage return failure.localizedMessage
} }
fun toHumanReadable(throwable: Throwable): String { fun toHumanReadable(throwable: Throwable?): String {
return when (throwable) { return when (throwable) {
null -> ""
is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network) is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network)
else -> throwable.localizedMessage else -> throwable.localizedMessage
} }

View File

@ -20,6 +20,7 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -43,14 +44,13 @@ class ButtonStateView @JvmOverloads constructor(context: Context, attrs: Attribu
fun onRetryClicked() fun onRetryClicked()
} }
// Big or Flat button
var button: Button
init { init {
View.inflate(context, R.layout.view_button_state, this) View.inflate(context, R.layout.view_button_state, this)
layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
buttonStateButton.setOnClickListener {
callback?.onButtonClicked()
}
buttonStateRetry.setOnClickListener { buttonStateRetry.setOnClickListener {
callback?.onRetryClicked() callback?.onRetryClicked()
} }
@ -62,20 +62,32 @@ class ButtonStateView @JvmOverloads constructor(context: Context, attrs: Attribu
0, 0) 0, 0)
.apply { .apply {
try { 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)) buttonStateLoaded.setImageDrawable(getDrawable(R.styleable.ButtonStateView_bsv_loaded_image_src))
} finally { } finally {
recycle() recycle()
} }
} }
button.setOnClickListener {
callback?.onButtonClicked()
}
} }
fun render(newState: State) { fun render(newState: State) {
if (newState == State.Button) { if (newState == State.Button) {
buttonStateButton.isVisible = true button.isVisible = true
} else { } else {
// We use isInvisible because we want to keep button space in the layout // We use isInvisible because we want to keep button space in the layout
buttonStateButton.isInvisible = true button.isInvisible = true
} }
buttonStateLoading.isVisible = newState == State.Loading buttonStateLoading.isVisible = newState == State.Loading

View File

@ -0,0 +1,61 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.platform
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.Checkable
import androidx.constraintlayout.widget.ConstraintLayout
class CheckableConstraintLayout : ConstraintLayout, Checkable {
private var mChecked = false
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun isChecked(): Boolean {
return mChecked
}
override fun setChecked(b: Boolean) {
if (b != mChecked) {
mChecked = b
refreshDrawableState()
}
}
override fun toggle() {
isChecked = !mChecked
}
public override fun onCreateDrawableState(extraSpace: Int): IntArray {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
if (isChecked) {
View.mergeDrawableStates(drawableState, CHECKED_STATE_SET)
}
return drawableState
}
companion object {
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
}
}

View File

@ -17,9 +17,9 @@
package im.vector.riotredesign.core.platform package im.vector.riotredesign.core.platform
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import im.vector.riotredesign.R import im.vector.riotredesign.R
import kotlinx.android.synthetic.main.view_state.view.* import kotlinx.android.synthetic.main.view_state.view.*
@ -30,7 +30,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
sealed class State { sealed class State {
object Content : State() object Content : State()
object Loading : 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() data class Error(val message: CharSequence? = null) : State()
} }
@ -52,7 +52,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
init { init {
View.inflate(context, R.layout.view_state, this) 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 { errorRetryView.setOnClickListener {
eventCallback?.onRetryClicked() eventCallback?.onRetryClicked()
} }
@ -62,35 +62,33 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
private fun update(newState: State) { private fun update(newState: State) {
when (newState) { when (newState) {
is StateView.State.Content -> { is State.Content -> {
progressBar.visibility = View.INVISIBLE progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE errorView.visibility = View.INVISIBLE
emptyView.visibility = View.INVISIBLE emptyView.visibility = View.INVISIBLE
contentView?.visibility = View.VISIBLE contentView?.visibility = View.VISIBLE
} }
is StateView.State.Loading -> { is State.Loading -> {
progressBar.visibility = View.VISIBLE progressBar.visibility = View.VISIBLE
errorView.visibility = View.INVISIBLE errorView.visibility = View.INVISIBLE
emptyView.visibility = View.INVISIBLE emptyView.visibility = View.INVISIBLE
contentView?.visibility = View.INVISIBLE contentView?.visibility = View.INVISIBLE
} }
is StateView.State.Empty -> { is State.Empty -> {
progressBar.visibility = View.INVISIBLE progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE errorView.visibility = View.INVISIBLE
emptyView.visibility = View.VISIBLE emptyView.visibility = View.VISIBLE
emptyImageView.setImageDrawable(newState.image)
emptyMessageView.text = newState.message emptyMessageView.text = newState.message
if (contentView != null) { emptyTitleView.text = newState.title
contentView!!.visibility = View.INVISIBLE contentView?.visibility = View.INVISIBLE
}
} }
is StateView.State.Error -> { is State.Error -> {
progressBar.visibility = View.INVISIBLE progressBar.visibility = View.INVISIBLE
errorView.visibility = View.VISIBLE errorView.visibility = View.VISIBLE
emptyView.visibility = View.INVISIBLE emptyView.visibility = View.INVISIBLE
errorMessageView.text = newState.message errorMessageView.text = newState.message
if (contentView != null) { contentView?.visibility = View.INVISIBLE
contentView!!.visibility = View.INVISIBLE
}
} }
} }
} }

View File

@ -49,11 +49,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
* UI * UI
* ========================================================================================== */ * ========================================================================================== */
@Nullable
@JvmField
@BindView(R.id.toolbar)
var toolbar: Toolbar? = null
@Nullable @Nullable
@JvmField @JvmField
@BindView(R.id.vector_coordinator_layout) @BindView(R.id.vector_coordinator_layout)
@ -245,14 +240,16 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
protected fun isFirstCreation() = savedInstanceState == null 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) setSupportActionBar(toolbar)
supportActionBar?.let { if (displayBack) {
it.setDisplayShowHomeEnabled(true) supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true) it.setDisplayShowHomeEnabled(true)
it.setDisplayHomeAsUpEnabled(true)
}
} }
} }
@ -297,8 +294,12 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
* Temporary method * Temporary method
* ========================================================================================== */ * ========================================================================================== */
fun notImplemented() { fun notImplemented(message: String = "") {
toast(getString(R.string.not_implemented)) if (message.isNotBlank()) {
toast(getString(R.string.not_implemented) + ": $message")
} else {
toast(getString(R.string.not_implemented))
}
} }
} }

View File

@ -22,14 +22,17 @@ import android.view.*
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.appcompat.widget.Toolbar
import butterknife.ButterKnife import butterknife.ButterKnife
import butterknife.Unbinder import butterknife.Unbinder
import com.airbnb.mvrx.BaseMvRxFragment import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util.assertMainThread 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.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
import timber.log.Timber import timber.log.Timber
abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed { abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
@ -41,6 +44,12 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
activity as VectorBaseActivity activity as VectorBaseActivity
} }
/* ==========================================================================================
* Navigator
* ========================================================================================== */
protected val navigator: Navigator by inject { parametersOf(this) }
/* ========================================================================================== /* ==========================================================================================
* Life cycle * Life cycle
* ========================================================================================== */ * ========================================================================================== */
@ -123,6 +132,20 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
return this return this
} }
/* ==========================================================================================
* Toolbar
* ========================================================================================== */
/**
* Configure the Toolbar.
*/
protected fun setupToolbar(toolbar: Toolbar) {
val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) {
parentActivity.configure(toolbar)
}
}
/* ========================================================================================== /* ==========================================================================================
* Disposable * Disposable
* ========================================================================================== */ * ========================================================================================== */

View File

@ -29,4 +29,9 @@ object DateProvider {
return LocalDateTime.ofInstant(instant, zoneId) return LocalDateTime.ofInstant(instant, zoneId)
} }
fun currentLocalDateTime(): LocalDateTime {
val instant = Instant.now()
return LocalDateTime.ofInstant(instant, zoneId)
}
} }

View File

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

View File

@ -21,7 +21,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
@ -32,16 +31,12 @@ import com.airbnb.mvrx.viewModel
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.hideKeyboard 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.extensions.replaceFragment
import im.vector.riotredesign.core.platform.OnBackPressed import im.vector.riotredesign.core.platform.OnBackPressed
import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseActivity 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.BugReporter
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler 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 im.vector.riotredesign.features.workers.signout.SignOutUiWorker
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@ -69,15 +64,13 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
bindScope(getOrCreateScope(HomeModule.HOME_SCOPE)) bindScope(getOrCreateScope(HomeModule.HOME_SCOPE))
homeNavigator.activity = this homeNavigator.activity = this
drawerLayout.addDrawerListener(drawerListener) drawerLayout.addDrawerListener(drawerListener)
if (savedInstanceState == null) { if (isFirstCreation()) {
val homeDrawerFragment = HomeDrawerFragment.newInstance() val homeDrawerFragment = HomeDrawerFragment.newInstance()
val loadingDetail = LoadingRoomDetailFragment.newInstance() val loadingDetail = LoadingFragment.newInstance()
replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer) replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer)
replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer) replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer)
} }
homeActivityViewModel.openRoomLiveData.observeEvent(this) {
homeNavigator.openRoomDetail(it, null)
}
homeActivityViewModel.isLoading.observe(this, Observer<Boolean> { homeActivityViewModel.isLoading.observe(this, Observer<Boolean> {
// TODO better UI // TODO better UI
if (it) { if (it) {
@ -113,36 +106,21 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
} }
override fun configure(toolbar: Toolbar) { override fun configure(toolbar: Toolbar) {
setSupportActionBar(toolbar) configureToolbar(toolbar, false)
supportActionBar?.setHomeButtonEnabled(true)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val drawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, 0, 0)
drawerLayout.addDrawerListener(drawerToggle)
drawerToggle.syncState()
} }
override fun getMenuRes() = R.menu.home override fun getMenuRes() = R.menu.home
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
drawerLayout.openDrawer(GravityCompat.START) drawerLayout.openDrawer(GravityCompat.START)
return true return true
} }
R.id.sliding_menu_settings -> { R.id.sliding_menu_sign_out -> {
startActivity(VectorSettingsActivity.getIntent(this, "TODO"))
return true
}
R.id.sliding_menu_sign_out -> {
SignOutUiWorker(this).perform(Matrix.getInstance().currentSession!!) SignOutUiWorker(this).perform(Matrix.getInstance().currentSession!!)
return true 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 return true
@ -160,8 +138,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
} }
private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean { private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
if (fm.backStackEntryCount == 0) // if (fm.backStackEntryCount == 0)
return false // return false
val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed() val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed()
for (f in reverseOrder) { for (f in reverseOrder) {
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager) val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)

View File

@ -18,26 +18,31 @@ package im.vector.riotredesign.features.home
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session 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.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.VectorViewModel import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent import im.vector.riotredesign.features.home.group.ALL_COMMUNITIES_GROUP_ID
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository import im.vector.riotredesign.features.home.group.SelectedGroupStore
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit
data class EmptyState(val isEmpty: Boolean = true) : MvRxState data class EmptyState(val isEmpty: Boolean = true) : MvRxState
class HomeActivityViewModel(state: EmptyState, class HomeActivityViewModel(state: EmptyState,
private val session: Session, private val session: Session,
roomSelectionRepository: RoomSelectionRepository private val selectedGroupStore: SelectedGroupStore,
private val homeRoomListStore: HomeRoomListObservableStore
) : VectorViewModel<EmptyState>(state), Session.Listener { ) : VectorViewModel<EmptyState>(state), Session.Listener {
companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> { companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> {
@ -45,8 +50,9 @@ class HomeActivityViewModel(state: EmptyState,
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? { override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
val session = Matrix.getInstance().currentSession!! val session = Matrix.getInstance().currentSession!!
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>() val selectedGroupStore = viewModelContext.activity.get<SelectedGroupStore>()
return HomeActivityViewModel(state, session, roomSelectionRepository) val homeRoomListObservableSource = viewModelContext.activity.get<HomeRoomListObservableStore>()
return HomeActivityViewModel(state, session, selectedGroupStore, homeRoomListObservableSource)
} }
} }
@ -54,29 +60,41 @@ class HomeActivityViewModel(state: EmptyState,
val isLoading: LiveData<Boolean> val isLoading: LiveData<Boolean>
get() = _isLoading get() = _isLoading
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
val openRoomLiveData: LiveData<LiveEvent<String>>
get() = _openRoomLiveData
init { init {
session.addListener(this) session.addListener(this)
val lastSelectedRoomId = roomSelectionRepository.lastSelectedRoom() observeRoomAndGroup()
if (lastSelectedRoomId == null || session.getRoom(lastSelectedRoomId) == null) {
getTheFirstRoomWhenAvailable()
} else {
_openRoomLiveData.postValue(LiveEvent(lastSelectedRoomId))
}
} }
private fun getTheFirstRoomWhenAvailable() { private fun observeRoomAndGroup() {
session.rx().liveRoomSummaries() Observable
.filter { it.isNotEmpty() } .combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>(
.first(emptyList()) session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
.subscribeBy { selectedGroupStore.observe(),
val firstRoom = it.firstOrNull() BiFunction { rooms, selectedGroupOption ->
if (firstRoom != null) { val selectedGroup = selectedGroupOption.orNull()
_openRoomLiveData.postValue(LiveEvent(firstRoom.roomId)) 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() .disposeOnClear()
} }
@ -87,8 +105,6 @@ class HomeActivityViewModel(state: EmptyState,
session.createRoom(createRoomParams, object : MatrixCallback<String> { session.createRoom(createRoomParams, object : MatrixCallback<String> {
override fun onSuccess(data: String) { override fun onSuccess(data: String) {
_isLoading.value = false _isLoading.value = false
// Open room id
_openRoomLiveData.postValue(LiveEvent(data))
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {

View File

@ -0,0 +1,154 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import androidx.core.view.forEachIndexed
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.room.list.RoomListFragment
import im.vector.riotredesign.features.home.room.list.RoomListParams
import im.vector.riotredesign.features.home.room.list.UnreadCounterBadgeView
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_home_detail.*
@Parcelize
data class HomeDetailParams(
val groupId: String,
val groupName: String,
val groupAvatar: String
) : Parcelable
private const val CURRENT_DISPLAY_MODE = "CURRENT_DISPLAY_MODE"
private const val INDEX_CATCHUP = 0
private const val INDEX_PEOPLE = 1
private const val INDEX_ROOMS = 2
class HomeDetailFragment : VectorBaseFragment() {
private val params: HomeDetailParams by args()
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
private lateinit var currentDisplayMode: RoomListFragment.DisplayMode
private val viewModel: HomeDetailViewModel by fragmentViewModel()
override fun getLayoutResId(): Int {
return R.layout.fragment_home_detail
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
currentDisplayMode = savedInstanceState?.getSerializable(CURRENT_DISPLAY_MODE) as? RoomListFragment.DisplayMode
?: RoomListFragment.DisplayMode.HOME
switchDisplayMode(currentDisplayMode)
setupBottomNavigationView()
setupToolbar()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(CURRENT_DISPLAY_MODE, currentDisplayMode)
super.onSaveInstanceState(outState)
}
private fun setupToolbar() {
val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) {
parentActivity.configure(groupToolbar)
}
groupToolbar.title = ""
AvatarRenderer.render(
params.groupAvatar,
params.groupId,
params.groupName,
groupToolbarAvatarImageView
)
groupToolbarAvatarImageView.setOnClickListener {
vectorBaseActivity.notImplemented("Group click in toolbar")
}
}
private fun setupBottomNavigationView() {
bottomNavigationView.setOnNavigationItemSelectedListener {
val displayMode = when (it.itemId) {
R.id.bottom_action_home -> RoomListFragment.DisplayMode.HOME
R.id.bottom_action_people -> RoomListFragment.DisplayMode.PEOPLE
R.id.bottom_action_rooms -> RoomListFragment.DisplayMode.ROOMS
else -> RoomListFragment.DisplayMode.HOME
}
if (currentDisplayMode != displayMode) {
currentDisplayMode = displayMode
switchDisplayMode(displayMode)
}
true
}
val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView
menuView.forEachIndexed { index, view ->
val itemView = view as BottomNavigationItemView
val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false)
val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView)
itemView.addView(badgeLayout)
unreadCounterBadgeViews.add(index, unreadCounterBadgeView)
}
}
private fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) {
groupToolbarTitleView.setText(displayMode.titleRes)
updateSelectedFragment(displayMode)
}
private fun updateSelectedFragment(displayMode: RoomListFragment.DisplayMode) {
val fragmentTag = "FRAGMENT_TAG_${displayMode.name}"
var fragment = childFragmentManager.findFragmentByTag(fragmentTag)
if (fragment == null) {
fragment = RoomListFragment.newInstance(RoomListParams(displayMode))
}
childFragmentManager.beginTransaction()
.replace(R.id.roomListContainer, fragment, fragmentTag)
.addToBackStack(fragmentTag)
.commit()
}
override fun invalidate() = withState(viewModel) {
unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup))
unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
}
companion object {
fun newInstance(args: HomeDetailParams): HomeDetailFragment {
return HomeDetailFragment().apply {
setArguments(args)
}
}
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.riotredesign.core.platform.VectorViewModel
import org.koin.android.ext.android.get
/**
* View model used to update the home bottom bar notification counts
*/
class HomeDetailViewModel(initialState: HomeDetailViewState,
private val homeRoomListStore: HomeRoomListObservableStore)
: VectorViewModel<HomeDetailViewState>(initialState) {
companion object : MvRxViewModelFactory<HomeDetailViewModel, HomeDetailViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: HomeDetailViewState): HomeDetailViewModel? {
val homeRoomListStore = viewModelContext.activity.get<HomeRoomListObservableStore>()
return HomeDetailViewModel(state, homeRoomListStore)
}
}
init {
observeRoomSummaries()
}
// PRIVATE METHODS *****************************************************************************
private fun observeRoomSummaries() {
homeRoomListStore
.observe()
.subscribe { list ->
list.let { summaries ->
val peopleNotifications = summaries
.filter { it.isDirect }
.map { it.notificationCount }
.reduce { acc, i -> acc + i }
val peopleHasHighlight = summaries
.filter { it.isDirect }
.any { it.highlightCount > 0 }
val roomsNotifications = summaries
.filter { !it.isDirect }
.map { it.notificationCount }
.reduce { acc, i -> acc + i }
val roomsHasHighlight = summaries
.filter { !it.isDirect }
.any { it.highlightCount > 0 }
setState {
copy(
notificationCountCatchup = peopleNotifications + roomsNotifications,
notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight,
notificationCountPeople = peopleNotifications,
notificationHighlightPeople = peopleHasHighlight,
notificationCountRooms = roomsNotifications,
notificationHighlightRooms = roomsHasHighlight
)
}
}
}
.disposeOnClear()
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home
import com.airbnb.mvrx.MvRxState
data class HomeDetailViewState(
val notificationCountCatchup: Int = 0,
val notificationHighlightCatchup: Boolean = false,
val notificationCountPeople: Int = 0,
val notificationHighlightPeople: Boolean = false,
val notificationCountRooms: Int = 0,
val notificationHighlightRooms: Boolean = false
) : MvRxState

View File

@ -17,11 +17,14 @@
package im.vector.riotredesign.features.home package im.vector.riotredesign.features.home
import android.os.Bundle import android.os.Bundle
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.observeK
import im.vector.riotredesign.core.extensions.replaceChildFragment import im.vector.riotredesign.core.extensions.replaceChildFragment
import im.vector.riotredesign.core.platform.VectorBaseFragment import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.group.GroupListFragment 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() { class HomeDrawerFragment : VectorBaseFragment() {
@ -32,16 +35,26 @@ class HomeDrawerFragment : VectorBaseFragment() {
} }
} }
val session by inject<Session>()
override fun getLayoutResId() = R.layout.fragment_home_drawer override fun getLayoutResId() = R.layout.fragment_home_drawer
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null) { if (savedInstanceState == null) {
val groupListFragment = GroupListFragment.newInstance() val groupListFragment = GroupListFragment.newInstance()
replaceChildFragment(groupListFragment, R.id.groupListFragmentContainer) replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer)
val roomListFragment = RoomListFragment.newInstance() }
replaceChildFragment(roomListFragment, R.id.roomListFragmentContainer)
session.observeUser(session.sessionParams.credentials.userId).observeK(this) { user ->
if (user != null) {
AvatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
homeDrawerUsernameView.text = user.displayName
homeDrawerUserIdView.text = user.userId
}
}
homeDrawerHeaderSettingsView.setOnClickListener {
navigator.openSettings()
} }
} }
} }

View File

@ -25,11 +25,16 @@ import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserControl
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.home.group.GroupSummaryController 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.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.TimelineDateFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider 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.home.room.list.RoomSummaryController
import im.vector.riotredesign.features.html.EventHtmlRenderer import im.vector.riotredesign.features.html.EventHtmlRenderer
import org.koin.core.parameter.parametersOf
import org.koin.dsl.module.module import org.koin.dsl.module.module
class HomeModule { class HomeModule {
@ -37,8 +42,6 @@ class HomeModule {
companion object { companion object {
const val HOME_SCOPE = "HOME_SCOPE" const val HOME_SCOPE = "HOME_SCOPE"
const val ROOM_DETAIL_SCOPE = "ROOM_DETAIL_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 { val definition = module {
@ -50,31 +53,37 @@ class HomeModule {
} }
scope(HOME_SCOPE) { scope(HOME_SCOPE) {
HomePermalinkHandler(get()) HomePermalinkHandler(get(), get())
} }
// Fragment scopes // Fragment scopes
factory {
TimelineDateFormatter(get())
}
factory {
NoticeEventFormatter(get())
}
factory { (fragment: Fragment) -> factory { (fragment: Fragment) ->
val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get()) val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get())
val timelineDateFormatter = TimelineDateFormatter(get()) val noticeEventFormatter = get<NoticeEventFormatter>(parameters = { parametersOf(fragment) })
val timelineMediaSizeProvider = TimelineMediaSizeProvider() val timelineMediaSizeProvider = TimelineMediaSizeProvider()
val colorProvider = ColorProvider(fragment.requireContext()) val colorProvider = ColorProvider(fragment.requireContext())
val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer,get()) val timelineDateFormatter = get<TimelineDateFormatter>()
val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer, get())
val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory, val timelineItemFactory = TimelineItemFactory(
roomNameItemFactory = RoomNameItemFactory(get()), messageItemFactory = messageItemFactory,
roomTopicItemFactory = RoomTopicItemFactory(get()), noticeItemFactory = NoticeItemFactory(noticeEventFormatter),
roomMemberItemFactory = RoomMemberItemFactory(get()),
roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()),
callItemFactory = CallItemFactory(get()),
defaultItemFactory = DefaultItemFactory() defaultItemFactory = DefaultItemFactory()
) )
TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider) TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider)
} }
factory { factory {
RoomSummaryController(get()) RoomSummaryController(get(), get(), get())
} }
factory { factory {

View File

@ -18,11 +18,10 @@ package im.vector.riotredesign.features.home
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
import im.vector.riotredesign.core.extensions.replaceFragment import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs import im.vector.riotredesign.features.navigation.Navigator
import im.vector.riotredesign.features.home.room.detail.RoomDetailFragment
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import timber.log.Timber import timber.log.Timber
@ -32,22 +31,24 @@ class HomeNavigator {
private var rootRoomId: String? = null 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, fun openRoomDetail(roomId: String,
eventId: String?, eventId: String?,
addToBackstack: Boolean = false) { navigator: Navigator) {
Timber.v("Open room detail $roomId - $eventId - $addToBackstack") Timber.v("Open room detail $roomId - $eventId")
activity?.let { activity?.let {
//TODO enable eventId permalink. It doesn't work enough at the moment. //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) it.drawerLayout?.closeDrawer(GravityCompat.START)
if (addToBackstack) { navigator.openRoom(roomId)
it.addFragmentToBackstack(roomDetailFragment, R.id.homeDetailFragmentContainer, roomId)
} else {
rootRoomId = roomId
clearBackStack(it.supportFragmentManager)
it.replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
}
} }
} }

View File

@ -19,8 +19,10 @@ package im.vector.riotredesign.features.home
import android.net.Uri import android.net.Uri
import im.vector.matrix.android.api.permalinks.PermalinkData import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser 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?) { fun launch(deepLink: String?) {
val uri = deepLink?.let { Uri.parse(it) } val uri = deepLink?.let { Uri.parse(it) }
@ -34,16 +36,16 @@ class HomePermalinkHandler(private val navigator: HomeNavigator) {
val permalinkData = PermalinkParser.parse(deepLink) val permalinkData = PermalinkParser.parse(deepLink)
when (permalinkData) { when (permalinkData) {
is PermalinkData.EventLink -> { is PermalinkData.EventLink -> {
navigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, true) homeNavigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, navigator)
} }
is PermalinkData.RoomLink -> { is PermalinkData.RoomLink -> {
navigator.openRoomDetail(permalinkData.roomIdOrAlias, null, true) homeNavigator.openRoomDetail(permalinkData.roomIdOrAlias, null, navigator)
} }
is PermalinkData.GroupLink -> { is PermalinkData.GroupLink -> {
navigator.openGroupDetail(permalinkData.groupId) homeNavigator.openGroupDetail(permalinkData.groupId)
} }
is PermalinkData.UserLink -> { is PermalinkData.UserLink -> {
navigator.openUserDetail(permalinkData.userId) homeNavigator.openUserDetail(permalinkData.userId)
} }
is PermalinkData.FallbackLink -> { is PermalinkData.FallbackLink -> {

View File

@ -0,0 +1,35 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.core.utils.RxStore
import im.vector.riotredesign.features.home.room.list.RoomListDisplayModeFilter
import im.vector.riotredesign.features.home.room.list.RoomListFragment
import io.reactivex.Observable
class HomeRoomListObservableStore : RxStore<List<RoomSummary>>() {
fun observeFilteredBy(displayMode: RoomListFragment.DisplayMode): Observable<List<RoomSummary>> {
return observe()
.flatMapSingle {
Observable.fromIterable(it).filter(RoomListDisplayModeFilter(displayMode)).toList()
}
}
}

View File

@ -0,0 +1,49 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.riotredesign.features.home
import android.graphics.drawable.AnimationDrawable
import android.os.Bundle
import android.view.View
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_loading.*
class LoadingFragment : VectorBaseFragment() {
companion object {
fun newInstance(): LoadingFragment {
return LoadingFragment()
}
}
override fun getLayoutResId() = R.layout.fragment_loading
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val background = animatedLogoImageView.background
if (background is AnimationDrawable) {
background.start()
}
}
}

View File

@ -22,13 +22,12 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotredesign.R 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.StateView
import im.vector.riotredesign.core.platform.VectorBaseFragment 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 kotlinx.android.synthetic.main.fragment_group_list.*
import org.koin.android.ext.android.inject 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 { class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback {
@ -39,17 +38,20 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback
} }
private val viewModel: GroupListViewModel by fragmentViewModel() private val viewModel: GroupListViewModel by fragmentViewModel()
private val homeNavigator by inject<HomeNavigator>()
private val groupController by inject<GroupSummaryController>() private val groupController by inject<GroupSummaryController>()
override fun getLayoutResId() = R.layout.fragment_group_list override fun getLayoutResId() = R.layout.fragment_group_list
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(HomeModule.GROUP_LIST_SCOPE))
groupController.callback = this groupController.callback = this
stateView.contentView = epoxyRecyclerView stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(groupController) epoxyRecyclerView.setController(groupController)
viewModel.subscribe { renderState(it) } viewModel.subscribe { renderState(it) }
viewModel.openGroupLiveData.observeEvent(this) {
homeNavigator.openSelectedGroup(it)
}
} }
private fun renderState(state: GroupListViewState) { private fun renderState(state: GroupListViewState) {

View File

@ -16,17 +16,26 @@
package im.vector.riotredesign.features.home.group package im.vector.riotredesign.features.home.group
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option import arrow.core.Option
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.session.Session 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.matrix.rx.rx
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorViewModel 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 import org.koin.android.ext.android.get
const val ALL_COMMUNITIES_GROUP_ID = "ALL_COMMUNITIES_GROUP_ID"
class GroupListViewModel(initialState: GroupListViewState, class GroupListViewModel(initialState: GroupListViewState,
private val selectedGroupHolder: SelectedGroupStore, private val selectedGroupHolder: SelectedGroupStore,
private val session: Session private val session: Session,
private val stringProvider: StringProvider
) : VectorViewModel<GroupListViewState>(initialState) { ) : VectorViewModel<GroupListViewState>(initialState) {
companion object : MvRxViewModelFactory<GroupListViewModel, GroupListViewState> { companion object : MvRxViewModelFactory<GroupListViewModel, GroupListViewState> {
@ -35,19 +44,27 @@ class GroupListViewModel(initialState: GroupListViewState,
override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? { override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? {
val currentSession = viewModelContext.activity.get<Session>() val currentSession = viewModelContext.activity.get<Session>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>() val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
return GroupListViewModel(state, selectedGroupHolder, currentSession) val stringProvider = viewModelContext.activity.get<StringProvider>()
return GroupListViewModel(state, selectedGroupHolder, currentSession, stringProvider)
} }
} }
private val _openGroupLiveData = MutableLiveData<LiveEvent<GroupSummary>>()
val openGroupLiveData: LiveData<LiveEvent<GroupSummary>>
get() = _openGroupLiveData
init { init {
observeGroupSummaries() observeGroupSummaries()
observeState() observeSelectionState()
} }
private fun observeState() { private fun observeSelectionState() {
subscribe { selectSubscribe(GroupListViewState::selectedGroup) {
val selectedGroup = Option.fromNullable(it.selectedGroup) if (it != null) {
selectedGroupHolder.post(selectedGroup) _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 -> private fun handleSelectGroup(action: GroupListActions.SelectGroup) = withState { state ->
if (state.selectedGroup?.groupId != action.groupSummary.groupId) { if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
setState { copy(selectedGroup = action.groupSummary) } setState { copy(selectedGroup = action.groupSummary) }
} else {
setState { copy(selectedGroup = null) }
} }
} }
private fun observeGroupSummaries() { private fun observeGroupSummaries() {
session session
.rx().liveGroupSummaries() .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 -> .execute { async ->
copy(asyncGroups = async) val newSelectedGroup = selectedGroup ?: async()?.firstOrNull()
copy(asyncGroups = async, selectedGroup = newSelectedGroup)
} }
} }

View File

@ -17,12 +17,13 @@
package im.vector.riotredesign.features.home.group package im.vector.riotredesign.features.home.group
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel 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 import im.vector.riotredesign.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_group) @EpoxyModelClass(layout = R.layout.item_group)
@ -36,14 +37,16 @@ abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.rootView.isSelected = selected
holder.rootView.setOnClickListener { listener?.invoke() } holder.rootView.setOnClickListener { listener?.invoke() }
holder.groupNameView.text = groupName
holder.rootView.isChecked = selected
AvatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView) AvatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val avatarImageView by bind<ImageView>(R.id.groupAvatarImageView) val avatarImageView by bind<ImageView>(R.id.groupAvatarImageView)
val rootView by bind<CheckableFrameLayout>(R.id.itemGroupLayout) val groupNameView by bind<TextView>(R.id.groupNameView)
val rootView by bind<CheckableConstraintLayout>(R.id.itemGroupLayout)
} }
} }

View File

@ -1,47 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.detail
import android.graphics.drawable.AnimationDrawable
import android.os.Bundle
import android.view.View
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_loading_room_detail.*
class LoadingRoomDetailFragment : VectorBaseFragment() {
companion object {
fun newInstance(): LoadingRoomDetailFragment {
return LoadingRoomDetailFragment()
}
}
override fun getLayoutResId() = R.layout.fragment_loading_room_detail
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val background = animatedLogoImageView.background
if (background is AnimationDrawable) {
background.start()
}
}
}

View File

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

View File

@ -0,0 +1,63 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.riotredesign.features.home.room.detail
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseActivity
class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
override fun getLayoutRes(): Int {
return R.layout.activity_room_detail
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isFirstCreation()) {
val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
?: return
val roomDetailFragment = RoomDetailFragment.newInstance(roomDetailArgs)
replaceFragment(roomDetailFragment, R.id.roomDetailContainer)
}
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}
companion object {
private const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS"
fun newIntent(context: Context, roomDetailArgs: RoomDetailArgs): Intent {
return Intent(context, RoomDetailActivity::class.java).apply {
putExtra(EXTRA_ROOM_DETAIL_ARGS, roomDetailArgs)
}
}
}
}

View File

@ -42,7 +42,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader 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.hideKeyboard
import im.vector.riotredesign.core.extensions.observeEvent import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.glide.GlideApp 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.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.* import im.vector.riotredesign.core.utils.*
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
@ -154,6 +153,7 @@ class RoomDetailFragment :
} }
} }
private val roomDetailArgs: RoomDetailArgs by args()
private val session by inject<Session>() private val session by inject<Session>()
private val glideRequests by lazy { private val glideRequests by lazy {
GlideApp.with(this) GlideApp.with(this)
@ -180,8 +180,8 @@ class RoomDetailFragment :
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE)) bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
setupToolbar(roomToolbar)
setupRecyclerView() setupRecyclerView()
setupToolbar()
setupComposer() setupComposer()
setupAttachmentButton() setupAttachmentButton()
setupInviteView() setupInviteView()
@ -213,7 +213,7 @@ class RoomDetailFragment :
} }
SendMode.EDIT, SendMode.EDIT,
SendMode.QUOTE, SendMode.QUOTE,
SendMode.REPLY -> { SendMode.REPLY -> {
commandAutocompletePolicy.enabled = false commandAutocompletePolicy.enabled = false
if (event == null) { if (event == null) {
//we should ignore? can this happen? //we should ignore? can this happen?
@ -276,7 +276,7 @@ class RoomDetailFragment :
if (resultCode == RESULT_OK && data != null) { if (resultCode == RESULT_OK && data != null) {
when (requestCode) { when (requestCode) {
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) 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) val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
?: return ?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) 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 METHODS *****************************************************************************
private fun setupToolbar() {
val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) {
parentActivity.configure(toolbar)
}
}
private fun setupRecyclerView() { private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker() val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView) epoxyVisibilityTracker.attach(recyclerView)
@ -444,24 +432,24 @@ class RoomDetailFragment :
private fun onSendChoiceClicked(dialogListItem: DialogListItem) { private fun onSendChoiceClicked(dialogListItem: DialogListItem) {
Timber.v("On send choice clicked: $dialogListItem") Timber.v("On send choice clicked: $dialogListItem")
when (dialogListItem) { when (dialogListItem) {
is DialogListItem.SendFile -> { is DialogListItem.SendFile -> {
// launchFileIntent // launchFileIntent
} }
is DialogListItem.SendVoice -> { is DialogListItem.SendVoice -> {
//launchAudioRecorderIntent() //launchAudioRecorderIntent()
} }
is DialogListItem.SendSticker -> { is DialogListItem.SendSticker -> {
//startStickerPickerActivity() //startStickerPickerActivity()
} }
is DialogListItem.TakePhotoVideo -> is DialogListItem.TakePhotoVideo ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
// launchCamera() // launchCamera()
} }
is DialogListItem.TakePhoto -> is DialogListItem.TakePhoto ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) {
openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE) 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)) { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) {
// launchNativeVideoRecorder() // launchNativeVideoRecorder()
} }
@ -476,7 +464,7 @@ class RoomDetailFragment :
private fun renderState(state: RoomDetailViewState) { private fun renderState(state: RoomDetailViewState) {
renderRoomSummary(state) renderRoomSummary(state)
val summary = state.asyncRoomSummary() val summary = state.asyncRoomSummary()
val inviter = state.inviter() val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) { if (summary?.membership == Membership.JOIN) {
timelineEventController.setTimeline(state.timeline) timelineEventController.setTimeline(state.timeline)
inviteView.visibility = View.GONE inviteView.visibility = View.GONE
@ -488,20 +476,20 @@ class RoomDetailFragment :
} else if (summary?.membership == Membership.INVITE && inviter != null) { } else if (summary?.membership == Membership.INVITE && inviter != null) {
inviteView.visibility = View.VISIBLE inviteView.visibility = View.VISIBLE
inviteView.render(inviter, VectorInviteView.Mode.LARGE) inviteView.render(inviter, VectorInviteView.Mode.LARGE)
} else { } else if (state.asyncInviter.complete) {
//TODO : close the screen vectorBaseActivity.finish()
} }
} }
private fun renderRoomSummary(state: RoomDetailViewState) { private fun renderRoomSummary(state: RoomDetailViewState) {
state.asyncRoomSummary()?.let { state.asyncRoomSummary()?.let {
toolbarTitleView.text = it.displayName roomToolbarTitleView.text = it.displayName
AvatarRenderer.render(it, toolbarAvatarImageView) AvatarRenderer.render(it, roomToolbarAvatarImageView)
if (it.topic.isNotEmpty()) { if (it.topic.isNotEmpty()) {
toolbarSubtitleView.visibility = View.VISIBLE roomToolbarSubtitleView.visibility = View.VISIBLE
toolbarSubtitleView.text = it.topic roomToolbarSubtitleView.text = it.topic
} else { } else {
toolbarSubtitleView.visibility = View.GONE roomToolbarSubtitleView.visibility = View.GONE
} }
} }
} }
@ -513,20 +501,20 @@ class RoomDetailFragment :
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
when (sendMessageResult) { when (sendMessageResult) {
is SendMessageResult.MessageSent, is SendMessageResult.MessageSent,
is SendMessageResult.SlashCommandHandled -> { is SendMessageResult.SlashCommandHandled -> {
// Clear composer // Clear composer
composerLayout.composerEditText.text = null composerLayout.composerEditText.text = null
} }
is SendMessageResult.SlashCommandError -> { is SendMessageResult.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) 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)) displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
} }
is SendMessageResult.SlashCommandResultOk -> { is SendMessageResult.SlashCommandResultOk -> {
// Ignore // Ignore
} }
is SendMessageResult.SlashCommandResultError -> { is SendMessageResult.SlashCommandResultError -> {
displayCommandError(sendMessageResult.throwable.localizedMessage) displayCommandError(sendMessageResult.throwable.localizedMessage)
} }
is SendMessageResult.SlashCommandNotImplemented -> { is SendMessageResult.SlashCommandNotImplemented -> {
@ -577,11 +565,8 @@ class RoomDetailFragment :
override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean { override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val roomId = (arguments?.get(MvRx.KEY_ARG) as? RoomDetailArgs)?.roomId val roomId = roomDetailArgs.roomId
if (roomId.isNullOrBlank()) {
Timber.e("Missing RoomId, cannot open bottomsheet")
return false
}
this.view?.hideKeyboard() this.view?.hideKeyboard()
MessageActionsBottomSheet MessageActionsBottomSheet
.newInstance(roomId, informationData) .newInstance(roomId, informationData)
@ -624,22 +609,22 @@ class RoomDetailFragment :
it?.getContentIfNotHandled()?.let { actionData -> it?.getContentIfNotHandled()?.let { actionData ->
when (actionData.actionId) { when (actionData.actionId) {
MessageMenuViewModel.ACTION_ADD_REACTION -> { MessageMenuViewModel.ACTION_ADD_REACTION -> {
val eventId = actionData.data?.toString() ?: return val eventId = actionData.data?.toString() ?: return
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE) startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE)
} }
MessageMenuViewModel.ACTION_COPY -> { MessageMenuViewModel.ACTION_COPY -> {
//I need info about the current selected message :/ //I need info about the current selected message :/
copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false) copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false)
val snack = Snackbar.make(view!!, requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) 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.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
snack.show() snack.show()
} }
MessageMenuViewModel.ACTION_DELETE -> { MessageMenuViewModel.ACTION_DELETE -> {
val eventId = actionData.data?.toString() ?: return val eventId = actionData.data?.toString() ?: return
roomDetailViewModel.process(RoomDetailActions.RedactAction(eventId, context?.getString(R.string.event_redacted_by_user_reason))) 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 //TODO current data communication is too limited
//Need to now the media type //Need to now the media type
actionData.data?.toString()?.let { actionData.data?.toString()?.let {
@ -682,25 +667,25 @@ class RoomDetailFragment :
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } .setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
.show() .show()
} }
MessageMenuViewModel.ACTION_QUICK_REACT -> { MessageMenuViewModel.ACTION_QUICK_REACT -> {
//eventId,ClickedOn,Opposite //eventId,ClickedOn,Opposite
(actionData.data as? Triple<String, String, String>)?.let { (eventId, clickedOn, opposite) -> (actionData.data as? Triple<String, String, String>)?.let { (eventId, clickedOn, opposite) ->
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite)) roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite))
} }
} }
MessageMenuViewModel.ACTION_EDIT -> { MessageMenuViewModel.ACTION_EDIT -> {
val eventId = actionData.data.toString() val eventId = actionData.data.toString()
roomDetailViewModel.process(RoomDetailActions.EnterEditMode(eventId)) roomDetailViewModel.process(RoomDetailActions.EnterEditMode(eventId))
} }
MessageMenuViewModel.ACTION_QUOTE -> { MessageMenuViewModel.ACTION_QUOTE -> {
val eventId = actionData.data.toString() val eventId = actionData.data.toString()
roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(eventId)) roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(eventId))
} }
MessageMenuViewModel.ACTION_REPLY -> { MessageMenuViewModel.ACTION_REPLY -> {
val eventId = actionData.data.toString() val eventId = actionData.data.toString()
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
} }
else -> { else -> {
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show() 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) { 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.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
snack.show() snack.show()
} }

View File

@ -37,7 +37,6 @@ import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.command.CommandParser import im.vector.riotredesign.features.command.CommandParser
import im.vector.riotredesign.features.command.ParsedCommand 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 im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
@ -49,8 +48,7 @@ import java.util.concurrent.TimeUnit
class RoomDetailViewModel(initialState: RoomDetailViewState, class RoomDetailViewModel(initialState: RoomDetailViewState,
private val session: Session, private val session: Session
private val visibleRoomHolder: VisibleRoomStore
) : VectorViewModel<RoomDetailViewState>(initialState) { ) : VectorViewModel<RoomDetailViewState>(initialState) {
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
@ -66,8 +64,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? { override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? {
val currentSession = viewModelContext.activity.get<Session>() val currentSession = viewModelContext.activity.get<Session>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>() return RoomDetailViewModel(state, currentSession)
return RoomDetailViewModel(state, currentSession, visibleRoomHolder)
} }
} }
@ -82,21 +79,20 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
fun process(action: RoomDetailActions) { fun process(action: RoomDetailActions) {
when (action) { when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> handleIsDisplayed() is RoomDetailActions.SendMedia -> handleSendMedia(action)
is RoomDetailActions.SendMedia -> handleSendMedia(action) is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) is RoomDetailActions.LoadMore -> handleLoadMore(action)
is RoomDetailActions.LoadMore -> handleLoadMore(action) is RoomDetailActions.SendReaction -> handleSendReaction(action)
is RoomDetailActions.SendReaction -> handleSendReaction(action) is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
is RoomDetailActions.AcceptInvite -> handleAcceptInvite() is RoomDetailActions.RejectInvite -> handleRejectInvite()
is RoomDetailActions.RejectInvite -> handleRejectInvite() is RoomDetailActions.RedactAction -> handleRedactEvent(action)
is RoomDetailActions.RedactAction -> handleRedactEvent(action) is RoomDetailActions.UndoReaction -> handleUndoReact(action)
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action) is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action) is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action)
is RoomDetailActions.EnterEditMode -> handleEditAction(action) is RoomDetailActions.EnterEditMode -> handleEditAction(action)
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
} }
} }
@ -136,69 +132,69 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
val slashCommandResult = CommandParser.parseSplashCommand(action.text) val slashCommandResult = CommandParser.parseSplashCommand(action.text)
when (slashCommandResult) { when (slashCommandResult) {
is ParsedCommand.ErrorNotACommand -> { is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room // Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
} }
is ParsedCommand.ErrorSyntax -> { is ParsedCommand.ErrorSyntax -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)))
} }
is ParsedCommand.ErrorEmptySlashCommand -> { is ParsedCommand.ErrorEmptySlashCommand -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/"))) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/")))
} }
is ParsedCommand.ErrorUnknownSlashCommand -> { is ParsedCommand.ErrorUnknownSlashCommand -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)))
} }
is ParsedCommand.Invite -> { is ParsedCommand.Invite -> {
handleInviteSlashCommand(slashCommandResult) handleInviteSlashCommand(slashCommandResult)
} }
is ParsedCommand.SetUserPowerLevel -> { is ParsedCommand.SetUserPowerLevel -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.ClearScalarToken -> { is ParsedCommand.ClearScalarToken -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.SetMarkdown -> { is ParsedCommand.SetMarkdown -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.UnbanUser -> { is ParsedCommand.UnbanUser -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.BanUser -> { is ParsedCommand.BanUser -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.KickUser -> { is ParsedCommand.KickUser -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.JoinRoom -> { is ParsedCommand.JoinRoom -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.PartRoom -> { is ParsedCommand.PartRoom -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.SendEmote -> { is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
} }
is ParsedCommand.ChangeTopic -> { is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(slashCommandResult) handleChangeTopicSlashCommand(slashCommandResult)
} }
is ParsedCommand.ChangeDisplayName -> { is ParsedCommand.ChangeDisplayName -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
} }
} }
SendMode.EDIT -> { SendMode.EDIT -> {
room.editTextMessage(state.selectedEvent?.root?.eventId ?: "", action.text, action.autoMarkdown) room.editTextMessage(state.selectedEvent?.root?.eventId ?: "", action.text, action.autoMarkdown)
setState { setState {
copy( copy(
@ -208,7 +204,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
} }
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
} }
SendMode.QUOTE -> { SendMode.QUOTE -> {
val messageContent: MessageContent? = val messageContent: MessageContent? =
state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel() state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel()
?: state.selectedEvent?.root?.content.toModel() ?: state.selectedEvent?.root?.content.toModel()
@ -234,7 +230,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
} }
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
} }
SendMode.REPLY -> { SendMode.REPLY -> {
state.selectedEvent?.let { state.selectedEvent?.let {
room.replyToMessage(it.root, action.text) room.replyToMessage(it.root, action.text)
setState { setState {
@ -356,10 +352,6 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
displayedEventsObservable.accept(action) displayedEventsObservable.accept(action)
} }
private fun handleIsDisplayed() {
visibleRoomHolder.post(roomId)
}
private fun handleLoadMore(action: RoomDetailActions.LoadMore) { private fun handleLoadMore(action: RoomDetailActions.LoadMore) {
timeline.paginate(action.direction, PAGINATION_COUNT) timeline.paginate(action.direction, PAGINATION_COUNT)
} }
@ -388,6 +380,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
} }
} }
} }
private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) { private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) {
room.getTimeLineEvent(action.eventId)?.let { room.getTimeLineEvent(action.eventId)?.let {
setState { setState {
@ -400,7 +393,6 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
} }
private fun observeEventDisplayedActions() { private fun observeEventDisplayedActions() {
// We are buffering scroll events for one second // We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on. // 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 -> summary.lastMessage?.sender?.let { senderId ->
session.getUser(senderId) session.getUser(senderId)
}?.also { }?.also {
setState { copy(inviter = Success(it)) } setState { copy(asyncInviter = Success(it)) }
} }
} }
} }

View File

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

View File

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

View File

@ -1,59 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
class CallItemFactory(private val stringProvider: StringProvider) {
fun create(event: TimelineEvent): NoticeItem? {
val text = buildNoticeText(event.root, event.senderName) ?: return null
return NoticeItem_()
.noticeText(text)
.avatarUrl(event.senderAvatar)
.memberName(event.senderName)
}
private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
return when {
EventType.CALL_INVITE == event.type -> {
val content = event.content.toModel<CallInviteContent>() ?: return null
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
return if (isVideoCall) {
stringProvider.getString(R.string.notice_placed_video_call, senderName)
} else {
stringProvider.getString(R.string.notice_placed_voice_call, senderName)
}
}
EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, senderName)
EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName)
else -> null
}
}
}

View File

@ -16,28 +16,24 @@
package im.vector.riotredesign.features.home.room.detail.timeline.factory 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.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.core.resources.StringProvider 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
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? { 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_() return NoticeItem_()
.noticeText(text) .noticeText(formattedText)
.avatarUrl(event.senderAvatar) .avatarUrl(senderAvatar)
.memberName(event.senderName) .memberName(senderName)
} }

View File

@ -1,55 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvider) {
fun create(event: TimelineEvent): NoticeItem? {
val noticeText = buildNoticeText(event.root, event.senderName) ?: return null
return NoticeItem_()
.noticeText(noticeText)
.avatarUrl(event.senderAvatar)
.memberName(event.senderName)
}
private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
val content = event.content.toModel<RoomHistoryVisibilityContent>() ?: return null
val formattedVisibility = when (content.historyVisibility) {
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited)
RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined)
RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable)
}
return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility)
}
}

View File

@ -1,135 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.detail.timeline.factory
import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
//TODO : complete with call membership events¬
class RoomMemberItemFactory(private val stringProvider: StringProvider) {
fun create(event: TimelineEvent): NoticeItem? {
val eventContent: RoomMember? = event.root.content.toModel()
val prevEventContent: RoomMember? = event.root.prevContent.toModel()
val noticeText = buildRoomMemberNotice(event, eventContent, prevEventContent) ?: return null
val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, event)
val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event)
return NoticeItem_()
.userId(event.root.sender ?: "")
.noticeText(noticeText)
.avatarUrl(senderAvatar)
.memberName(senderName)
}
private fun buildRoomMemberNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
return if (isMembershipEvent) {
buildMembershipNotice(event, eventContent, prevEventContent)
} else {
buildProfileNotice(event, eventContent, prevEventContent)
}
}
private fun buildProfileNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val displayText = StringBuilder()
// Check display name has been changed
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
val displayNameText = when {
prevEventContent?.displayName.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_display_name_set, event.root.sender, eventContent?.displayName)
eventContent?.displayName.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_display_name_removed, event.root.sender, prevEventContent?.displayName)
else ->
stringProvider.getString(R.string.notice_display_name_changed_from,
event.root.sender, prevEventContent?.displayName, eventContent?.displayName)
}
displayText.append(displayNameText)
}
// Check whether the avatar has been changed
if (!TextUtils.equals(eventContent?.avatarUrl, prevEventContent?.avatarUrl)) {
val displayAvatarText = if (displayText.isNotEmpty()) {
displayText.append(" ")
stringProvider.getString(R.string.notice_avatar_changed_too)
} else {
stringProvider.getString(R.string.notice_avatar_url_changed, event.senderName)
}
displayText.append(displayAvatarText)
}
return displayText.toString()
}
private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val senderDisplayName = event.senderName ?: event.root.sender
val targetDisplayName = eventContent?.displayName ?: event.root.sender
return when {
Membership.INVITE == eventContent?.membership -> {
// TODO get userId
val selfUserId = ""
when {
eventContent.thirdPartyInvite != null ->
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
TextUtils.equals(event.root.stateKey, selfUserId) ->
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
event.root.stateKey.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
else ->
stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
}
}
Membership.JOIN == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_join, senderDisplayName)
Membership.LEAVE == eventContent?.membership ->
// 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
return if (TextUtils.equals(event.root.sender, event.root.stateKey)) {
if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
} else {
val leftDisplayName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event)
stringProvider.getString(R.string.notice_room_leave, leftDisplayName)
}
} else if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.JOIN) {
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.BAN) {
stringProvider.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName)
} else {
null
}
Membership.BAN == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
Membership.KNOCK == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
else -> null
}
}
}

View File

@ -1,45 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.detail.timeline.factory
import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
class RoomNameItemFactory(private val stringProvider: StringProvider) {
fun create(event: TimelineEvent): NoticeItem? {
val content: RoomNameContent = event.root.content.toModel() ?: return null
val text = if (!TextUtils.isEmpty(content.name)) {
stringProvider.getString(R.string.notice_room_name_changed, event.senderName, content.name)
} else {
stringProvider.getString(R.string.notice_room_name_removed, event.senderName)
}
return NoticeItem_()
.noticeText(text)
.avatarUrl(event.senderAvatar)
.memberName(event.senderName)
}
}

View File

@ -23,11 +23,7 @@ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
private val roomNameItemFactory: RoomNameItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val roomTopicItemFactory: RoomTopicItemFactory,
private val roomMemberItemFactory: RoomMemberItemFactory,
private val roomHistoryVisibilityItemFactory: RoomHistoryVisibilityItemFactory,
private val callItemFactory: CallItemFactory,
private val defaultItemFactory: DefaultItemFactory) { private val defaultItemFactory: DefaultItemFactory) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
@ -36,23 +32,22 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
val computedModel = try { val computedModel = try {
when (event.root.type) { when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback) 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.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_HISTORY_VISIBILITY,
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> callItemFactory.create(event) EventType.CALL_ANSWER -> noticeItemFactory.create(event)
EventType.ENCRYPTED, EventType.ENCRYPTED,
EventType.ENCRYPTION, EventType.ENCRYPTION,
EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER, EventType.STICKER,
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)
else -> null
else -> null
} }
} catch (e: Exception) { } catch (e: Exception) {
defaultItemFactory.create(event, e) defaultItemFactory.create(event, e)

View File

@ -0,0 +1,185 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.detail.timeline.format
import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
import timber.log.Timber
class NoticeEventFormatter(private val stringProvider: StringProvider) {
fun format(timelineEvent: TimelineEvent): CharSequence? {
return when (timelineEvent.root.type) {
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName())
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderName)
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.senderName)
else -> {
Timber.v("Type ${timelineEvent.root.type} not handled by this formatter")
null
}
}
}
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
val content = event.content.toModel<RoomNameContent>() ?: return null
return if (!TextUtils.isEmpty(content.name)) {
stringProvider.getString(R.string.notice_room_name_changed, senderName, content.name)
} else {
stringProvider.getString(R.string.notice_room_name_removed, senderName)
}
}
private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
val content = event.content.toModel<RoomTopicContent>() ?: return null
return if (content.topic.isNullOrEmpty()) {
stringProvider.getString(R.string.notice_room_topic_removed, senderName)
} else {
stringProvider.getString(R.string.notice_room_topic_changed, senderName, content.topic)
}
}
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
val content = event.content.toModel<RoomHistoryVisibilityContent>() ?: return null
val formattedVisibility = when (content.historyVisibility) {
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited)
RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined)
RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable)
}
return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility)
}
private fun formatCallEvent(event: Event, senderName: String?): CharSequence? {
return when {
EventType.CALL_INVITE == event.type -> {
val content = event.content.toModel<CallInviteContent>() ?: return null
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
return if (isVideoCall) {
stringProvider.getString(R.string.notice_placed_video_call, senderName)
} else {
stringProvider.getString(R.string.notice_placed_voice_call, senderName)
}
}
EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, senderName)
EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName)
else -> null
}
}
private fun formatRoomMemberEvent(event: Event, senderName: String?): String? {
val eventContent: RoomMember? = event.content.toModel()
val prevEventContent: RoomMember? = event.prevContent.toModel()
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
return if (isMembershipEvent) {
buildMembershipNotice(event, senderName, eventContent, prevEventContent)
} else {
buildProfileNotice(event, senderName, eventContent, prevEventContent)
}
}
private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val displayText = StringBuilder()
// Check display name has been changed
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
val displayNameText = when {
prevEventContent?.displayName.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_display_name_set, event.sender, eventContent?.displayName)
eventContent?.displayName.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_display_name_removed, event.sender, prevEventContent?.displayName)
else ->
stringProvider.getString(R.string.notice_display_name_changed_from,
event.sender, prevEventContent?.displayName, eventContent?.displayName)
}
displayText.append(displayNameText)
}
// Check whether the avatar has been changed
if (!TextUtils.equals(eventContent?.avatarUrl, prevEventContent?.avatarUrl)) {
val displayAvatarText = if (displayText.isNotEmpty()) {
displayText.append(" ")
stringProvider.getString(R.string.notice_avatar_changed_too)
} else {
stringProvider.getString(R.string.notice_avatar_url_changed, senderName)
}
displayText.append(displayAvatarText)
}
return displayText.toString()
}
private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val senderDisplayName = senderName ?: event.sender
val targetDisplayName = eventContent?.displayName ?: event.sender
return when {
Membership.INVITE == eventContent?.membership -> {
// TODO get userId
val selfUserId = ""
when {
eventContent.thirdPartyInvite != null ->
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
TextUtils.equals(event.stateKey, selfUserId) ->
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
event.stateKey.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
else ->
stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
}
}
Membership.JOIN == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_join, senderDisplayName)
Membership.LEAVE == eventContent?.membership ->
// 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
return if (TextUtils.equals(event.sender, event.stateKey)) {
if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
} else {
stringProvider.getString(R.string.notice_room_leave, senderDisplayName)
}
} else if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.JOIN) {
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.BAN) {
stringProvider.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName)
} else {
null
}
Membership.BAN == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
Membership.KNOCK == eventContent?.membership ->
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
else -> null
}
}
}

View File

@ -16,25 +16,11 @@
package im.vector.riotredesign.features.home.room.detail.timeline.helper 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.model.RoomMember
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
object RoomMemberEventHelper { object RoomMemberEventHelper {
fun senderAvatar(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? {
return if (eventContent?.membership == Membership.LEAVE && eventContent.avatarUrl == null && prevEventContent?.avatarUrl != null) {
prevEventContent.avatarUrl
} else {
event.senderAvatar
}
}
fun senderName(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? {
return if (eventContent?.membership == Membership.LEAVE && eventContent.displayName == null && prevEventContent?.displayName != null) {
prevEventContent.displayName
} else {
event.senderName
}
}
} }

View File

@ -19,8 +19,8 @@ package im.vector.riotredesign.features.home.room.detail.timeline.helper
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.RelationType 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.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.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.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.extensions.localDateTime
@ -64,6 +64,26 @@ fun TimelineEvent.isDisplayable(): Boolean {
// } // }
//} //}
fun TimelineEvent.senderAvatar(): String? {
// We might have no avatar when user leave, so we try to get it from prevContent
return senderAvatar
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
root.prevContent.toModel<RoomMember>()?.avatarUrl
} else {
null
}
}
fun TimelineEvent.senderName(): String? {
// We might have no senderName when user leave, so we try to get it from prevContent
return senderName
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
root.prevContent.toModel<RoomMember>()?.displayName
} else {
null
}
}
fun TimelineEvent.canBeMerged(): Boolean { fun TimelineEvent.canBeMerged(): Boolean {
return root.type == EventType.STATE_ROOM_MEMBER return root.type == EventType.STATE_ROOM_MEMBER
} }

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.list
import im.vector.matrix.android.api.session.room.model.RoomSummary
class AlphabeticalRoomComparator
: Comparator<RoomSummary> {
override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
return when {
rightRoomSummary?.displayName == null -> -1
leftRoomSummary?.displayName == null -> 1
else -> leftRoomSummary.displayName.compareTo(rightRoomSummary.displayName)
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.list
import im.vector.matrix.android.api.session.room.model.RoomSummary
class ChronologicalRoomComparator : Comparator<RoomSummary> {
override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
var rightTimestamp = 0L
var leftTimestamp = 0L
if (null != leftRoomSummary) {
leftTimestamp = leftRoomSummary.lastMessage?.originServerTs ?: 0
}
if (null != rightRoomSummary) {
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0
}
return if (rightRoomSummary?.lastMessage == null) {
-1
} else if (leftRoomSummary?.lastMessage == null) {
1
} else {
val deltaTimestamp = rightTimestamp - leftTimestamp
if (deltaTimestamp > 0) {
1
} else if (deltaTimestamp < 0) {
-1
} else {
0
}
}
}
}

View File

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

View File

@ -16,21 +16,17 @@
package im.vector.riotredesign.features.home.room.list 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? { init {
return sharedPreferences.getString(SHARED_PREFS_SELECTED_ROOM_KEY, null) addDuration = ANIM_DURATION_IN_MILLIS
removeDuration = ANIM_DURATION_IN_MILLIS
moveDuration = 0
changeDuration = 0
} }
fun saveLastSelectedRoom(roomId: String) { }
sharedPreferences.edit()
.putString(SHARED_PREFS_SELECTED_ROOM_KEY, roomId)
.apply()
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.list
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import io.reactivex.functions.Predicate
class RoomListDisplayModeFilter(private val displayMode: RoomListFragment.DisplayMode) : Predicate<RoomSummary> {
override fun test(roomSummary: RoomSummary): Boolean {
return when (displayMode) {
RoomListFragment.DisplayMode.HOME -> roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE
RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
}
}
}

View File

@ -16,94 +16,255 @@
package im.vector.riotredesign.features.home.room.list package im.vector.riotredesign.features.home.room.list
import android.animation.Animator
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.os.Parcelable
import android.text.TextWatcher import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.Fail import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.*
import com.airbnb.mvrx.Success import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.failure.Failure 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.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R 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.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.extensions.observeEvent 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.StateView
import im.vector.riotredesign.core.platform.VectorBaseFragment import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.HomeModule import kotlinx.android.parcel.Parcelize
import im.vector.riotredesign.features.home.HomeNavigator
import kotlinx.android.synthetic.main.fragment_room_list.* import kotlinx.android.synthetic.main.fragment_room_list.*
import org.koin.android.ext.android.inject 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 { companion object {
fun newInstance(): RoomListFragment { fun newInstance(roomListParams: RoomListParams): RoomListFragment {
return RoomListFragment() return RoomListFragment().apply {
setArguments(roomListParams)
}
} }
} }
private val roomListParams: RoomListParams by args()
private val roomController by inject<RoomSummaryController>() private val roomController by inject<RoomSummaryController>()
private val homeNavigator by inject<HomeNavigator>()
private val roomListViewModel: RoomListViewModel by fragmentViewModel() private val roomListViewModel: RoomListViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_room_list override fun getLayoutResId() = R.layout.fragment_room_list
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(HomeModule.ROOM_LIST_SCOPE)) setupCreateRoomButton()
setupRecyclerView() setupRecyclerView()
setupFilterView()
roomListViewModel.subscribe { renderState(it) } roomListViewModel.subscribe { renderState(it) }
roomListViewModel.openRoomLiveData.observeEvent(this) { 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() { private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context) val layoutManager = LinearLayoutManager(context)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
epoxyRecyclerView.layoutManager = layoutManager epoxyRecyclerView.layoutManager = layoutManager
epoxyRecyclerView.itemAnimator = RoomListAnimator()
roomController.callback = this roomController.callback = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) } roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
stateView.contentView = epoxyRecyclerView stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController) epoxyRecyclerView.setController(roomController)
} }
private fun setupFilterView() { private val showFabRunnable = Runnable {
filterRoomView.setupAsSearch() fabButton.show()
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 fun renderState(state: RoomListViewState) { private fun renderState(state: RoomListViewState) {
when (state.asyncRooms) { when (state.asyncFilteredRooms) {
is Incomplete -> renderLoading() is Incomplete -> renderLoading()
is Success -> renderSuccess(state) is Success -> renderSuccess(state)
is Fail -> renderFailure(state.asyncRooms.error) is Fail -> renderFailure(state.asyncFilteredRooms.error)
} }
} }
private fun renderSuccess(state: RoomListViewState) { private fun renderSuccess(state: RoomListViewState) {
if (state.asyncRooms().isNullOrEmpty()) { val allRooms = state.asyncRooms()
stateView.state = StateView.State.Empty(getString(R.string.room_list_empty)) val filteredRooms = state.asyncFilteredRooms()
if (filteredRooms.isNullOrEmpty()) {
renderEmptyState(allRooms)
} else { } else {
stateView.state = StateView.State.Content stateView.state = StateView.State.Content
} }
roomController.setData(state) roomController.setData(state)
} }
private fun renderEmptyState(allRooms: List<RoomSummary>?) {
val hasNoRoom = allRooms
?.filter {
it.membership == Membership.JOIN || it.membership == Membership.INVITE
}
.isNullOrEmpty()
val emptyState = when (roomListParams.displayMode) {
DisplayMode.HOME -> {
if (hasNoRoom) {
StateView.State.Empty(
getString(R.string.room_list_catchup_welcome_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_catchup),
getString(R.string.room_list_catchup_welcome_body)
)
} else {
StateView.State.Empty(
getString(R.string.room_list_catchup_empty_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_noun_party_popper),
getString(R.string.room_list_catchup_empty_body))
}
}
DisplayMode.PEOPLE ->
StateView.State.Empty(
getString(R.string.room_list_people_empty_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_chat),
getString(R.string.room_list_people_empty_body)
)
DisplayMode.ROOMS ->
StateView.State.Empty(
getString(R.string.room_list_rooms_empty_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_group),
getString(R.string.room_list_rooms_empty_body)
)
}
stateView.state = emptyState
}
private fun renderLoading() { private fun renderLoading() {
stateView.state = StateView.State.Loading stateView.state = StateView.State.Loading
} }
@ -116,6 +277,15 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback {
stateView.state = StateView.State.Error(message) stateView.state = StateView.State.Error(message)
} }
override fun onBackPressed(): Boolean {
if (isFabMenuOpened) {
toggleFabMenu()
return true
}
return super.onBackPressed()
}
// RoomSummaryController.Callback ************************************************************** // RoomSummaryController.Callback **************************************************************
override fun onRoomSelected(room: RoomSummary) { override fun onRoomSelected(room: RoomSummary) {

View File

@ -23,28 +23,21 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.session.Session 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.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary 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.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.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.home.group.SelectedGroupStore import im.vector.riotredesign.features.home.HomeRoomListObservableStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import io.reactivex.Observable
import io.reactivex.functions.Function3
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit
typealias RoomListFilterName = CharSequence typealias RoomListFilterName = CharSequence
class RoomListViewModel(initialState: RoomListViewState, class RoomListViewModel(initialState: RoomListViewState,
private val session: Session, private val session: Session,
private val selectedGroupHolder: SelectedGroupStore, private val homeRoomListObservableSource: HomeRoomListObservableStore,
private val visibleRoomHolder: VisibleRoomStore, private val alphabeticalRoomComparator: AlphabeticalRoomComparator,
private val roomSelectionRepository: RoomSelectionRepository, private val chronologicalRoomComparator: ChronologicalRoomComparator)
private val roomSummaryComparator: RoomSummaryComparator)
: VectorViewModel<RoomListViewState>(initialState) { : VectorViewModel<RoomListViewState>(initialState) {
companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> { companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {
@ -52,15 +45,15 @@ class RoomListViewModel(initialState: RoomListViewState,
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? { override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? {
val currentSession = viewModelContext.activity.get<Session>() val currentSession = viewModelContext.activity.get<Session>()
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>() val homeRoomListObservableSource = viewModelContext.activity.get<HomeRoomListObservableStore>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>() val chronologicalRoomComparator = viewModelContext.activity.get<ChronologicalRoomComparator>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>() val alphabeticalRoomComparator = viewModelContext.activity.get<AlphabeticalRoomComparator>()
val roomSummaryComparator = viewModelContext.activity.get<RoomSummaryComparator>() return RoomListViewModel(state, currentSession, homeRoomListObservableSource, alphabeticalRoomComparator, chronologicalRoomComparator)
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator)
} }
} }
private val displayMode = initialState.displayMode
private val roomListDisplayModeFilter = RoomListDisplayModeFilter(displayMode)
private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty()) private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty())
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>() private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
@ -69,7 +62,6 @@ class RoomListViewModel(initialState: RoomListViewState,
init { init {
observeRoomSummaries() observeRoomSummaries()
observeVisibleRoom()
} }
fun accept(action: RoomListActions) { fun accept(action: RoomListActions) {
@ -82,11 +74,8 @@ class RoomListViewModel(initialState: RoomListViewState,
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state -> private fun handleSelectRoom(action: RoomListActions.SelectRoom) {
if (state.visibleRoomId != action.roomSummary.roomId) { _openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
}
} }
private fun handleFilterRooms(action: RoomListActions.FilterRooms) { private fun handleFilterRooms(action: RoomListActions.FilterRooms) {
@ -98,61 +87,21 @@ class RoomListViewModel(initialState: RoomListViewState,
this.toggle(action.category) this.toggle(action.category)
} }
private fun observeVisibleRoom() {
visibleRoomHolder.observe()
.doOnNext {
setState { copy(visibleRoomId = it) }
}
.subscribe()
.disposeOnClear()
}
private fun observeRoomSummaries() { private fun observeRoomSummaries() {
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>( homeRoomListObservableSource
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS), .observe()
selectedGroupHolder.observe(), .execute { asyncRooms ->
roomListFilter.throttleLast(300, TimeUnit.MILLISECONDS), copy(asyncRooms = asyncRooms)
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.observeFilteredBy(displayMode)
.map { buildRoomSummaries(it) }
.execute { async -> .execute { async ->
copy( copy(asyncFilteredRooms = async)
asyncRooms = async
)
} }
} }
private fun filterRooms(rooms: List<RoomSummary>, filterRoomOption: Option<RoomListFilterName>): List<RoomSummary> {
val filterRoom = filterRoomOption.orNull()
return rooms.filter {
if (filterRoom.isNullOrBlank()) {
true
} else {
it.displayName.contains(other = filterRoom, ignoreCase = true)
}
}
}
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries { private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
val invites = ArrayList<RoomSummary>() val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>() val favourites = ArrayList<RoomSummary>()
@ -174,13 +123,19 @@ class RoomListViewModel(initialState: RoomListViewState,
} }
} }
val roomComparator = when (displayMode) {
RoomListFragment.DisplayMode.HOME -> chronologicalRoomComparator
RoomListFragment.DisplayMode.PEOPLE -> chronologicalRoomComparator
RoomListFragment.DisplayMode.ROOMS -> alphabeticalRoomComparator
}
return RoomSummaries().apply { return RoomSummaries().apply {
put(RoomCategory.INVITE, invites.sortedWith(roomSummaryComparator)) put(RoomCategory.INVITE, invites.sortedWith(roomComparator))
put(RoomCategory.FAVOURITE, favourites.sortedWith(roomSummaryComparator)) put(RoomCategory.FAVOURITE, favourites.sortedWith(roomComparator))
put(RoomCategory.DIRECT, directChats.sortedWith(roomSummaryComparator)) put(RoomCategory.DIRECT, directChats.sortedWith(roomComparator))
put(RoomCategory.GROUP, groupRooms.sortedWith(roomSummaryComparator)) put(RoomCategory.GROUP, groupRooms.sortedWith(roomComparator))
put(RoomCategory.LOW_PRIORITY, lowPriorities.sortedWith(roomSummaryComparator)) put(RoomCategory.LOW_PRIORITY, lowPriorities.sortedWith(roomComparator))
put(RoomCategory.SERVER_NOTICE, serverNotices.sortedWith(roomSummaryComparator)) put(RoomCategory.SERVER_NOTICE, serverNotices.sortedWith(roomComparator))
} }
} }

View File

@ -24,8 +24,9 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
data class RoomListViewState( data class RoomListViewState(
val asyncRooms: Async<RoomSummaries> = Uninitialized, val displayMode: RoomListFragment.DisplayMode,
val visibleRoomId: String? = null, val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val asyncFilteredRooms: Async<RoomSummaries> = Uninitialized,
val isInviteExpanded: Boolean = true, val isInviteExpanded: Boolean = true,
val isFavouriteRoomsExpanded: Boolean = true, val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = true, val isDirectRoomsExpanded: Boolean = true,
@ -34,6 +35,8 @@ data class RoomListViewState(
val isServerNoticeRoomsExpanded: Boolean = true val isServerNoticeRoomsExpanded: Boolean = true
) : MvRxState { ) : MvRxState {
constructor(args: RoomListParams) : this(displayMode = args.displayMode)
fun isCategoryExpanded(roomCategory: RoomCategory): Boolean { fun isCategoryExpanded(roomCategory: RoomCategory): Boolean {
return when (roomCategory) { return when (roomCategory) {
RoomCategory.INVITE -> isInviteExpanded RoomCategory.INVITE -> isInviteExpanded
@ -69,5 +72,5 @@ enum class RoomCategory(@StringRes val titleRes: Int) {
} }
fun RoomSummaries?.isNullOrEmpty(): Boolean { fun RoomSummaries?.isNullOrEmpty(): Boolean {
return this == null || isEmpty() return this == null || this.values.flatten().isEmpty()
} }

View File

@ -18,16 +18,25 @@ package im.vector.riotredesign.features.home.room.list
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController 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.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.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
class RoomSummaryController(private val stringProvider: StringProvider class RoomSummaryController(private val stringProvider: StringProvider,
private val eventFormatter: NoticeEventFormatter,
private val timelineDateFormatter: TimelineDateFormatter
) : TypedEpoxyController<RoomListViewState>() { ) : TypedEpoxyController<RoomListViewState>() {
var callback: Callback? = null var callback: Callback? = null
override fun buildModels(viewState: RoomListViewState) { override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncRooms() val roomSummaries = viewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) -> roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) { if (summaries.isEmpty()) {
return@forEach return@forEach
@ -37,7 +46,7 @@ class RoomSummaryController(private val stringProvider: StringProvider
callback?.onToggleRoomCategory(category) callback?.onToggleRoomCategory(category)
} }
if (isExpanded) { if (isExpanded) {
buildRoomModels(summaries, viewState.visibleRoomId) buildRoomModels(summaries)
} }
} }
} }
@ -71,18 +80,41 @@ class RoomSummaryController(private val stringProvider: StringProvider
} }
} }
private fun buildRoomModels(summaries: List<RoomSummary>, selectedRoomId: String?) { private fun buildRoomModels(summaries: List<RoomSummary>) {
summaries.forEach { roomSummary -> summaries.forEach { roomSummary ->
val unreadCount = roomSummary.notificationCount val unreadCount = roomSummary.notificationCount
val showHighlighted = roomSummary.highlightCount > 0 val showHighlighted = roomSummary.highlightCount > 0
val isSelected = roomSummary.roomId == selectedRoomId
var lastMessageFormatted: CharSequence = ""
var lastMessageTime: CharSequence = ""
val lastMessage = roomSummary.lastMessage
if (lastMessage != null) {
val date = lastMessage.localDateTime()
val currentData = DateProvider.currentLocalDateTime()
val isSameDay = date.toLocalDate() == currentData.toLocalDate()
//TODO: get formatted
if (lastMessage.type == EventType.MESSAGE) {
val content = lastMessage.content?.toModel<MessageContent>()
lastMessageFormatted = content?.body ?: ""
} else {
lastMessageFormatted = lastMessage.type
}
lastMessageTime = if (isSameDay) {
timelineDateFormatter.formatMessageHour(date)
} else {
//TODO: change this
timelineDateFormatter.formatMessageDay(date)
}
}
roomSummaryItem { roomSummaryItem {
id(roomSummary.roomId) id(roomSummary.roomId)
roomId(roomSummary.roomId) roomId(roomSummary.roomId)
lastEventTime(lastMessageTime)
lastFormattedEvent(lastMessageFormatted)
roomName(roomSummary.displayName) roomName(roomSummary.displayName)
avatarUrl(roomSummary.avatarUrl) avatarUrl(roomSummary.avatarUrl)
selected(isSelected)
showHighlighted(showHighlighted) showHighlighted(showHighlighted)
unreadCount(unreadCount) unreadCount(unreadCount)
listener { callback?.onRoomSelected(roomSummary) } listener { callback?.onRoomSelected(roomSummary) }

View File

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

View File

@ -16,6 +16,7 @@
package im.vector.riotredesign.features.home.room.list package im.vector.riotredesign.features.home.room.list
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
@ -23,7 +24,6 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.platform.CheckableFrameLayout
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
@ -32,8 +32,9 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
@EpoxyAttribute lateinit var roomName: CharSequence @EpoxyAttribute lateinit var roomName: CharSequence
@EpoxyAttribute lateinit var roomId: String @EpoxyAttribute lateinit var roomId: String
@EpoxyAttribute lateinit var lastFormattedEvent: CharSequence
@EpoxyAttribute lateinit var lastEventTime: CharSequence
@EpoxyAttribute var avatarUrl: String? = null @EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var unreadCount: Int = 0 @EpoxyAttribute var unreadCount: Int = 0
@EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null @EpoxyAttribute var listener: (() -> Unit)? = null
@ -41,18 +42,21 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.unreadCounterBadgeView.render(unreadCount, showHighlighted)
holder.rootView.isChecked = selected
holder.rootView.setOnClickListener { listener?.invoke() } holder.rootView.setOnClickListener { listener?.invoke() }
holder.titleView.text = roomName 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) AvatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
val titleView by bind<TextView>(R.id.roomNameView) val titleView by bind<TextView>(R.id.roomNameView)
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
val lastEventView by bind<TextView>(R.id.roomLastEventView)
val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView) val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
val rootView by bind<CheckableFrameLayout>(R.id.itemRoomLayout) val rootView by bind<ViewGroup>(R.id.itemRoomLayout)
} }
} }

View File

@ -29,24 +29,24 @@ class UnreadCounterBadgeView : AppCompatTextView {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
fun render(count: Int, highlighted: Boolean) { fun render(state: State) {
if (count == 0) { if (state.count == 0) {
visibility = View.INVISIBLE visibility = View.INVISIBLE
} else { } else {
visibility = View.VISIBLE visibility = View.VISIBLE
val bgRes = if (highlighted) { val bgRes = if (state.highlighted) {
R.drawable.bg_unread_highlight R.drawable.bg_unread_highlight
} else { } else {
R.drawable.bg_unread_notification R.drawable.bg_unread_notification
} }
setBackgroundResource(bgRes) setBackgroundResource(bgRes)
text = RoomSummaryFormatter.formatUnreadMessagesCounter(count) text = RoomSummaryFormatter.formatUnreadMessagesCounter(state.count)
} }
} }
enum class Status { data class State(
NOTIFICATION, val count: Int,
HIGHLIGHT val highlighted: Boolean
} )
} }

View File

@ -21,7 +21,6 @@ import android.graphics.Color
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R 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) { fun render(sender: User, mode: Mode = Mode.LARGE) {
if (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) AvatarRenderer.render(sender.avatarUrl, sender.userId, sender.displayName, inviteAvatarView)
inviteIdentifierView.text = sender.userId inviteIdentifierView.text = sender.userId
inviteNameView.text = sender.displayName inviteNameView.text = sender.displayName
inviteLabelView.text = context.getString(R.string.send_you_invite) inviteLabelView.text = context.getString(R.string.send_you_invite)
} else { } else {
updateLayoutParams { height = ConstraintLayout.LayoutParams.WRAP_CONTENT } updateLayoutParams { height = LayoutParams.WRAP_CONTENT }
inviteAvatarView.visibility = View.GONE inviteAvatarView.visibility = View.GONE
inviteIdentifierView.visibility = View.GONE inviteIdentifierView.visibility = View.GONE
inviteNameView.visibility = View.GONE inviteNameView.visibility = View.GONE

View File

@ -0,0 +1,53 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.navigation
import android.app.Activity
import android.content.Intent
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotredesign.features.home.room.detail.RoomDetailActivity
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotredesign.features.roomdirectory.roompreview.RoomPreviewActivity
import im.vector.riotredesign.features.settings.VectorSettingsActivity
class DefaultNavigator(private val fraqment: Fragment) : Navigator {
val activity: Activity = fraqment.requireActivity()
override fun openRoom(roomId: String) {
val args = RoomDetailArgs(roomId)
val intent = RoomDetailActivity.newIntent(activity, args)
activity.startActivity(intent)
}
override fun openRoomPreview(publicRoom: PublicRoom) {
val intent = RoomPreviewActivity.getIntent(activity, publicRoom)
activity.startActivity(intent)
}
override fun openRoomDirectory() {
val intent = Intent(activity, RoomDirectoryActivity::class.java)
activity.startActivity(intent)
}
override fun openSettings() {
val intent = VectorSettingsActivity.getIntent(activity, "TODO")
activity.startActivity(intent)
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.navigation
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
interface Navigator {
fun openRoom(roomId: String)
fun openRoomPreview(publicRoom: PublicRoom)
fun openRoomDirectory()
fun openSettings()
}

View File

@ -27,6 +27,7 @@ import butterknife.OnCheckedChanged
import butterknife.OnTextChanged import butterknife.OnTextChanged
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity import im.vector.riotredesign.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_bug_report.*
import timber.log.Timber import timber.log.Timber
/** /**
@ -68,7 +69,7 @@ class BugReportActivity : VectorBaseActivity() {
override fun getLayoutRes() = R.layout.activity_bug_report override fun getLayoutRes() = R.layout.activity_bug_report
override fun initUiAndData() { override fun initUiAndData() {
configureToolbar() configureToolbar(bugReportToolbar)
if (BugReporter.screenshot != null) { if (BugReporter.screenshot != null) {
mScreenShotPreview.setImageBitmap(BugReporter.screenshot) mScreenShotPreview.setImageBitmap(BugReporter.screenshot)

View File

@ -34,6 +34,7 @@ import androidx.lifecycle.ViewModelProviders
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity import im.vector.riotredesign.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.*
import timber.log.Timber import timber.log.Timber
/** /**
@ -80,8 +81,7 @@ class EmojiReactionPickerActivity : VectorBaseActivity() {
} }
override fun initUiAndData() { override fun initUiAndData() {
configureToolbar(emojiPickerToolbar)
configureToolbar()
requestEmojivUnicode10CompatibleFont() requestEmojivUnicode10CompatibleFont()

View File

@ -0,0 +1,28 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory
/**
* Join state of a room
*/
enum class JoinState {
NOT_JOINED,
JOINING,
JOINING_ERROR,
// Room is joined and this is confirmed by the sync
JOINED
}

View File

@ -24,19 +24,13 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel 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.core.platform.ButtonStateView
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_public_room) @EpoxyModelClass(layout = R.layout.item_public_room)
abstract class PublicRoomItem : VectorEpoxyModel<PublicRoomItem.Holder>() { abstract class PublicRoomItem : VectorEpoxyModel<PublicRoomItem.Holder>() {
enum class JoinState {
NOT_JOINED,
JOINING,
JOINING_ERROR,
JOINED
}
@EpoxyAttribute @EpoxyAttribute
var avatarUrl: String? = null var avatarUrl: String? = null
@ -46,6 +40,12 @@ abstract class PublicRoomItem : VectorEpoxyModel<PublicRoomItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var roomName: String? = null var roomName: String? = null
@EpoxyAttribute
var roomAlias: String? = null
@EpoxyAttribute
var roomTopic: String? = null
@EpoxyAttribute @EpoxyAttribute
var nbOfMembers: Int = 0 var nbOfMembers: Int = 0
@ -63,6 +63,8 @@ abstract class PublicRoomItem : VectorEpoxyModel<PublicRoomItem.Holder>() {
AvatarRenderer.render(avatarUrl, roomId!!, roomName, holder.avatarView) AvatarRenderer.render(avatarUrl, roomId!!, roomName, holder.avatarView)
holder.nameView.text = roomName holder.nameView.text = roomName
holder.aliasView.setTextOrHide(roomAlias)
holder.topicView.setTextOrHide(roomTopic)
// TODO Use formatter for big numbers? // TODO Use formatter for big numbers?
holder.counterView.text = nbOfMembers.toString() holder.counterView.text = nbOfMembers.toString()
@ -92,6 +94,8 @@ abstract class PublicRoomItem : VectorEpoxyModel<PublicRoomItem.Holder>() {
val avatarView by bind<ImageView>(R.id.itemPublicRoomAvatar) val avatarView by bind<ImageView>(R.id.itemPublicRoomAvatar)
val nameView by bind<TextView>(R.id.itemPublicRoomName) val nameView by bind<TextView>(R.id.itemPublicRoomName)
val aliasView by bind<TextView>(R.id.itemPublicRoomAlias)
val topicView by bind<TextView>(R.id.itemPublicRoomTopic)
val counterView by bind<TextView>(R.id.itemPublicRoomMembersCount) val counterView by bind<TextView>(R.id.itemPublicRoomMembersCount)
val buttonState by bind<ButtonStateView>(R.id.itemPublicRoomButtonState) val buttonState by bind<ButtonStateView>(R.id.itemPublicRoomButtonState)

View File

@ -82,24 +82,30 @@ class PublicRoomsController(private val stringProvider: StringProvider,
roomId(publicRoom.roomId) roomId(publicRoom.roomId)
avatarUrl(publicRoom.avatarUrl) avatarUrl(publicRoom.avatarUrl)
roomName(publicRoom.name) roomName(publicRoom.name)
roomAlias(publicRoom.canonicalAlias)
roomTopic(publicRoom.topic)
nbOfMembers(publicRoom.numJoinedMembers) nbOfMembers(publicRoom.numJoinedMembers)
when {
viewState.joinedRoomsIds.contains(publicRoom.roomId) -> joinState(PublicRoomItem.JoinState.JOINED) val joinState = when {
viewState.joiningRoomsIds.contains(publicRoom.roomId) -> joinState(PublicRoomItem.JoinState.JOINING) viewState.joinedRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINED
viewState.joiningErrorRoomsIds.contains(publicRoom.roomId) -> joinState(PublicRoomItem.JoinState.JOINING_ERROR) viewState.joiningRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINING
else -> joinState(PublicRoomItem.JoinState.NOT_JOINED) viewState.joiningErrorRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINING_ERROR
else -> JoinState.NOT_JOINED
} }
joinState(joinState)
joinListener { joinListener {
callback?.onPublicRoomJoin(publicRoom) callback?.onPublicRoomJoin(publicRoom)
} }
globalListener { globalListener {
callback?.onPublicRoomClicked(publicRoom) callback?.onPublicRoomClicked(publicRoom, joinState)
} }
} }
} }
interface Callback { interface Callback {
fun onPublicRoomClicked(publicRoom: PublicRoom) fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState)
fun onPublicRoomJoin(publicRoom: PublicRoom) fun onPublicRoomJoin(publicRoom: PublicRoom)
fun loadMore() fun loadMore()
} }

View File

@ -17,6 +17,7 @@
package im.vector.riotredesign.features.roomdirectory package im.vector.riotredesign.features.roomdirectory
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -43,14 +44,6 @@ import java.util.concurrent.TimeUnit
/** /**
* What can be improved: * 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 * - 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 { class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback {
@ -60,6 +53,8 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
override fun getLayoutResId() = R.layout.fragment_public_rooms override fun getLayoutResId() = R.layout.fragment_public_rooms
override fun getMenuRes() = R.menu.menu_room_directory
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -83,10 +78,6 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
vectorBaseActivity.notImplemented() vectorBaseActivity.notImplemented()
} }
publicRoomsChangeDirectory.setOnClickListener {
vectorBaseActivity.addFragmentToBackstack(RoomDirectoryPickerFragment(), R.id.simpleFragmentContainer)
}
viewModel.joinRoomErrorLiveData.observe(this, Observer { viewModel.joinRoomErrorLiveData.observe(this, Observer {
it.getContentIfNotHandled()?.let { throwable -> it.getContentIfNotHandled()?.let { throwable ->
Snackbar.make(publicRoomsCoordinator, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT) 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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE)) bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))
@ -114,9 +116,23 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
publicRoomsList.setController(publicRoomsController) publicRoomsList.setController(publicRoomsController)
} }
override fun onPublicRoomClicked(publicRoom: PublicRoom) { override fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) {
Timber.v("PublicRoomClicked: $publicRoom") 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) { override fun onPublicRoomJoin(publicRoom: PublicRoom) {
@ -131,8 +147,5 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
// Populate list with Epoxy // Populate list with Epoxy
publicRoomsController.setData(state) publicRoomsController.setData(state)
// Directory name
publicRoomsDirectoryName.text = state.roomDirectoryDisplayName
} }
} }

View File

@ -82,22 +82,25 @@ class RoomDirectoryViewModel(initialState: PublicRoomsViewState,
session session
.rx() .rx()
.liveRoomSummaries() .liveRoomSummaries()
.execute { async -> .subscribe { list ->
val joinedRoomIds = async.invoke() val joinedRoomIds = list
// Keep only joined room // Keep only joined room
?.filter { it.membership == Membership.JOIN } ?.filter { it.membership == Membership.JOIN }
?.map { it.roomId } ?.map { it.roomId }
?.toList() ?.toList()
?: emptyList() ?: emptyList()
copy( setState {
joinedRoomsIds = joinedRoomIds, copy(
// Remove (newly) joined room id from the joining room list joinedRoomsIds = joinedRoomIds,
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) }, // Remove (newly) joined room id from the joining room list
// Remove (newly) joined room id from the joining room list in error joiningRoomsIds = joiningRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) },
joiningErrorRoomsIds = joiningErrorRoomsIds.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) { fun setRoomDirectoryData(roomDirectoryData: RoomDirectoryData) {

View File

@ -41,7 +41,7 @@ class RoomDirectoryPickerController(private val stringProvider: StringProvider,
when (asyncThirdPartyProtocol) { when (asyncThirdPartyProtocol) {
is Success -> { is Success -> {
val directories = roomDirectoryListCreator.computeDirectories(asyncThirdPartyProtocol.invoke()) val directories = roomDirectoryListCreator.computeDirectories(asyncThirdPartyProtocol())
directories.forEach { directories.forEach {
buildDirectory(it) buildDirectory(it)
@ -88,7 +88,7 @@ class RoomDirectoryPickerController(private val stringProvider: StringProvider,
} }
interface Callback { interface Callback {
fun onRoomDirectoryClicked(roomDirectory: RoomDirectoryData) fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData)
fun retry() fun retry()
} }

View File

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

View File

@ -0,0 +1,88 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory.roompreview
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.widget.Toolbar
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.addFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
import kotlinx.android.parcel.Parcelize
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
@Parcelize
data class RoomPreviewData(
val roomId: String,
val roomName: String?,
val topic: String?,
val worldReadable: Boolean,
val avatarUrl: String?
) : Parcelable
class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
companion object {
private const val ARG = "ARG"
fun getIntent(context: Context, publicRoom: PublicRoom): Intent {
return Intent(context, RoomPreviewActivity::class.java).apply {
putExtra(ARG, RoomPreviewData(
roomId = publicRoom.roomId,
roomName = publicRoom.name,
topic = publicRoom.topic,
worldReadable = publicRoom.worldReadable,
avatarUrl = publicRoom.avatarUrl
))
}
}
}
override fun getLayoutRes() = R.layout.activity_simple
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))
}
override fun initUiAndData() {
if (isFirstCreation()) {
val args = intent.getParcelableExtra<RoomPreviewData>(ARG)
if (args.worldReadable) {
// TODO Room preview: Note: M does not recommend to use /events anymore, so for now we just display the room preview
// TODO the same way if it was not world readable
addFragment(RoomPreviewNoPreviewFragment.newInstance(args), R.id.simpleFragmentContainer)
} else {
addFragment(RoomPreviewNoPreviewFragment.newInstance(args), R.id.simpleFragmentContainer)
}
}
}
}

View File

@ -0,0 +1,114 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory.roompreview
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.transition.TransitionManager
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotredesign.R
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.extensions.setTextOrHide
import im.vector.riotredesign.core.platform.ButtonStateView
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.roomdirectory.JoinState
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
import kotlinx.android.synthetic.main.fragment_room_preview_no_preview.*
import org.koin.android.ext.android.get
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
/**
* Note: this Fragment is also used for world readable room for the moment
*/
class RoomPreviewNoPreviewFragment : VectorBaseFragment() {
companion object {
fun newInstance(arg: RoomPreviewData): Fragment {
return RoomPreviewNoPreviewFragment().apply { setArguments(arg) }
}
}
private val errorFormatter = get<ErrorFormatter>()
private val roomPreviewViewModel: RoomPreviewViewModel by fragmentViewModel()
private val roomPreviewData: RoomPreviewData by args()
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))
setupToolbar(roomPreviewNoPreviewToolbar)
}
override fun getLayoutResId() = R.layout.fragment_room_preview_no_preview
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Toolbar
AvatarRenderer.render(roomPreviewData.avatarUrl, roomPreviewData.roomId, roomPreviewData.roomName, roomPreviewNoPreviewToolbarAvatar)
roomPreviewNoPreviewToolbarTitle.text = roomPreviewData.roomName
// Screen
AvatarRenderer.render(roomPreviewData.avatarUrl, roomPreviewData.roomId, roomPreviewData.roomName, roomPreviewNoPreviewAvatar)
roomPreviewNoPreviewName.text = roomPreviewData.roomName
roomPreviewNoPreviewTopic.setTextOrHide(roomPreviewData.topic)
if (roomPreviewData.worldReadable) {
roomPreviewNoPreviewLabel.setText(R.string.room_preview_world_readable_room_not_supported_yet)
} else {
roomPreviewNoPreviewLabel.setText(R.string.room_preview_no_preview)
}
roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
roomPreviewViewModel.joinRoom()
}
override fun onRetryClicked() {
// Same action
onButtonClicked()
}
}
}
override fun invalidate() = withState(roomPreviewViewModel) { state ->
TransitionManager.beginDelayedTransition(roomPreviewNoPreviewRoot)
roomPreviewNoPreviewJoin.render(
when (state.roomJoinState) {
JoinState.NOT_JOINED -> ButtonStateView.State.Button
JoinState.JOINING -> ButtonStateView.State.Loading
JoinState.JOINED -> ButtonStateView.State.Loaded
JoinState.JOINING_ERROR -> ButtonStateView.State.Error
}
)
roomPreviewNoPreviewError.setTextOrHide(errorFormatter.toHumanReadable(state.lastError))
if (state.roomJoinState == JoinState.JOINED) {
// Quit this screen
requireActivity().finish()
// Open room
navigator.openRoom(roomPreviewData.roomId)
}
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory.roompreview
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.features.roomdirectory.JoinState
import org.koin.android.ext.android.get
import timber.log.Timber
class RoomPreviewViewModel(initialState: RoomPreviewViewState,
private val session: Session) : VectorViewModel<RoomPreviewViewState>(initialState) {
companion object : MvRxViewModelFactory<RoomPreviewViewModel, RoomPreviewViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomPreviewViewState): RoomPreviewViewModel? {
val currentSession = viewModelContext.activity.get<Session>()
return RoomPreviewViewModel(state, currentSession)
}
}
init {
// Observe joined room (from the sync)
observeJoinedRooms()
}
private fun observeJoinedRooms() {
session
.rx()
.liveRoomSummaries()
.subscribe { list ->
withState { state ->
val isRoomJoined = list
// Keep only joined room
?.filter { it.membership == Membership.JOIN }
?.map { it.roomId }
?.toList()
?.contains(state.roomId) == true
if (isRoomJoined) {
setState {
copy(
roomJoinState = JoinState.JOINED
)
}
}
}
}
.disposeOnClear()
}
fun joinRoom() = withState { state ->
if (state.roomJoinState == JoinState.JOINING) {
// Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
setState {
copy(
roomJoinState = JoinState.JOINING,
lastError = null
)
}
session.joinRoom(state.roomId, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined
}
override fun onFailure(failure: Throwable) {
setState {
copy(
roomJoinState = JoinState.JOINING_ERROR,
lastError = failure
)
}
}
})
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory.roompreview
import com.airbnb.mvrx.MvRxState
import im.vector.riotredesign.features.roomdirectory.JoinState
data class RoomPreviewViewState(
// The room id
val roomId: String = "",
// Current state of the room in preview
val roomJoinState: JoinState = JoinState.NOT_JOINED,
// Last error of join room request
val lastError: Throwable? = null
) : MvRxState {
constructor(args: RoomPreviewData) : this(roomId = args.roomId)
}

View File

@ -24,6 +24,7 @@ import androidx.preference.PreferenceFragmentCompat
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity import im.vector.riotredesign.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_vector_settings.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
/** /**
@ -45,7 +46,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
private val session by inject<Session>() private val session by inject<Session>()
override fun initUiAndData() { override fun initUiAndData() {
configureToolbar() configureToolbar(settingsToolbar)
if (isFirstCreation()) { if (isFirstCreation()) {
vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId) vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId)

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval"> android:shape="rectangle">
<corners android:radius="40dp" />
<solid android:color="@color/grey_lynch" /> <solid android:color="@color/grey_lynch" />
</shape> </shape>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="8dp"
android:height="8dp" />
<solid android:color="#FF4B55" />
</shape>

View File

@ -6,7 +6,7 @@
android:orientation="vertical"> android:orientation="vertical">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/bugReportToolbar"
style="@style/VectorToolbarStyle" style="@style/VectorToolbarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />

View File

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

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/roomDetailContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>

View File

@ -10,7 +10,7 @@
android:orientation="vertical"> android:orientation="vertical">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/settingsToolbar"
style="@style/VectorToolbarStyle" style="@style/VectorToolbarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />

View File

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

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