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 🙌:
- Add Setting Item to Change PIN (#2462)
- Improve room history visibility setting UX (#1579)
- Matrix.to deeplink custom scheme support
Bugfix 🐛:
- Fix cancellation of sending event (#2438)
- Double bottomsheet effect after verify with passphrase
- 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)
- 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.rest.DeviceInfo
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) {
@ -139,7 +140,7 @@ class RxSession(private val session: Session) {
}
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean): Single<Optional<String>> = singleBuilder {
searchOnServer: Boolean): Single<Optional<RoomAliasDescription>> = singleBuilder {
session.getRoomIdByAlias(roomAlias, searchOnServer, it)
}

View File

@ -25,6 +25,7 @@ interface PermalinkService {
companion object {
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 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.model.RoomMemberSummary
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.peeking.PeekResult
import org.matrix.android.sdk.api.util.Cancelable
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.
@ -120,7 +123,7 @@ interface RoomService {
*/
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean,
callback: MatrixCallback<Optional<String>>): Cancelable
callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable
/**
* Delete a room alias
@ -163,4 +166,16 @@ interface RoomService {
* @return a LiveData of the optional found room member
*/
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 com.zhuinden.monarchy.Monarchy
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.RoomService
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.RoomSummary
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.Optional
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.session.room.alias.DeleteRoomAliasTask
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.membership.RoomChangeMembershipStateDataSource
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.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.summary.RoomSummaryDataSource
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 roomIdByAliasTask: GetRoomIdByAliasTask,
private val deleteRoomAliasTask: DeleteRoomAliasTask,
private val resolveRoomStateTask: ResolveRoomStateTask,
private val peekRoomTask: PeekRoomTask,
private val roomGetter: RoomGetter,
private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@ -119,7 +126,7 @@ internal class DefaultRoomService @Inject constructor(
.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
.configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) {
this.callback = callback
@ -154,4 +161,20 @@ internal class DefaultRoomService @Inject constructor(
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>
/**
* Send a generic state events
* Send a generic state event
*
* @param roomId the room id.
* @param stateEventType the state event type
@ -195,7 +195,7 @@ internal interface RoomAPI {
@Body params: JsonDict): Call<Unit>
/**
* Send a generic state events
* Send a generic state event
*
* @param roomId the room id.
* @param stateEventType the state event type
@ -208,6 +208,13 @@ internal interface RoomAPI {
@Path("state_key") stateKey: String,
@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.
*

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.threepid.DefaultInviteThreePidTask
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.DefaultSetReadMarkersTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
@ -223,4 +227,10 @@ internal abstract class RoomModule {
@Binds
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 javax.inject.Inject
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> {
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<RoomAliasDescription>> {
data class Params(
val roomAlias: String,
val searchOnServer: Boolean
@ -42,21 +42,21 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor(
private val eventBus: EventBus
) : GetRoomIdByAliasTask {
override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional<String> {
var roomId = Realm.getInstance(monarchy.realmConfiguration).use {
override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional<RoomAliasDescription> {
val roomId = Realm.getInstance(monarchy.realmConfiguration).use {
RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId
}
return if (roomId != null) {
Optional.from(roomId)
Optional.from(RoomAliasDescription(roomId))
} else if (!params.searchOnServer) {
Optional.from<String>(null)
Optional.from(null)
} else {
roomId = tryOrNull("## Failed to get roomId from alias") {
val description = tryOrNull("## Failed to get roomId from alias") {
executeRequest<RoomAliasDescription>(eventBus) {
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
@JsonClass(generateAdapter = true)
internal data class RoomAliasDescription(
data class RoomAliasDescription(
/**
* 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
### 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
import org.matrix.android.sdk.internal.legacy.riot===3

View File

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

View File

@ -162,11 +162,27 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
private fun handleIntent(intent: Intent?) {
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,
buildTask = true)
buildTask = true
)
// .delay(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { isHandled ->
@ -345,11 +361,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
bugReporter.openBugReportScreen(this, false)
return true
}
R.id.menu_home_filter -> {
R.id.menu_home_filter -> {
navigator.openRoomsFiltering(this)
return true
}
R.id.menu_home_setting -> {
R.id.menu_home_setting -> {
navigator.openSettings(this)
return true
}
@ -390,5 +406,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
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.room.model.Membership
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.rx.rx
import javax.inject.Inject
@ -111,7 +112,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
private fun PermalinkData.RoomLink.getRoomId(): Single<Optional<String>> {
val session = activeSessionHolder.getSafeActiveSession()
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 {
Single.just(Optional.from(roomIdOrAlias))
}
@ -149,16 +150,28 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
navigator.openRoom(context, roomId, eventId, buildTask)
}
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)
if (roomSummary == null) {
// we don't know this room, try to peek
val roomPreviewData = RoomPreviewData(
roomId = roomId,
roomAlias = roomAlias,
peekFromServer = true,
buildTask = buildTask,
homeServers = permalinkData.viaParameters
)
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() {
val currentActivity = weakCurrentActivity?.get()
if (Alerter.isShowing || currentActivity == null) {
if (Alerter.isShowing || currentActivity == null || currentActivity.isDestroyed) {
// will retry later
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 avatarUrl: String? = null,
val homeServers: List<String> = emptyList(),
val peekFromServer: Boolean = false,
val buildTask: Boolean = false
) : Parcelable {
val matrixItem: MatrixItem

View File

@ -20,6 +20,8 @@ import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
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.roomdirectory.JoinState
import kotlinx.android.synthetic.main.fragment_room_preview_no_preview.*
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject
/**
@ -48,22 +51,6 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
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 {
override fun onButtonClicked() {
@ -100,7 +87,62 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
// Quit this screen
requireActivity().finish()
// 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
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
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.VectorViewModel
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.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue
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.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.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx
import timber.log.Timber
@ -56,6 +64,56 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini
// Observe joined room (from the sync)
observeRoomSummary()
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() {

View File

@ -16,13 +16,23 @@
package im.vector.app.features.roomdirectory.roompreview
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.roomdirectory.JoinState
import org.matrix.android.sdk.api.util.MatrixItem
data class RoomPreviewViewState(
val peekingState: Async<PeekingState> = Uninitialized,
// The room id
val roomId: String = "",
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.
*/
@ -36,6 +46,14 @@ data class RoomPreviewViewState(
constructor(args: RoomPreviewData) : this(
roomId = args.roomId,
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>
<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
android:layout_width="match_parent"
@ -71,7 +79,7 @@
android:id="@+id/roomPreviewNoPreviewAvatar"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_marginTop="123dp"
android:layout_marginTop="60dp"
tools:src="@tools:sample/avatars" />
<TextView

View File

@ -1711,7 +1711,8 @@
<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_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_chat">"Direct Messages"</string>