Join room from room preview

This commit is contained in:
Benoit Marty 2019-05-28 17:03:27 +02:00
parent 33fbcc8ba3
commit 6244913ab9
14 changed files with 277 additions and 32 deletions

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,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

@ -30,13 +30,6 @@ 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



View File

@ -85,10 +85,10 @@ class PublicRoomsController(private val stringProvider: StringProvider,
nbOfMembers(publicRoom.numJoinedMembers) nbOfMembers(publicRoom.numJoinedMembers)


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


joinState(joinState) joinState(joinState)
@ -103,7 +103,7 @@ class PublicRoomsController(private val stringProvider: StringProvider,
} }


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

View File

@ -117,21 +117,21 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
publicRoomsList.setController(publicRoomsController) publicRoomsList.setController(publicRoomsController)
} }


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


when (joinState) { when (joinState) {
PublicRoomItem.JoinState.JOINED -> { JoinState.JOINED -> {
val args = RoomDetailArgs(publicRoom.roomId) val args = RoomDetailArgs(publicRoom.roomId)
val roomDetailIntent = RoomDetailActivity.newIntent(requireActivity(), args) val roomDetailIntent = RoomDetailActivity.newIntent(requireActivity(), args)
requireActivity().startActivity(roomDetailIntent) requireActivity().startActivity(roomDetailIntent)
} }
PublicRoomItem.JoinState.NOT_JOINED, JoinState.NOT_JOINED,
PublicRoomItem.JoinState.JOINING_ERROR -> { JoinState.JOINING_ERROR -> {
// ROOM PREVIEW // ROOM PREVIEW
requireActivity().startActivity(RoomPreviewActivity.getIntent(requireActivity(), publicRoom)) requireActivity().startActivity(RoomPreviewActivity.getIntent(requireActivity(), publicRoom))
} }
else -> { else -> {
Snackbar.make(publicRoomsCoordinator, getString(R.string.please_wait), Snackbar.LENGTH_SHORT) Snackbar.make(publicRoomsCoordinator, getString(R.string.please_wait), Snackbar.LENGTH_SHORT)
.show() .show()
} }

View File

@ -19,13 +19,20 @@ package im.vector.riotredesign.features.roomdirectory.roompreview
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.transition.TransitionManager
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.extensions.setTextOrHide 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.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.roomdirectory.JoinState
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
import kotlinx.android.synthetic.main.fragment_room_preview_no_preview.* 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.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope import org.koin.android.scope.ext.android.getOrCreateScope


@ -37,12 +44,17 @@ class RoomPreviewNoPreviewFragment : VectorBaseFragment() {
} }
} }


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

private val roomPreviewData: RoomPreviewData by args() private val roomPreviewData: RoomPreviewData by args()


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))
setupToolbar(roomPreviewNoPreviewToolbar) setupToolbar(roomPreviewNoPreviewToolbar)

roomPreviewViewModel.init(roomPreviewData.roomId)
} }


override fun getLayoutResId() = R.layout.fragment_room_preview_no_preview override fun getLayoutResId() = R.layout.fragment_room_preview_no_preview
@ -54,8 +66,35 @@ class RoomPreviewNoPreviewFragment : VectorBaseFragment() {
roomPreviewNoPreviewName.text = roomPreviewData.roomName roomPreviewNoPreviewName.text = roomPreviewData.roomName
roomPreviewNoPreviewTopic.setTextOrHide(roomPreviewData.topic) roomPreviewNoPreviewTopic.setTextOrHide(roomPreviewData.topic)


roomPreviewNoPreviewJoin.setOnClickListener { roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback {
vectorBaseActivity.notImplemented("Join from preview") 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) {
// TODO Quit this screen and open the room
vectorBaseActivity.notImplemented("Open newly join room")
} }
} }
} }

View File

@ -0,0 +1,111 @@
/*
* 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()
.execute { async ->
val isRoomJoined = async.invoke()
// Keep only joined room
?.filter { it.membership == Membership.JOIN }
?.map { it.roomId }
?.toList()
?.contains(roomId) == true

if (isRoomJoined) {
copy(
roomJoinState = JoinState.JOINED
)
} else {
// TODO No change...
copy()
}
}
}

// TODO I should not have to do that
fun init(roomId: String) = withState {
setState {
copy(
roomId = roomId
)
}
}

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,29 @@
/*
* 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

View File

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/roomPreviewNoPreviewRoot"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -64,15 +66,31 @@
android:textAppearance="@style/TextAppearance.Vector.Subtitle2" android:textAppearance="@style/TextAppearance.Vector.Subtitle2"
android:textSize="14sp" /> android:textSize="14sp" />


<Button <TextView
android:id="@+id/roomPreviewNoPreviewJoin" android:id="@+id/roomPreviewNoPreviewError"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin" android:layout_marginTop="@dimen/layout_vertical_margin"
android:gravity="center"
android:textColor="@color/vector_error_color"
android:textSize="15sp"
android:visibility="gone"
tools:text="Error"
tools:visibility="visible" />

<im.vector.riotredesign.core.platform.ButtonStateView
android:id="@+id/roomPreviewNoPreviewJoin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="@dimen/layout_vertical_margin" android:layout_marginBottom="@dimen/layout_vertical_margin"
android:minWidth="120dp" android:minWidth="120dp"
android:text="@string/join" /> app:bsv_button_text="@string/join"
app:bsv_loaded_image_src="@drawable/ic_tick"
app:bsv_use_flat_button="false"
app:layout_constraintBottom_toTopOf="@+id/itemPublicRoomBottomSeparator" />


</LinearLayout> </LinearLayout>



View File

@ -65,6 +65,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginRight="8dp" android:layout_marginRight="8dp"
app:bsv_use_flat_button="true"
app:bsv_button_text="@string/join" app:bsv_button_text="@string/join"
app:bsv_loaded_image_src="@drawable/ic_tick" app:bsv_loaded_image_src="@drawable/ic_tick"
app:layout_constraintBottom_toTopOf="@+id/itemPublicRoomBottomSeparator" app:layout_constraintBottom_toTopOf="@+id/itemPublicRoomBottomSeparator"

View File

@ -7,7 +7,7 @@
tools:parentTag="android.widget.FrameLayout"> tools:parentTag="android.widget.FrameLayout">


<Button <Button
android:id="@+id/buttonStateButton" android:id="@+id/buttonStateButtonFlat"
style="@style/VectorButtonStyleFlat" style="@style/VectorButtonStyleFlat"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -17,6 +17,18 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@string/join" /> tools:text="@string/join" />


<Button
android:id="@+id/buttonStateButtonBig"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minWidth="120dp"
app:layout_constraintBottom_toTopOf="@+id/itemPublicRoomBottomSeparator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/join" />

<ProgressBar <ProgressBar
android:id="@+id/buttonStateLoading" android:id="@+id/buttonStateLoading"
android:layout_width="32dp" android:layout_width="32dp"

View File

@ -3,6 +3,7 @@


<declare-styleable name="ButtonStateView"> <declare-styleable name="ButtonStateView">


<attr name="bsv_use_flat_button" format="boolean" />
<attr name="bsv_loaded_image_src" format="reference" /> <attr name="bsv_loaded_image_src" format="reference" />
<attr name="bsv_button_text" format="reference|string" /> <attr name="bsv_button_text" format="reference|string" />