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

Create Room
This commit is contained in:
Benoit Marty 2019-04-08 14:16:47 +02:00 committed by GitHub
commit bb65dc5247
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 538 additions and 11 deletions

View File

@ -54,7 +54,7 @@ inline fun <reified T> T?.toContent(): Content? {
@JsonClass(generateAdapter = true)
data class Event(
@Json(name = "type") val type: String,
@Json(name = "event_id") val eventId: String?,
@Json(name = "event_id") val eventId: String? = null,
@Json(name = "content") val content: Content? = null,
@Json(name = "prev_content") val prevContent: Content? = null,
@Json(name = "origin_server_ts") val originServerTs: Long? = null,

View File

@ -17,13 +17,21 @@
package im.vector.matrix.android.api.session.room

import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams

/**
* This interface defines methods to get rooms. It's implemented at the session level.
*/
interface RoomService {

/**
* Create a room
*/
fun createRoom(createRoomParams: CreateRoomParams,
callback: MatrixCallback<String>)

/**
* Get a room from a roomId
* @param roomId the roomId to look for.

View File

@ -0,0 +1,253 @@
/*
* 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.api.session.room.model.create

import android.util.Patterns
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.MatrixPatterns.isUserId
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
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.toContent
import im.vector.matrix.android.api.session.room.model.PowerLevels
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import java.util.*

/**
* Parameter to create a room, with facilities functions to configure it
*/
@JsonClass(generateAdapter = true)
class CreateRoomParams {

/**
* A public visibility indicates that the room will be shown in the published room list.
* A private visibility will hide the room from the published room list.
* Rooms default to private visibility if this key is not included.
* NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
*/
var visibility: RoomDirectoryVisibility? = null

/**
* The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
* The alias will belong on the same homeserver which created the room.
* For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
*/
@Json(name = "room_alias_name")
var roomAliasName: String? = null

/**
* If this is included, an m.room.name event will be sent into the room to indicate the name of the room.
* See Room Events for more information on m.room.name.
*/
var name: String? = null

/**
* If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room.
* See Room Events for more information on m.room.topic.
*/
var topic: String? = null

/**
* A list of user IDs to invite to the room.
* This will tell the server to invite everyone in the list to the newly created room.
*/
@Json(name = "invite")
var invitedUserIds: MutableList<String>? = null

/**
* A list of objects representing third party IDs to invite into the room.
*/
@Json(name = "invite_3pid")
var invite3pids: MutableList<Invite3Pid>? = null

/**
* Extra keys to be added to the content of the m.room.create.
* The server will clobber the following keys: creator.
* Future versions of the specification may allow the server to clobber other keys.
*/
@Json(name = "creation_content")
var creationContent: Any? = null

/**
* A list of state events to set in the new room.
* This allows the user to override the default state events set in the new room.
* The expected format of the state events are an object with type, state_key and content keys set.
* Takes precedence over events set by presets, but gets overriden by name and topic keys.
*/
@Json(name = "initial_state")
var initialStates: MutableList<Event>? = null

/**
* Convenience parameter for setting various default state events based on a preset. Must be either:
* 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"]
*/
var preset: CreateRoomPreset? = null

/**
* This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
* See Direct Messaging for more information.
*/
@Json(name = "is_direct")
var isDirect: Boolean? = null

/**
* The power level content to override in the default power level event
*/
@Json(name = "power_level_content_override")
var powerLevelContentOverride: PowerLevels? = null

/**
* Add the crypto algorithm to the room creation parameters.
*
* @param algorithm the algorithm
*/
fun addCryptoAlgorithm(algorithm: String) {
if (algorithm.isNotBlank()) {
val contentMap = HashMap<String, String>()
contentMap["algorithm"] = algorithm

val algoEvent = Event(type = EventType.ENCRYPTION,
content = contentMap.toContent()
)

if (null == initialStates) {
initialStates = Arrays.asList<Event>(algoEvent)
} else {
initialStates!!.add(algoEvent)
}
}
}

/**
* Force the history visibility in the room creation parameters.
*
* @param historyVisibility the expected history visibility, set null to remove any existing value.
*/
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) {
// Remove the existing value if any.
if (initialStates != null && !initialStates!!.isEmpty()) {
val newInitialStates = ArrayList<Event>()
for (event in initialStates!!) {
if (event.type != EventType.STATE_HISTORY_VISIBILITY) {
newInitialStates.add(event)
}
}
initialStates = newInitialStates
}

if (historyVisibility != null) {
val contentMap = HashMap<String, RoomHistoryVisibility>()
contentMap["history_visibility"] = historyVisibility

val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY,
content = contentMap.toContent())

if (null == initialStates) {
initialStates = Arrays.asList<Event>(historyVisibilityEvent)
} else {
initialStates!!.add(historyVisibilityEvent)
}
}
}

/**
* Mark as a direct message room.
*/
fun setDirectMessage() {
preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
isDirect = true
}

/**
* @return the invite count
*/
private fun getInviteCount(): Int {
return invitedUserIds?.size ?: 0
}

/**
* @return the pid invite count
*/
private fun getInvite3PidCount(): Int {
return invite3pids?.size ?: 0
}

/**
* Tells if the created room can be a direct chat one.
*
* @return true if it is a direct chat
*/
fun isDirect(): Boolean {
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
&& isDirect == true
&& (1 == getInviteCount() || 1 == getInvite3PidCount())
}

/**
* @return the first invited user id
*/
fun getFirstInvitedUserId(): String? {
if (0 != getInviteCount()) {
return invitedUserIds!![0]
}

return if (0 != getInvite3PidCount()) {
invite3pids!![0].address
} else null
}

/**
* Add some ids to the room creation
* ids might be a matrix id or an email address.
*
* @param ids the participant ids to add.
*/
fun addParticipantIds(hsConfig: HomeServerConnectionConfig,
credentials: Credentials,
ids: List<String>) {
for (id in ids) {
if (Patterns.EMAIL_ADDRESS.matcher(id).matches()) {
if (null == invite3pids) {
invite3pids = ArrayList()
}

val pid = Invite3Pid(idServer = hsConfig.identityServerUri.host!!,
medium = ThreePidMedium.EMAIL,
address = id)

invite3pids!!.add(pid)
} else if (isUserId(id)) {
// do not invite oneself
if (credentials.userId != id) {
if (null == invitedUserIds) {
invitedUserIds = ArrayList()
}

invitedUserIds!!.add(id)
}
}
// TODO add phonenumbers when it will be available
}
}
}

View File

@ -0,0 +1,30 @@
/*
* 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.api.session.room.model.create

import com.squareup.moshi.Json

enum class CreateRoomPreset {
@Json(name = "private_chat")
PRESET_PRIVATE_CHAT,

@Json(name = "public_chat")
PRESET_PUBLIC_CHAT,

@Json(name = "trusted_private_chat")
PRESET_TRUSTED_PRIVATE_CHAT
}

View File

@ -0,0 +1,25 @@
/*
* 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.api.session.room.model.create

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
internal data class CreateRoomResponse(
@Json(name = "room_id") var roomId: String? = null
)

View File

@ -0,0 +1,42 @@
/*
* 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.api.session.room.model.create

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class Invite3Pid(
/**
* Required.
* The hostname+port of the identity server which should be used for third party identifier lookups.
*/
@Json(name = "id_server")
val idServer: String,

/**
* Required.
* The kind of address being passed in the address field, for example email.
*/
val medium: String,

/**
* Required.
* The invitee's third party identifier.
*/
val address: String
)

View File

@ -30,10 +30,10 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService
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.signout.SignOutService
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.util.Cancelable
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.di.MatrixKoinHolder
@ -131,6 +131,11 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi

// ROOM SERVICE

override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>) {
assert(isOpen)
return roomService.createRoom(createRoomParams, callback)
}

override fun getRoom(roomId: String): Room? {
assert(isOpen)
return roomService.getRoom(roomId)

View File

@ -97,7 +97,7 @@ internal class SessionModule(private val sessionParams: SessionParams) {
}

scope(DefaultSession.SCOPE) {
DefaultRoomService(get(), get()) as RoomService
DefaultRoomService(get(), get(), get(), get()) as RoomService
}

scope(DefaultSession.SCOPE) {

View File

@ -18,18 +18,32 @@ package im.vector.matrix.android.internal.session.room

import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService
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.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchManaged

internal class DefaultRoomService(private val monarchy: Monarchy,
private val roomFactory: RoomFactory) : RoomService {
private val createRoomTask: CreateRoomTask,
private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService {

override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>) {
createRoomTask
.configureWith(createRoomParams)
.dispatchTo(callback)
.executeBy(taskExecutor)
}

override fun getRoom(roomId: String): Room? {
monarchy.fetchManaged { RoomEntity.where(it, roomId).findFirst() } ?: return null

View File

@ -18,21 +18,27 @@ package im.vector.matrix.android.internal.session.room

import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
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.network.NetworkConstants
import im.vector.matrix.android.internal.session.room.members.RoomMembersResponse
import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.*

internal interface RoomAPI {

/**
* Create a room.
* Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-createroom
*
* @param param the creation room parameter
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom")
fun createRoom(@Body param: CreateRoomParams): Call<CreateRoomResponse>

/**
* Get a list of messages starting from a reference.
*

View File

@ -17,6 +17,8 @@
package im.vector.matrix.android.internal.session.room

import im.vector.matrix.android.internal.session.DefaultSession
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
@ -64,5 +66,9 @@ class RoomModule {
RoomFactory(get(), get(), get(), get(), get(), get(), get(), get())
}

scope(DefaultSession.SCOPE) {
DefaultCreateRoomTask(get(), get()) as CreateRoomTask
}

}
}

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


internal class DefaultCreateRoomTask(private val roomAPI: RoomAPI,
private val realmConfiguration: RealmConfiguration) : CreateRoomTask {


override fun execute(params: CreateRoomParams): Try<String> {
return executeRequest<CreateRoomResponse> {
apiCall = roomAPI.createRoom(params)
}.flatMap { createRoomResponse ->
val roomId = createRoomResponse.roomId!!

val latch = CountDownLatch(1)

// 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()
}
}

latch.await()
handlerThread.quit()
return Try.just(roomId)
}
}
}

View File

@ -41,6 +41,7 @@ internal class DefaultSendService(private val roomId: String,
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

// TODO callback is not used
override fun sendTextMessage(text: String, callback: MatrixCallback<Event>): Cancelable {
val event = eventFactory.createTextEvent(roomId, text)


View File

@ -16,6 +16,7 @@

package im.vector.riotredesign.features.home

import android.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
@ -26,6 +27,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import com.airbnb.mvrx.viewModel
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.R
@ -51,15 +53,18 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
private val homeNavigator by inject<HomeNavigator>()

private var progress: ProgressDialog? = null

private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) {
hideKeyboard()
}
}

override fun getLayoutRes() = R.layout.activity_home

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
bindScope(getOrCreateScope(HomeModule.HOME_SCOPE))
homeNavigator.activity = this
drawerLayout.addDrawerListener(drawerListener)
@ -72,6 +77,17 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
homeActivityViewModel.openRoomLiveData.observeEvent(this) {
homeNavigator.openRoomDetail(it, null)
}
homeActivityViewModel.isLoading.observe(this, Observer<Boolean> {
// TODO better UI
if (it) {
progress?.dismiss()
progress = ProgressDialog(this)
progress?.setMessage(getString(R.string.room_recents_create_room))
progress?.show()
} else {
progress?.dismiss()
}
})
}

override fun onDestroy() {
@ -120,6 +136,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
SignOutUiWorker(this).perform(Matrix.getInstance().currentSession!!)
return true
}
// TODO Temporary code here to create a room
R.id.tmp_menu_create_room -> {
homeActivityViewModel.createRoom()
return true
}
}

return true

View File

@ -22,7 +22,9 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent
@ -47,6 +49,10 @@ class HomeActivityViewModel(state: EmptyState,
}
}

private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean>
get() = _isLoading

private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
val openRoomLiveData: LiveData<LiveEvent<String>>
get() = _openRoomLiveData
@ -73,5 +79,22 @@ class HomeActivityViewModel(state: EmptyState,
.disposeOnClear()
}

fun createRoom(createRoomParams: CreateRoomParams = CreateRoomParams()) {
_isLoading.value = true

session.createRoom(createRoomParams, object : MatrixCallback<String> {
override fun onSuccess(data: String) {
_isLoading.value = false
// Open room id
_openRoomLiveData.postValue(LiveEvent(data))
}

override fun onFailure(failure: Throwable) {
_isLoading.value = false
super.onFailure(failure)
}
})
}


}

View File

@ -11,4 +11,9 @@
android:icon="@drawable/ic_material_exit_to_app"
android:title="@string/action_sign_out" />

<!-- TODO Temporary code -->
<item
android:id="@+id/tmp_menu_create_room"
android:title="@string/room_recents_create_room" />

</menu>