Merge pull request #173 from vector-im/feature/create_room

Create Room screen
This commit is contained in:
Benoit Marty 2019-06-10 17:01:44 +02:00 committed by GitHub
commit 3439a9ca27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 898 additions and 86 deletions

View File

@ -100,7 +100,7 @@ class CreateRoomParams {
* private_chat => join_rules is set to invite. history_visibility is set to shared.
* trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
* room creator.
* public_chat: => join_rules is set to public. history_visibility is set to shared. One of: ["private_chat", "public_chat", "trusted_private_chat"]
* public_chat: => join_rules is set to public. history_visibility is set to shared.
*/
var preset: CreateRoomPreset? = null


View File

@ -18,12 +18,7 @@ package im.vector.matrix.android.internal.database

import android.os.Handler
import android.os.HandlerThread
import io.realm.Realm
import io.realm.RealmChangeListener
import io.realm.RealmConfiguration
import io.realm.RealmObject
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.*
import java.util.concurrent.CountDownLatch

private const val THREAD_NAME = "REALM_QUERY_LATCH"
@ -39,6 +34,7 @@ class RealmQueryLatch<E : RealmObject>(private val realmConfiguration: RealmConf
val runnable = Runnable {
val realm = Realm.getInstance(realmConfiguration)
val result = realmQueryBuilder(realm).findAllAsync()

result.addChangeListener(object : RealmChangeListener<RealmResults<E>> {
override fun onChange(t: RealmResults<E>) {
if (t.isNotEmpty()) {

View File

@ -16,23 +16,16 @@

package im.vector.matrix.android.internal.session.room.create

import android.os.Handler
import android.os.HandlerThread
import arrow.core.Try
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
import im.vector.matrix.android.internal.database.RealmQueryLatch
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomEntityFields
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import io.realm.Realm
import io.realm.RealmChangeListener
import io.realm.RealmConfiguration
import io.realm.RealmResults
import java.util.concurrent.CountDownLatch

private const val THREAD_NAME = "CREATE_ROOM_"

internal interface CreateRoomTask : Task<CreateRoomParams, String>

@ -47,41 +40,15 @@ internal class DefaultCreateRoomTask(private val roomAPI: RoomAPI,
}.flatMap { createRoomResponse ->
val roomId = createRoomResponse.roomId!!

val latch = CountDownLatch(1)

// TODO Maybe do the same code for join room request ?
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
val handlerThread = HandlerThread(THREAD_NAME + hashCode())
handlerThread.start()
val handler = Handler(handlerThread.looper)

handler.post {
val realm = Realm.getInstance(realmConfiguration)

if (realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId)
.findAll()
.isEmpty()) {
val result = realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId)
.findAllAsync()

result.addChangeListener(object : RealmChangeListener<RealmResults<RoomEntity>> {
override fun onChange(t: RealmResults<RoomEntity>) {
if (t.isNotEmpty()) {
result.removeChangeListener(this)
realm.close()
latch.countDown()
}
}
})
} else {
realm.close()
latch.countDown()
}
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId)
}

latch.await()
handlerThread.quit()
rql.await()

return Try.just(roomId)
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.mvrx

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import im.vector.riotredesign.core.utils.LiveEvent

abstract class NavigationViewModel<NavigationClass> : ViewModel() {

private val _navigateTo = MutableLiveData<LiveEvent<NavigationClass>>()
val navigateTo: LiveData<LiveEvent<NavigationClass>>
get() = _navigateTo


fun goTo(navigation: NavigationClass) {
_navigateTo.postValue(LiveEvent(navigation))
}
}

View File

@ -0,0 +1,77 @@
/*
* 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.form

import android.text.Editable
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.platform.SimpleTextWatcher

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

@EpoxyAttribute
var hint: String? = null

@EpoxyAttribute
var value: String? = null

@EpoxyAttribute
var enabled: Boolean = true

@EpoxyAttribute
var onTextChange: ((String) -> Unit)? = null

private val onTextChangeListener = object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) {
onTextChange?.invoke(s.toString())
}
}

override fun bind(holder: Holder) {
holder.textInputLayout.isEnabled = enabled
holder.textInputLayout.hint = hint

// Update only if text is different
if (holder.textInputEditText.text.toString() != value) {
holder.textInputEditText.setText(value)
}
holder.textInputEditText.isEnabled = enabled

holder.textInputEditText.addTextChangedListener(onTextChangeListener)
}

override fun shouldSaveViewState(): Boolean {
return false
}

override fun unbind(holder: Holder) {
super.unbind(holder)
holder.textInputEditText.removeTextChangedListener(onTextChangeListener)
}

class Holder : VectorEpoxyHolder() {
val textInputLayout by bind<TextInputLayout>(R.id.formTextInputTextInputLayout)
val textInputEditText by bind<TextInputEditText>(R.id.formTextInputTextInputEditText)
}
}

View File

@ -0,0 +1,77 @@
/*
* 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.form

import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.switchmaterial.SwitchMaterial
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.setTextOrHide

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

@EpoxyAttribute
var listener: ((Boolean) -> Unit)? = null

@EpoxyAttribute
var enabled: Boolean = true

@EpoxyAttribute
var switchChecked: Boolean = false

@EpoxyAttribute
var title: String? = null

@EpoxyAttribute
var summary: String? = null

override fun bind(holder: Holder) {
holder.titleView.text = title
holder.summaryView.setTextOrHide(summary)

holder.switchView.isEnabled = enabled

holder.switchView.isChecked = switchChecked

holder.switchView.setOnCheckedChangeListener { _, isChecked ->
listener?.invoke(isChecked)
}
}

override fun shouldSaveViewState(): Boolean {
return false
}

override fun unbind(holder: Holder) {
super.unbind(holder)

holder.switchView.setOnCheckedChangeListener(null)
}


class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.formSwitchTitle)
val summaryView by bind<TextView>(R.id.formSwitchSummary)
val switchView by bind<SwitchMaterial>(R.id.formSwitchSwitch)
}

}

View File

@ -27,10 +27,12 @@ import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.viewModel
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.hideKeyboard
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.core.platform.OnBackPressed
import im.vector.riotredesign.core.platform.ToolbarConfigurable
@ -46,7 +48,13 @@ import org.koin.android.scope.ext.android.getOrCreateScope

class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {

// Supported navigation actions for this Activity
sealed class Navigation {
object OpenDrawer : Navigation()
}

private val homeActivityViewModel: HomeActivityViewModel by viewModel()
private lateinit var navigationViewModel: HomeNavigationViewModel
private val homeNavigator by inject<HomeNavigator>()

private var progress: ProgressDialog? = null
@ -63,6 +71,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
super.onCreate(savedInstanceState)
bindScope(getOrCreateScope(HomeModule.HOME_SCOPE))
homeNavigator.activity = this

navigationViewModel = ViewModelProviders.of(this).get(HomeNavigationViewModel::class.java)

drawerLayout.addDrawerListener(drawerListener)
if (isFirstCreation()) {
val homeDrawerFragment = HomeDrawerFragment.newInstance()
@ -82,6 +93,12 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
progress?.dismiss()
}
})

navigationViewModel.navigateTo.observeEvent(this) { navigation ->
when (navigation) {
is Navigation.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
}
}
}

override fun onDestroy() {
@ -113,10 +130,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {

override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
drawerLayout.openDrawer(GravityCompat.START)
return true
}
R.id.sliding_menu_sign_out -> {
SignOutUiWorker(this).perform(Matrix.getInstance().currentSession!!)
return true

View File

@ -20,6 +20,7 @@ import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import androidx.core.view.forEachIndexed
import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -56,6 +57,7 @@ class HomeDetailFragment : VectorBaseFragment() {
private lateinit var currentDisplayMode: RoomListFragment.DisplayMode

private val viewModel: HomeDetailViewModel by fragmentViewModel()
private lateinit var navigationViewModel: HomeNavigationViewModel

override fun getLayoutResId(): Int {
return R.layout.fragment_home_detail
@ -65,6 +67,9 @@ class HomeDetailFragment : VectorBaseFragment() {
super.onActivityCreated(savedInstanceState)
currentDisplayMode = savedInstanceState?.getSerializable(CURRENT_DISPLAY_MODE) as? RoomListFragment.DisplayMode
?: RoomListFragment.DisplayMode.HOME

navigationViewModel = ViewModelProviders.of(requireActivity()).get(HomeNavigationViewModel::class.java)

switchDisplayMode(currentDisplayMode)
setupBottomNavigationView()
setupToolbar()
@ -89,7 +94,7 @@ class HomeDetailFragment : VectorBaseFragment() {
groupToolbarAvatarImageView
)
groupToolbarAvatarImageView.setOnClickListener {
vectorBaseActivity.notImplemented("Group click in toolbar")
navigationViewModel.goTo(HomeActivity.Navigation.OpenDrawer)
}
}


View File

@ -0,0 +1,21 @@
/*
* 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.riotredesign.core.mvrx.NavigationViewModel

class HomeNavigationViewModel : NavigationViewModel<HomeActivity.Navigation>()

View File

@ -16,10 +16,8 @@

package im.vector.riotredesign.features.navigation

import android.app.Activity
import android.content.Context
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.debug.DebugMenuActivity
import im.vector.riotredesign.features.home.room.detail.RoomDetailActivity

View File

@ -20,6 +20,7 @@ import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.activityViewModel
@ -29,9 +30,7 @@ import com.jakewharton.rxbinding2.widget.RxTextView
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotredesign.R
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.roomdirectory.picker.RoomDirectoryPickerFragment
import im.vector.riotredesign.features.themes.ThemeUtils
import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_public_rooms.*
@ -49,6 +48,7 @@ import java.util.concurrent.TimeUnit
class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback {

private val viewModel: RoomDirectoryViewModel by activityViewModel()
private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel
private val publicRoomsController: PublicRoomsController by inject()
private val errorFormatter: ErrorFormatter by inject()

@ -76,9 +76,7 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
.disposeOnDestroy()

publicRoomsCreateNewRoom.setOnClickListener {
// TODO homeActivityViewModel.createRoom()

vectorBaseActivity.notImplemented()
navigationViewModel.goTo(RoomDirectoryActivity.Navigation.CreateRoom)
}

viewModel.joinRoomErrorLiveData.observe(this, Observer {
@ -92,7 +90,7 @@ 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)
navigationViewModel.goTo(RoomDirectoryActivity.Navigation.ChangeProtocol)
true
}
else ->
@ -104,6 +102,8 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))

navigationViewModel = ViewModelProviders.of(requireActivity()).get(RoomDirectoryNavigationViewModel::class.java)

setupRecyclerView()
}


View File

@ -17,14 +17,29 @@
package im.vector.riotredesign.features.roomdirectory

import android.os.Bundle
import androidx.lifecycle.ViewModelProviders
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.addFragment
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.features.roomdirectory.createroom.CreateRoomFragment
import im.vector.riotredesign.features.roomdirectory.picker.RoomDirectoryPickerFragment
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope

class RoomDirectoryActivity : VectorBaseActivity() {

// Supported navigation actions for this Activity
sealed class Navigation {
object Back : Navigation()
object CreateRoom : Navigation()
object Close : Navigation()
object ChangeProtocol : Navigation()
}


private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel

override fun getLayoutRes() = R.layout.activity_simple

@ -32,6 +47,17 @@ class RoomDirectoryActivity : VectorBaseActivity() {
super.onCreate(savedInstanceState)

bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))

navigationViewModel = ViewModelProviders.of(this).get(RoomDirectoryNavigationViewModel::class.java)

navigationViewModel.navigateTo.observeEvent(this) { navigation ->
when (navigation) {
is Navigation.Back -> onBackPressed()
is Navigation.CreateRoom -> addFragmentToBackstack(CreateRoomFragment(), R.id.simpleFragmentContainer)
is Navigation.ChangeProtocol -> addFragmentToBackstack(RoomDirectoryPickerFragment(), R.id.simpleFragmentContainer)
is Navigation.Close -> finish()
}
}
}

override fun initUiAndData() {

View File

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

import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.features.roomdirectory.createroom.CreateRoomController
import im.vector.riotredesign.features.roomdirectory.picker.RoomDirectoryListCreator
import im.vector.riotredesign.features.roomdirectory.picker.RoomDirectoryPickerController
import org.koin.dsl.module.module

// TODO Ganfra: When do we create a new module?
class RoomDirectoryModule {

companion object {
@ -41,5 +41,14 @@ class RoomDirectoryModule {
scope(ROOM_DIRECTORY_SCOPE) {
PublicRoomsController(get(), get())
}

/* ==========================================================================================
* Create room
* ========================================================================================== */

scope(ROOM_DIRECTORY_SCOPE) {
CreateRoomController(get(), get())
}

}
}

View File

@ -0,0 +1,21 @@
/*
* 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

import im.vector.riotredesign.core.mvrx.NavigationViewModel

class RoomDirectoryNavigationViewModel : NavigationViewModel<RoomDirectoryActivity.Navigation>()

View File

@ -0,0 +1,112 @@
/*
* 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.createroom

import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.errorWithRetryItem
import im.vector.riotredesign.core.epoxy.loadingItem
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.form.formEditTextItem
import im.vector.riotredesign.features.form.formSwitchItem

class CreateRoomController(private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter
) : TypedEpoxyController<CreateRoomViewState>() {

var listener: Listener? = null

var index = 0

override fun buildModels(viewState: CreateRoomViewState) {
val asyncCreateRoom = viewState.asyncCreateRoomRequest

when (asyncCreateRoom) {
is Success -> {
// Nothing to display, the screen will be closed
}
is Loading -> {
// display the form
buildForm(viewState, false)
loadingItem {
id("loading")
}
}
is Uninitialized -> {
// display the form
buildForm(viewState, true)
}
is Fail -> {
// display the form
buildForm(viewState, true)
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(asyncCreateRoom.error))
listener { listener?.retry() }
}
}
}
}

private fun buildForm(viewState: CreateRoomViewState, enableFormElement: Boolean) {
formEditTextItem {
id("name")
enabled(enableFormElement)
value(viewState.roomName)
hint(stringProvider.getString(R.string.create_room_name_hint))

onTextChange { text ->
listener?.onNameChange(text)
}
}
formSwitchItem {
id("public")
enabled(enableFormElement)
title(stringProvider.getString(R.string.create_room_public_title))
summary(stringProvider.getString(R.string.create_room_public_description))
switchChecked(viewState.isPublic)

listener { value ->
listener?.setIsPublic(value)
}
}
formSwitchItem {
id("directory")
enabled(enableFormElement)
title(stringProvider.getString(R.string.create_room_directory_title))
summary(stringProvider.getString(R.string.create_room_directory_description))
switchChecked(viewState.isInRoomDirectory)

listener { value ->
listener?.setIsInRoomDirectory(value)
}
}
}

interface Listener {
fun onNameChange(newName: String)
fun setIsPublic(isPublic: Boolean)
fun setIsInRoomDirectory(isInRoomDirectory: Boolean)
fun retry()
}

}

View File

@ -0,0 +1,113 @@
/*
* 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.createroom

import android.os.Bundle
import android.view.MenuItem
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryNavigationViewModel
import kotlinx.android.synthetic.main.fragment_create_room.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
import timber.log.Timber

class CreateRoomFragment : VectorBaseFragment(), CreateRoomController.Listener {

private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel
private val viewModel: CreateRoomViewModel by fragmentViewModel()
private val createRoomController: CreateRoomController by inject()

override fun getLayoutResId() = R.layout.fragment_create_room

override fun getMenuRes() = R.menu.vector_room_creation

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

vectorBaseActivity.setSupportActionBar(createRoomToolbar)

navigationViewModel = ViewModelProviders.of(requireActivity()).get(RoomDirectoryNavigationViewModel::class.java)

setupRecyclerView()

createRoomClose.setOnClickListener {
navigationViewModel.goTo(RoomDirectoryActivity.Navigation.Back)
}
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_create_room -> {
viewModel.doCreateRoom()
true
}
else ->
super.onOptionsItemSelected(item)
}
}

private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)

createRoomForm.layoutManager = layoutManager
createRoomController.listener = this

createRoomForm.setController(createRoomController)
}

override fun onNameChange(newName: String) {
viewModel.setName(newName)
}

override fun setIsPublic(isPublic: Boolean) {
viewModel.setIsPublic(isPublic)
}

override fun setIsInRoomDirectory(isInRoomDirectory: Boolean) {
viewModel.setIsInRoomDirectory(isInRoomDirectory)
}

override fun retry() {
Timber.v("Retry")
viewModel.doCreateRoom()
}

override fun invalidate() = withState(viewModel) { state ->
val async = state.asyncCreateRoomRequest
if (async is Success) {
// Navigate to freshly created room
navigator.openRoom(async(), requireActivity())

navigationViewModel.goTo(RoomDirectoryActivity.Navigation.Close)
} else {
// Populate list with Epoxy
createRoomController.setData(state)
}
}


}

View File

@ -0,0 +1,81 @@
/*
* 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.createroom

import com.airbnb.mvrx.*
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
import im.vector.riotredesign.core.platform.VectorViewModel
import org.koin.android.ext.android.get

class CreateRoomViewModel(initialState: CreateRoomViewState,
private val session: Session) : VectorViewModel<CreateRoomViewState>(initialState) {

companion object : MvRxViewModelFactory<CreateRoomViewModel, CreateRoomViewState> {

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

return CreateRoomViewModel(state, currentSession)
}
}

fun setName(newName: String) = setState { copy(roomName = newName) }

fun setIsPublic(isPublic: Boolean) = setState { copy(isPublic = isPublic) }

fun setIsInRoomDirectory(isInRoomDirectory: Boolean) = setState { copy(isInRoomDirectory = isInRoomDirectory) }

fun doCreateRoom() = withState { state ->
if (state.asyncCreateRoomRequest is Loading || state.asyncCreateRoomRequest is Success) {
return@withState
}

setState {
copy(asyncCreateRoomRequest = Loading())
}

val createRoomParams = CreateRoomParams().apply {
name = state.roomName.takeIf { it.isNotBlank() }

// Directory visibility
visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE

// Public room
preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT
}

session.createRoom(createRoomParams, object : MatrixCallback<String> {
override fun onSuccess(data: String) {
setState {
copy(asyncCreateRoomRequest = Success(data))
}
}

override fun onFailure(failure: Throwable) {
setState {
copy(asyncCreateRoomRequest = Fail(failure))
}
}
})
}

}

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.createroom

import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized

data class CreateRoomViewState(
val roomName: String = "",
val isPublic: Boolean = false,
val isInRoomDirectory: Boolean = false,
val asyncCreateRoomRequest: Async<String> = Uninitialized
) : MvRxState

View File

@ -19,6 +19,7 @@ package im.vector.riotredesign.features.roomdirectory.picker
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
@ -26,7 +27,9 @@ import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryNavigationViewModel
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryViewModel
import kotlinx.android.synthetic.main.fragment_room_directory_picker.*
import org.koin.android.ext.android.inject
@ -39,6 +42,7 @@ import timber.log.Timber
class RoomDirectoryPickerFragment : VectorBaseFragment(), RoomDirectoryPickerController.Callback {

private val viewModel: RoomDirectoryViewModel by activityViewModel()
private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel
private val pickerViewModel: RoomDirectoryPickerViewModel by fragmentViewModel()
private val roomDirectoryPickerController: RoomDirectoryPickerController by inject()

@ -71,6 +75,8 @@ class RoomDirectoryPickerFragment : VectorBaseFragment(), RoomDirectoryPickerCon
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))

navigationViewModel = ViewModelProviders.of(requireActivity()).get(RoomDirectoryNavigationViewModel::class.java)

setupRecyclerView()
}

@ -88,8 +94,7 @@ class RoomDirectoryPickerFragment : VectorBaseFragment(), RoomDirectoryPickerCon
Timber.v("onRoomDirectoryClicked: $roomDirectoryData")
viewModel.setRoomDirectoryData(roomDirectoryData)

// TODO Not the best way to manage Fragment Backstack...
vectorBaseActivity.onBackPressed()
navigationViewModel.goTo(RoomDirectoryActivity.Navigation.Back)
}

override fun retry() {

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M16,2L2,16M2,2l14,14"
android:strokeLineJoin="round"
android:strokeWidth="2.333"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#03B381"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,71 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">


<androidx.appcompat.widget.Toolbar
android:id="@+id/createRoomToolbar"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
android:elevation="4dp"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/createRoomClose"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:scaleType="center"
android:src="@drawable/ic_x_18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/createRoomTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/create_room_title"
android:textColor="?riotx_text_primary"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/createRoomClose"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.appcompat.widget.Toolbar>

<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/createRoomForm"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/createRoomToolbar"
tools:listitem="@layout/item_form_switch" />

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -5,22 +5,33 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/publicRoomsCoordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_header_panel_background">
android:layout_height="match_parent">

<androidx.constraintlayout.widget.ConstraintLayout
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/publicRoomsList"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="?riotx_header_panel_background"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_public_room" />

<com.google.android.material.appbar.AppBarLayout
style="@style/VectorAppBarLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp">

<androidx.appcompat.widget.Toolbar
android:id="@+id/publicRoomsToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
android:minHeight="0dp"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways">

<!-- Note: Background is modified in the code for other themes -->
<EditText
@ -52,6 +63,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:minHeight="@dimen/layout_touch_size"
android:text="@string/create_new_room"
@ -61,16 +73,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/publicRoomsToolbar" />

<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/publicRoomsList"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/publicRoomsCreateNewRoom"
tools:listitem="@layout/item_public_room" />

</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -5,6 +5,7 @@
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:padding="16dp">

<TextView

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:minHeight="@dimen/item_form_min_height">

<TextView
android:id="@+id/formSwitchTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginLeft="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:duplicateParentState="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/formSwitchSummary"
app:layout_constraintEnd_toStartOf="@+id/formSwitchSwitch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/create_room_public_title" />

<TextView
android:id="@+id/formSwitchSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@+id/formSwitchDivider"
app:layout_constraintStart_toStartOf="@+id/formSwitchTitle"
app:layout_constraintTop_toBottomOf="@+id/formSwitchTitle"
tools:text="@string/create_room_public_description" />

<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/formSwitchSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="19dp"
android:layout_marginRight="19dp"
app:layout_constraintBottom_toTopOf="@+id/formSwitchDivider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<View
android:id="@+id/formSwitchDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?riotx_header_panel_border_mobile"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:minHeight="@dimen/item_form_min_height">

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/formTextInputTextInputLayout"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:layout_constraintBottom_toTopOf="@+id/formTextInputDivider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/formTextInputTextInputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:hint="@string/create_room_name_hint" />

</com.google.android.material.textfield.TextInputLayout>

<View
android:id="@+id/formTextInputDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?riotx_header_panel_border_mobile"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,6 +3,7 @@
android:id="@+id/itemNoResultText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:gravity="center"
android:padding="16dp"
android:text="@string/no_result_placeholder" />

View File

@ -9,6 +9,7 @@
android:background="?riotx_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="97dp">

<ImageView

View File

@ -7,7 +7,8 @@
android:layout_height="wrap_content"
android:background="?riotx_background"
android:clickable="true"
android:focusable="true">
android:focusable="true"
android:foreground="?attr/selectableItemBackground">

<ImageView
android:id="@+id/roomAvatarImageView"

View File

@ -13,6 +13,7 @@
android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="@dimen/layout_vertical_margin"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:foreground="?attr/selectableItemBackground"
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">


View File

@ -6,6 +6,7 @@
android:id="@+id/itemRoomDirectoryLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">

View File

@ -2,13 +2,11 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.matrix.vector.activity.VectorRoomCreationActivity">
tools:context=".features.roomdirectory.RoomDirectoryActivity">

<item
android:id="@+id/action_create_room"
android:icon="@drawable/ic_material_done_white"
android:orderInCategory="0"
android:title="@string/room_participants_create"
android:title="@string/create_room_action_create"
app:showAsAction="always" />

</menu>

View File

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

<style name="AppTheme.Status" parent="AppTheme.Base.Status">
<style name="AppTheme.Status.v21" parent="AppTheme.Base.Status">
<item name="android:statusBarColor">@color/riotx_header_panel_background_light</item>
<item name="android:navigationBarColor">@color/riotx_header_panel_background_light</item>

<!-- enable window content transitions -->
<item name="android:windowContentTransitions">true</item>
</style>

<style name="AppTheme.Status" parent="AppTheme.Status.v21" />

</resources>

View File

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

<style name="AppTheme.Status.v23" parent="AppTheme.Status.v21">
<item name="android:windowLightStatusBar">false</item>
</style>

<style name="AppTheme.Status" parent="AppTheme.Status.v23"/>

</resources>

View File

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

<style name="AppTheme.Status.v27" parent="AppTheme.Status.v23">
<item name="android:windowLightNavigationBar">true</item>
</style>

<style name="AppTheme.Status" parent="AppTheme.Status.v27" />

</resources>

View File

@ -27,4 +27,7 @@
<dimen name="pill_min_height">20dp</dimen>
<dimen name="pill_text_padding">4dp</dimen>


<dimen name="item_form_min_height">76dp</dimen>

</resources>

View File

@ -42,4 +42,13 @@
<string name="fab_menu_create_room">"Rooms"</string>
<string name="fab_menu_create_chat">"Direct Messages"</string>

<!-- Create room screen -->
<string name="create_room_title">"New Room"</string>
<string name="create_room_action_create">"CREATE"</string>
<string name="create_room_name_hint">"Room name"</string>
<string name="create_room_public_title">"Public"</string>
<string name="create_room_public_description">"Anyone will be able to join this room"</string>
<string name="create_room_directory_title">"Room Directory"</string>
<string name="create_room_directory_description">"Publish this room in the room directory"</string>

</resources>