Merge pull request #2526 from vector-im/feature/bca/fix_mxto

element:// support + basic peeking + fix join via server
This commit is contained in:
Benoit Marty 2020-12-11 21:44:06 +01:00 committed by GitHub
commit 163c05d5cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 532 additions and 55 deletions

View File

@ -11,11 +11,13 @@ Features ✨:
Improvements 🙌: Improvements 🙌:
- Add Setting Item to Change PIN (#2462) - Add Setting Item to Change PIN (#2462)
- Improve room history visibility setting UX (#1579) - Improve room history visibility setting UX (#1579)
- Matrix.to deeplink custom scheme support
Bugfix 🐛: Bugfix 🐛:
- Fix cancellation of sending event (#2438) - Fix cancellation of sending event (#2438)
- Double bottomsheet effect after verify with passphrase - Double bottomsheet effect after verify with passphrase
- EditText cursor jumps to the start while typing fast (#2469) - EditText cursor jumps to the start while typing fast (#2469)
- No known servers error is given when joining rooms on new Gitter bridge (#2516)
- Show preview when sending attachment from the keyboard (#2440) - Show preview when sending attachment from the keyboard (#2440)
- Do not compress GIFs (#1616, #1254) - Do not compress GIFs (#1616, #1254)

View File

@ -47,6 +47,7 @@ import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
class RxSession(private val session: Session) { class RxSession(private val session: Session) {
@ -139,7 +140,7 @@ class RxSession(private val session: Session) {
} }
fun getRoomIdByAlias(roomAlias: String, fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean): Single<Optional<String>> = singleBuilder { searchOnServer: Boolean): Single<Optional<RoomAliasDescription>> = singleBuilder {
session.getRoomIdByAlias(roomAlias, searchOnServer, it) session.getRoomIdByAlias(roomAlias, searchOnServer, it)
} }

View File

@ -25,6 +25,7 @@ interface PermalinkService {
companion object { companion object {
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/" const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://"
} }
/** /**

View File

@ -18,12 +18,15 @@ package org.matrix.android.sdk.api.session.room
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
/** /**
* This interface defines methods to get rooms. It's implemented at the session level. * This interface defines methods to get rooms. It's implemented at the session level.
@ -120,7 +123,7 @@ interface RoomService {
*/ */
fun getRoomIdByAlias(roomAlias: String, fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean, searchOnServer: Boolean,
callback: MatrixCallback<Optional<String>>): Cancelable callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable
/** /**
* Delete a room alias * Delete a room alias
@ -163,4 +166,16 @@ interface RoomService {
* @return a LiveData of the optional found room member * @return a LiveData of the optional found room member
*/ */
fun getRoomMemberLive(userId: String, roomId: String): LiveData<Optional<RoomMemberSummary>> fun getRoomMemberLive(userId: String, roomId: String): LiveData<Optional<RoomMemberSummary>>
/**
* Get some state events about a room
*/
fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>)
/**
* Use this if you want to get information from a room that you are not yet in (or invited)
* It might be possible to get some information on this room if it is public or if guest access is allowed
* This call will try to gather some information on this room, but it could fail and get nothing more
*/
fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>)
} }

View File

@ -0,0 +1,37 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.room.peeking
sealed class PeekResult {
data class Success(
val roomId: String,
val alias: String?,
val name: String?,
val topic: String?,
val avatarUrl: String?,
val numJoinedMembers: Int?,
val viaServers: List<String>
) : PeekResult()
data class PeekingNotAllowed(
val roomId: String,
val alias: String?,
val viaServers: List<String>
) : PeekResult()
object UnknownAlias : PeekResult()
}

View File

@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.RoomService
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
@ -27,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
@ -35,10 +37,13 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
@ -55,6 +60,8 @@ internal class DefaultRoomService @Inject constructor(
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomIdByAliasTask: GetRoomIdByAliasTask, private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val deleteRoomAliasTask: DeleteRoomAliasTask, private val deleteRoomAliasTask: DeleteRoomAliasTask,
private val resolveRoomStateTask: ResolveRoomStateTask,
private val peekRoomTask: PeekRoomTask,
private val roomGetter: RoomGetter, private val roomGetter: RoomGetter,
private val roomSummaryDataSource: RoomSummaryDataSource, private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@ -119,7 +126,7 @@ internal class DefaultRoomService @Inject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<String>>): Cancelable { override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable {
return roomIdByAliasTask return roomIdByAliasTask
.configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) { .configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) {
this.callback = callback this.callback = callback
@ -154,4 +161,20 @@ internal class DefaultRoomService @Inject constructor(
results.firstOrNull().toOptional() results.firstOrNull().toOptional()
} }
} }
override fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>) {
resolveRoomStateTask
.configureWith(ResolveRoomStateTask.Params(roomId)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>) {
peekRoomTask
.configureWith(PeekRoomTask.Params(roomIdOrAlias)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
} }

View File

@ -183,7 +183,7 @@ internal interface RoomAPI {
@Body body: ThreePidInviteBody): Call<Unit> @Body body: ThreePidInviteBody): Call<Unit>
/** /**
* Send a generic state events * Send a generic state event
* *
* @param roomId the room id. * @param roomId the room id.
* @param stateEventType the state event type * @param stateEventType the state event type
@ -195,7 +195,7 @@ internal interface RoomAPI {
@Body params: JsonDict): Call<Unit> @Body params: JsonDict): Call<Unit>
/** /**
* Send a generic state events * Send a generic state event
* *
* @param roomId the room id. * @param roomId the room id.
* @param stateEventType the state event type * @param stateEventType the state event type
@ -208,6 +208,13 @@ internal interface RoomAPI {
@Path("state_key") stateKey: String, @Path("state_key") stateKey: String,
@Body params: JsonDict): Call<Unit> @Body params: JsonDict): Call<Unit>
/**
* Get state events of a room
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-state
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state")
fun getRoomState(@Path("roomId") roomId: String) : Call<List<Event>>
/** /**
* Send a relation event to a room. * Send a relation event to a room.
* *

View File

@ -57,6 +57,10 @@ import org.matrix.android.sdk.internal.session.room.membership.leaving.DefaultLe
import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask
import org.matrix.android.sdk.internal.session.room.membership.threepid.DefaultInviteThreePidTask import org.matrix.android.sdk.internal.session.room.membership.threepid.DefaultInviteThreePidTask
import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask
import org.matrix.android.sdk.internal.session.room.peeking.DefaultPeekRoomTask
import org.matrix.android.sdk.internal.session.room.peeking.DefaultResolveRoomStateTask
import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask
import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
@ -223,4 +227,10 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask
@Binds
abstract fun bindResolveRoomStateTask(task: DefaultResolveRoomStateTask): ResolveRoomStateTask
@Binds
abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask
} }

View File

@ -29,7 +29,7 @@ import org.matrix.android.sdk.internal.session.directory.DirectoryAPI
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject import javax.inject.Inject
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> { internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<RoomAliasDescription>> {
data class Params( data class Params(
val roomAlias: String, val roomAlias: String,
val searchOnServer: Boolean val searchOnServer: Boolean
@ -42,21 +42,21 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor(
private val eventBus: EventBus private val eventBus: EventBus
) : GetRoomIdByAliasTask { ) : GetRoomIdByAliasTask {
override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional<String> { override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional<RoomAliasDescription> {
var roomId = Realm.getInstance(monarchy.realmConfiguration).use { val roomId = Realm.getInstance(monarchy.realmConfiguration).use {
RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId
} }
return if (roomId != null) { return if (roomId != null) {
Optional.from(roomId) Optional.from(RoomAliasDescription(roomId))
} else if (!params.searchOnServer) { } else if (!params.searchOnServer) {
Optional.from<String>(null) Optional.from(null)
} else { } else {
roomId = tryOrNull("## Failed to get roomId from alias") { val description = tryOrNull("## Failed to get roomId from alias") {
executeRequest<RoomAliasDescription>(eventBus) { executeRequest<RoomAliasDescription>(eventBus) {
apiCall = directoryAPI.getRoomIdByAlias(params.roomAlias) apiCall = directoryAPI.getRoomIdByAlias(params.roomAlias)
} }
}?.roomId }
Optional.from(roomId) Optional.from(description)
} }
} }
} }

View File

@ -20,7 +20,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class RoomAliasDescription( data class RoomAliasDescription(
/** /**
* The room ID for this alias. * The room ID for this alias.
*/ */

View File

@ -0,0 +1,145 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.peeking
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface PeekRoomTask : Task<PeekRoomTask.Params, PeekResult> {
data class Params(
val roomIdOrAlias: String
)
}
internal class DefaultPeekRoomTask @Inject constructor(
private val getRoomIdByAliasTask: GetRoomIdByAliasTask,
private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask,
private val getPublicRoomTask: GetPublicRoomTask,
private val resolveRoomStateTask: ResolveRoomStateTask
) : PeekRoomTask {
override suspend fun execute(params: PeekRoomTask.Params): PeekResult {
val roomId: String
val serverList: List<String>
val isAlias = MatrixPatterns.isRoomAlias(params.roomIdOrAlias)
if (isAlias) {
// get alias description
val aliasDescription = getRoomIdByAliasTask
.execute(GetRoomIdByAliasTask.Params(params.roomIdOrAlias, true))
.getOrNull()
?: return PeekResult.UnknownAlias
roomId = aliasDescription.roomId
serverList = aliasDescription.servers
} else {
roomId = params.roomIdOrAlias
serverList = emptyList()
}
// Is it a public room?
val publicRepoResult = when (getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId))) {
RoomDirectoryVisibility.PRIVATE -> {
// We cannot resolve this room :/
null
}
RoomDirectoryVisibility.PUBLIC -> {
// Try to find it in directory
val filter = if (isAlias) PublicRoomsFilter(searchTerm = params.roomIdOrAlias.substring(1))
else null
getPublicRoomTask.execute(GetPublicRoomTask.Params(
server = serverList.firstOrNull(),
publicRoomsParams = PublicRoomsParams(
filter = filter,
limit = 20.takeIf { filter != null } ?: 100
)
)).chunk?.firstOrNull { it.roomId == roomId }
}
}
if (publicRepoResult != null) {
return PeekResult.Success(
roomId = roomId,
alias = publicRepoResult.getPrimaryAlias() ?: params.roomIdOrAlias.takeIf { isAlias },
avatarUrl = publicRepoResult.avatarUrl,
name = publicRepoResult.name,
topic = publicRepoResult.topic,
numJoinedMembers = publicRepoResult.numJoinedMembers,
viaServers = serverList
)
}
// mm... try to peek state ? maybe the room is not public but yet allow guest to get events?
// this could be slow
try {
val stateEvents = resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomId))
val name = stateEvents
.lastOrNull { it.type == EventType.STATE_ROOM_NAME && it.stateKey == "" }
?.let { it.content?.toModel<RoomNameContent>()?.name }
val topic = stateEvents
.lastOrNull { it.type == EventType.STATE_ROOM_TOPIC && it.stateKey == "" }
?.let { it.content?.toModel<RoomTopicContent>()?.topic }
val avatarUrl = stateEvents
.lastOrNull { it.type == EventType.STATE_ROOM_AVATAR }
?.let { it.content?.toModel<RoomAvatarContent>()?.avatarUrl }
val alias = stateEvents
.lastOrNull { it.type == EventType.STATE_ROOM_CANONICAL_ALIAS }
?.let { it.content?.toModel<RoomCanonicalAliasContent>()?.canonicalAlias }
// not sure if it's the right way to do that :/
val memberCount = stateEvents
.filter { it.type == EventType.STATE_ROOM_MEMBER && it.stateKey?.isNotEmpty() == true }
.distinctBy { it.stateKey }
.count()
return PeekResult.Success(
roomId = roomId,
alias = alias,
avatarUrl = avatarUrl,
name = name,
topic = topic,
numJoinedMembers = memberCount,
viaServers = serverList
)
} catch (failure: Throwable) {
// Would be M_FORBIDDEN if cannot peek :/
// User XXX not in room !XXX, and room previews are disabled
return PeekResult.PeekingNotAllowed(
roomId = roomId,
alias = params.roomIdOrAlias.takeIf { isAlias },
viaServers = serverList
)
}
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.peeking
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface ResolveRoomStateTask : Task<ResolveRoomStateTask.Params, List<Event>> {
data class Params(
val roomId: String
)
}
internal class DefaultResolveRoomStateTask @Inject constructor(
private val roomAPI: RoomAPI,
private val eventBus: EventBus
) : ResolveRoomStateTask {
override suspend fun execute(params: ResolveRoomStateTask.Params): List<Event> {
return executeRequest(eventBus) {
apiCall = roomAPI.getRoomState(params.roomId)
}
}
}

View File

@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils # android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
enum class===83 enum class===84
### Do not import temporary legacy classes ### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.android.sdk.internal.legacy.riot===3

View File

@ -81,8 +81,9 @@
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity-alias> </activity-alias>
<activity android:name=".features.home.HomeActivity" <activity
android:launchMode="singleTask"/> android:name=".features.home.HomeActivity"
android:launchMode="singleTask" />
<activity <activity
android:name=".features.login.LoginActivity" android:name=".features.login.LoginActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
@ -190,15 +191,25 @@
<activity <activity
android:name=".features.signout.soft.SoftLogoutActivity" android:name=".features.signout.soft.SoftLogoutActivity"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity android:name=".features.permalink.PermalinkHandlerActivity" android:launchMode="singleTask"> <activity
android:name=".features.permalink.PermalinkHandlerActivity"
android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" /> <data android:scheme="http" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="matrix.to" /> <data android:host="matrix.to" />
<data
android:host="user"
android:scheme="element" />
<data
android:host="room"
android:scheme="element" />
</intent-filter> </intent-filter>
</activity> </activity>

View File

@ -162,11 +162,27 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
private fun handleIntent(intent: Intent?) { private fun handleIntent(intent: Intent?) {
intent?.dataString?.let { deepLink -> intent?.dataString?.let { deepLink ->
if (!deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)) return@let val resolvedLink = when {
deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> deepLink
deepLink.startsWith(PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> {
// This is a bit ugly, but for now just convert to matrix.to link for compatibility
when {
deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length)
deepLink.startsWith(ROOM_LINK_PREFIX) -> deepLink.substring(ROOM_LINK_PREFIX.length)
else -> null
}?.let {
activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(it)
}
}
else -> null
}
permalinkHandler.launch(this, deepLink, permalinkHandler.launch(
context = this,
deepLink = resolvedLink,
navigationInterceptor = this, navigationInterceptor = this,
buildTask = true) buildTask = true
)
// .delay(500, TimeUnit.MILLISECONDS) // .delay(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { isHandled -> .subscribe { isHandled ->
@ -345,11 +361,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
bugReporter.openBugReportScreen(this, false) bugReporter.openBugReportScreen(this, false)
return true return true
} }
R.id.menu_home_filter -> { R.id.menu_home_filter -> {
navigator.openRoomsFiltering(this) navigator.openRoomsFiltering(this)
return true return true
} }
R.id.menu_home_setting -> { R.id.menu_home_setting -> {
navigator.openSettings(this) navigator.openSettings(this)
return true return true
} }
@ -390,5 +406,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
putExtra(MvRx.KEY_ARG, args) putExtra(MvRx.KEY_ARG, args)
} }
} }
private const val ROOM_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/"
private const val USER_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/"
} }
} }

View File

@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import javax.inject.Inject import javax.inject.Inject
@ -111,7 +112,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
private fun PermalinkData.RoomLink.getRoomId(): Single<Optional<String>> { private fun PermalinkData.RoomLink.getRoomId(): Single<Optional<String>> {
val session = activeSessionHolder.getSafeActiveSession() val session = activeSessionHolder.getSafeActiveSession()
return if (isRoomAlias && session != null) { return if (isRoomAlias && session != null) {
session.rx().getRoomIdByAlias(roomIdOrAlias, true).subscribeOn(Schedulers.io()) session.rx().getRoomIdByAlias(roomIdOrAlias, true).map { it.getOrNull()?.roomId.toOptional() }.subscribeOn(Schedulers.io())
} else { } else {
Single.just(Optional.from(roomIdOrAlias)) Single.just(Optional.from(roomIdOrAlias))
} }
@ -149,16 +150,28 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
navigator.openRoom(context, roomId, eventId, buildTask) navigator.openRoom(context, roomId, eventId, buildTask)
} }
else -> { else -> {
val roomPreviewData = RoomPreviewData( if (roomSummary == null) {
roomId = roomId, // we don't know this room, try to peek
eventId = eventId, val roomPreviewData = RoomPreviewData(
roomAlias = roomAlias ?: roomSummary?.canonicalAlias, roomId = roomId,
roomName = roomSummary?.displayName, roomAlias = roomAlias,
avatarUrl = roomSummary?.avatarUrl, peekFromServer = true,
buildTask = buildTask, buildTask = buildTask,
homeServers = permalinkData.viaParameters homeServers = permalinkData.viaParameters
) )
navigator.openRoomPreview(context, roomPreviewData) navigator.openRoomPreview(context, roomPreviewData)
} else {
val roomPreviewData = RoomPreviewData(
roomId = roomId,
eventId = eventId,
roomAlias = roomAlias ?: roomSummary.canonicalAlias,
roomName = roomSummary.displayName,
avatarUrl = roomSummary.avatarUrl,
buildTask = buildTask,
homeServers = permalinkData.viaParameters
)
navigator.openRoomPreview(context, roomPreviewData)
}
} }
} }
} }

View File

@ -130,7 +130,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
private fun displayNextIfPossible() { private fun displayNextIfPossible() {
val currentActivity = weakCurrentActivity?.get() val currentActivity = weakCurrentActivity?.get()
if (Alerter.isShowing || currentActivity == null) { if (Alerter.isShowing || currentActivity == null || currentActivity.isDestroyed) {
// will retry later // will retry later
return return
} }

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2020 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.app.features.roomdirectory.roompreview
enum class PeekingState {
FOUND,
NOT_FOUND,
NO_ACCESS
}

View File

@ -40,6 +40,7 @@ data class RoomPreviewData(
val worldReadable: Boolean = false, val worldReadable: Boolean = false,
val avatarUrl: String? = null, val avatarUrl: String? = null,
val homeServers: List<String> = emptyList(), val homeServers: List<String> = emptyList(),
val peekFromServer: Boolean = false,
val buildTask: Boolean = false val buildTask: Boolean = false
) : Parcelable { ) : Parcelable {
val matrixItem: MatrixItem val matrixItem: MatrixItem

View File

@ -20,6 +20,8 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -30,6 +32,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.roomdirectory.JoinState import im.vector.app.features.roomdirectory.JoinState
import kotlinx.android.synthetic.main.fragment_room_preview_no_preview.* import kotlinx.android.synthetic.main.fragment_room_preview_no_preview.*
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -48,22 +51,6 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupToolbar(roomPreviewNoPreviewToolbar) setupToolbar(roomPreviewNoPreviewToolbar)
val titleText = roomPreviewData.roomName ?: roomPreviewData.roomAlias ?: roomPreviewData.roomId
// Toolbar
avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewToolbarAvatar)
roomPreviewNoPreviewToolbarTitle.text = titleText
// Screen
avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewAvatar)
roomPreviewNoPreviewName.text = titleText
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 { roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() { override fun onButtonClicked() {
@ -100,7 +87,62 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
// Quit this screen // Quit this screen
requireActivity().finish() requireActivity().finish()
// Open room // Open room
navigator.openRoom(requireActivity(), roomPreviewData.roomId, roomPreviewData.eventId, roomPreviewData.buildTask) navigator.openRoom(requireActivity(), state.roomId, roomPreviewData.eventId, roomPreviewData.buildTask)
}
val bestName = state.roomName ?: state.roomAlias ?: state.roomId
when (state.peekingState) {
is Loading -> {
roomPreviewPeekingProgress.isVisible = true
roomPreviewNoPreviewJoin.isVisible = false
}
is Success -> {
roomPreviewPeekingProgress.isVisible = false
when (state.peekingState.invoke()) {
PeekingState.FOUND -> {
// show join buttons
roomPreviewNoPreviewJoin.isVisible = true
renderState(bestName, state.matrixItem(), state.roomTopic)
}
PeekingState.NO_ACCESS -> {
roomPreviewNoPreviewJoin.isVisible = true
roomPreviewNoPreviewLabel.isVisible = true
roomPreviewNoPreviewLabel.setText(R.string.room_preview_no_preview_join)
renderState(bestName, state.matrixItem().takeIf { state.roomAlias != null }, state.roomTopic)
}
else -> {
roomPreviewNoPreviewJoin.isVisible = false
roomPreviewNoPreviewLabel.isVisible = true
roomPreviewNoPreviewLabel.setText(R.string.room_preview_not_found)
renderState(bestName, null, state.roomTopic)
}
}
}
else -> {
// Render with initial state, no peeking
roomPreviewPeekingProgress.isVisible = false
roomPreviewNoPreviewJoin.isVisible = true
renderState(bestName, state.matrixItem(), state.roomTopic)
roomPreviewNoPreviewLabel.isVisible = false
}
} }
} }
private fun renderState(roomName: String, matrixItem: MatrixItem?, topic: String?) {
// Toolbar
if (matrixItem != null) {
roomPreviewNoPreviewToolbarAvatar.isVisible = true
roomPreviewNoPreviewAvatar.isVisible = true
avatarRenderer.render(matrixItem, roomPreviewNoPreviewToolbarAvatar)
avatarRenderer.render(matrixItem, roomPreviewNoPreviewAvatar)
} else {
roomPreviewNoPreviewToolbarAvatar.isVisible = false
roomPreviewNoPreviewAvatar.isVisible = false
}
roomPreviewNoPreviewToolbarTitle.text = roomName
// Screen
roomPreviewNoPreviewName.text = roomName
roomPreviewNoPreviewTopic.setTextOrHide(topic)
}
} }

View File

@ -16,8 +16,11 @@
package im.vector.app.features.roomdirectory.roompreview package im.vector.app.features.roomdirectory.roompreview
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
@ -25,12 +28,17 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.roomdirectory.JoinState import im.vector.app.features.roomdirectory.JoinState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber import timber.log.Timber
@ -56,6 +64,56 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini
// Observe joined room (from the sync) // Observe joined room (from the sync)
observeRoomSummary() observeRoomSummary()
observeMembershipChanges() observeMembershipChanges()
if (initialState.shouldPeekFromServer) {
peekRoomFromServer()
}
}
private fun peekRoomFromServer() {
setState {
copy(peekingState = Loading())
}
viewModelScope.launch(Dispatchers.IO) {
val peekResult = tryOrNull {
awaitCallback<PeekResult> {
session.peekRoom(initialState.roomAlias ?: initialState.roomId, it)
}
}
when (peekResult) {
is PeekResult.Success -> {
setState {
copy(
roomId = peekResult.roomId,
avatarUrl = peekResult.avatarUrl,
roomAlias = peekResult.alias ?: initialState.roomAlias,
roomTopic = peekResult.topic,
homeServers = peekResult.viaServers,
peekingState = Success(PeekingState.FOUND)
)
}
}
is PeekResult.PeekingNotAllowed -> {
setState {
copy(
roomId = peekResult.roomId,
roomAlias = peekResult.alias ?: initialState.roomAlias,
homeServers = peekResult.viaServers,
peekingState = Success(PeekingState.NO_ACCESS)
)
}
}
PeekResult.UnknownAlias,
null -> {
setState {
copy(
peekingState = Success(PeekingState.NOT_FOUND)
)
}
}
}
}
} }
private fun observeRoomSummary() { private fun observeRoomSummary() {

View File

@ -16,13 +16,23 @@
package im.vector.app.features.roomdirectory.roompreview package im.vector.app.features.roomdirectory.roompreview
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.roomdirectory.JoinState import im.vector.app.features.roomdirectory.JoinState
import org.matrix.android.sdk.api.util.MatrixItem
data class RoomPreviewViewState( data class RoomPreviewViewState(
val peekingState: Async<PeekingState> = Uninitialized,
// The room id // The room id
val roomId: String = "", val roomId: String = "",
val roomAlias: String? = null, val roomAlias: String? = null,
val roomName: String? = null,
val roomTopic: String? = null,
val avatarUrl: String? = null,
val shouldPeekFromServer: Boolean = false,
/** /**
* Can be empty when the server is the current user's home server. * Can be empty when the server is the current user's home server.
*/ */
@ -36,6 +46,14 @@ data class RoomPreviewViewState(
constructor(args: RoomPreviewData) : this( constructor(args: RoomPreviewData) : this(
roomId = args.roomId, roomId = args.roomId,
roomAlias = args.roomAlias, roomAlias = args.roomAlias,
homeServers = args.homeServers homeServers = args.homeServers,
roomName = args.roomName,
roomTopic = args.topic,
avatarUrl = args.avatarUrl,
shouldPeekFromServer = args.peekFromServer
) )
fun matrixItem() : MatrixItem {
return MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl)
}
} }

View File

@ -54,6 +54,14 @@
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>
<ProgressBar
android:id="@+id/roomPreviewPeekingProgress"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="14dp"
android:layout_gravity="center"
android:background="?riotx_header_panel_background"
android:indeterminate="true" />
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -71,7 +79,7 @@
android:id="@+id/roomPreviewNoPreviewAvatar" android:id="@+id/roomPreviewNoPreviewAvatar"
android:layout_width="128dp" android:layout_width="128dp"
android:layout_height="128dp" android:layout_height="128dp"
android:layout_marginTop="123dp" android:layout_marginTop="60dp"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
<TextView <TextView

View File

@ -1711,7 +1711,8 @@
<string name="room_preview_no_preview">"This room can't be previewed"</string> <string name="room_preview_no_preview">"This room can't be previewed"</string>
<string name="room_preview_world_readable_room_not_supported_yet">"The preview of world-readable room is not supported yet in Element"</string> <string name="room_preview_world_readable_room_not_supported_yet">"The preview of world-readable room is not supported yet in Element"</string>
<string name="room_preview_not_found">This room is not accessible at this time.\nTry again later, or ask a room admin to check if you have access.</string>
<string name="room_preview_no_preview_join">"This room can't be previewed. Do you want to join it?"</string>
<string name="fab_menu_create_room">"Rooms"</string> <string name="fab_menu_create_room">"Rooms"</string>
<string name="fab_menu_create_chat">"Direct Messages"</string> <string name="fab_menu_create_chat">"Direct Messages"</string>